@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,188 @@
1
+ # -*- coding: utf-8 -*-
2
+ """CCG 六要素「形态口径一致性」验收(2026-09-28)。
3
+
4
+ 运行:python -m md_cg.test_ccg_form_parity
5
+
6
+ 为什么需要这一套(本套的由来,是一个真实的口径分叉):
7
+ 六要素标题存在两种书写形态——带冒号 `# 生效条件:<值>` 与不带冒号
8
+ `# 生效条件` 后换行接值。写入闸门 `data/policy.json` 的必需正则
9
+ `(?m)^#\\s*生效条件` **不要求冒号**,而检索面判据 `nodefile.ccg_completeness`
10
+ 原先**要求冒号**才计入「已声明」。二者对同一条正文给出相反结论:归档过闸
11
+ (ACCEPT)后到检索路由被判 BLINDSPOT「CCG 要素不全」——归档看着写成了,
12
+ 检索面却当它没声明。存量实测 6905 件含六要素节点中 88 件处于该状态。
13
+
14
+ 判据收敛在**单点** `nodefile.ccg_mark_present` / `nodefile.ccg_field_value`
15
+ (冒号可有可无),`mdcos._ccg_field`、`ccgc._has_ccg_line`、
16
+ `consolidate._has_ccg_line`、`tasks._field_line` 一律委托该单点。
17
+
18
+ 本套的六组断言:
19
+ ① 两形态都判齐(形态不改变「齐不齐」)
20
+ ② 缺要素仍判不全(放宽的是标点,不是要求——判别力自证,两种形态各测)
21
+ ③ 写入闸门与检索面**结论一致**(读真源 data/policy.json 复算,直接钉住分叉)
22
+ ④ 取值面同源:无冒号的「值」= 标题后首个非空非标题行
23
+ ⑤ 四个副本函数与单点结论一致(防未来再长出第五、第六套口径)
24
+ ⑥ 写读闭环:无冒号节点经 upsert 后取得到新值(不出现「写进去读不出」)
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import io
29
+ import json
30
+ import os
31
+ import re
32
+ import sys
33
+
34
+ from . import ccgc, consolidate, nodefile, tasks
35
+ from .mdcos import _ccg_field
36
+
37
+ PASS = FAIL = 0
38
+ FAILS = []
39
+
40
+
41
+ def ok(cond, label):
42
+ global PASS, FAIL
43
+ if cond:
44
+ PASS += 1
45
+ print(" [PASS] %s" % label)
46
+ else:
47
+ FAIL += 1
48
+ FAILS.append(label)
49
+ print(" [FAIL] %s" % label)
50
+
51
+
52
+ MARKS = ("功能名", "生效条件", "子功能", "执行", "验证方式", "不适用条件")
53
+
54
+ # ① 带冒号形态(既有惯例形态)
55
+ COLON = "".join("# %s:值_%s\n" % (m, m) for m in MARKS)
56
+ # ① 不带冒号形态(policy 正则接受,标题行 + 换行接值)
57
+ NONCOLON = "".join("# %s\n值_%s\n" % (m, m) for m in MARKS)
58
+ # ① 混形态(一半带冒号一半不带)
59
+ MIXED = "".join(("# %s:值_%s\n" if i % 2 == 0 else "# %s\n值_%s\n") % (m, m)
60
+ for i, m in enumerate(MARKS))
61
+
62
+
63
+ def _drop(body: str, mark: str) -> str:
64
+ """删掉某要素的标题行(连带其值行)——用于②的判别力自证。"""
65
+ out = []
66
+ skip_next = False
67
+ for ln in body.splitlines(True):
68
+ if skip_next:
69
+ skip_next = False
70
+ continue
71
+ if re.match(r"^#\s*%s\s*[::]?\s*$" % re.escape(mark), ln.rstrip("\n")):
72
+ skip_next = True
73
+ continue
74
+ if ln.startswith("# %s:" % mark) or ln.startswith("# %s:" % mark):
75
+ continue
76
+ out.append(ln)
77
+ return "".join(out)
78
+
79
+
80
+ def _policy_required() -> list:
81
+ """读**真源**策略文件的必需正则(相对本模块定位,不依赖 cwd)。"""
82
+ root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
83
+ path = os.path.join(root, "data", "policy.json")
84
+ with io.open(path, encoding="utf-8") as fh:
85
+ return [re.compile(p) for p in json.load(fh)["required"]]
86
+
87
+
88
+ def main() -> int:
89
+ print("== ① 两形态都判齐 ==")
90
+ for name, body in (("带冒号", COLON), ("无冒号", NONCOLON), ("混形态", MIXED)):
91
+ comp = nodefile.ccg_completeness(body)
92
+ ok(comp["complete"] and sorted(comp["required_present"]) == sorted(MARKS),
93
+ "①%s:六要素判齐(present=%d/%d)" % (name, len(comp["required_present"]), len(MARKS)))
94
+ ok(abs(comp["ratio"] - 1.0) < 1e-9, "①%s:ratio=1.0(%s)" % (name, comp["ratio"]))
95
+
96
+ print("== ② 缺要素仍判不全(放宽的是标点,不是要求)==")
97
+ for name, body in (("带冒号", COLON), ("无冒号", NONCOLON)):
98
+ for mark in MARKS:
99
+ broken = _drop(body, mark)
100
+ comp = nodefile.ccg_completeness(broken)
101
+ ok(not comp["complete"] and mark not in comp["required_present"],
102
+ "②%s 缺「%s」→ 判不全" % (name, mark))
103
+ ok(not nodefile.ccg_completeness("")["complete"], "②空正文 → 判不全")
104
+ ok(not nodefile.ccg_completeness("# 生效条件\n只要这一条\n")["complete"],
105
+ "②只声明一条 → 判不全(不冒充齐全)")
106
+
107
+ print("== ③ 写入闸门与检索面结论一致(读真源 policy 复算)==")
108
+ pats = _policy_required()
109
+ for name, body in (("带冒号", COLON), ("无冒号", NONCOLON), ("混形态", MIXED),
110
+ ("空正文", ""), ("只一条", "# 生效条件:x\n")):
111
+ gate = all(p.search(body) for p in pats)
112
+ retr = nodefile.ccg_completeness(body)["complete"]
113
+ ok(gate == retr, "③%s:闸门=%s 检索面=%s(必须相等)" % (name, gate, retr))
114
+ for mark in MARKS:
115
+ for name, body in (("带冒号", COLON), ("无冒号", NONCOLON)):
116
+ broken = _drop(body, mark)
117
+ gate = all(p.search(broken) for p in pats)
118
+ retr = nodefile.ccg_completeness(broken)["complete"]
119
+ ok(gate == retr and not retr,
120
+ "③%s 缺「%s」:两侧同判不全" % (name, mark))
121
+ # 边界形态:二级标题 / 缩进标题 / 无空格 —— 两侧必须同判(宽松方向也要一致)。
122
+ # 注意按**该要素自身**的那条必需正则比对(拿「全六条」的结论比「单条」判据
123
+ # 是错的口径——空正文与二级标题会因两侧同假而蒙对,暴露不出真差异)。
124
+ for name, line in (("二级标题", "## 生效条件:x\n"),
125
+ ("缩进标题", " # 生效条件:x\n"),
126
+ ("无空格", "#生效条件:x\n"),
127
+ ("制表符", "#\t生效条件:x\n")):
128
+ pat = next(p for p in pats if "生效条件" in p.pattern)
129
+ gate = bool(pat.search(line))
130
+ present = nodefile.ccg_mark_present(line, "生效条件")
131
+ ok(gate == present,
132
+ "③%s(%r):闸门=%s 判据=%s(必须相等)" % (name, line.strip(), gate, present))
133
+ # 全形态 × 全要素的逐条一致性(比「整体 complete」更细,能定位到具体要素)
134
+ for name, body in (("带冒号", COLON), ("无冒号", NONCOLON), ("混形态", MIXED)):
135
+ for mark in MARKS:
136
+ pat = next(p for p in pats if mark in p.pattern)
137
+ ok(bool(pat.search(body)) == nodefile.ccg_mark_present(body, mark),
138
+ "③%s 逐要素一致:「%s」" % (name, mark))
139
+
140
+ print("== ④ 取值面:无冒号的值 = 标题后首个非空非标题行 ==")
141
+ ok(nodefile.ccg_field_value(COLON, "功能名") == "值_功能名",
142
+ "④带冒号:取行内值")
143
+ ok(nodefile.ccg_field_value(NONCOLON, "功能名") == "值_功能名",
144
+ "④无冒号:取下一行值")
145
+ ok(nodefile.ccg_field_value("# 执行\n\n真实值行\n后文\n", "执行") == "真实值行",
146
+ "④无冒号:跳过空行取首个非空行")
147
+ ok(nodefile.ccg_field_value("# 功能名\n# 生效条件:x\n", "功能名") == "",
148
+ "④无冒号且下一行又是标题:取空串而非吞掉下一个要素")
149
+ ok(nodefile.ccg_field_value(COLON, "不存在的字段") is None,
150
+ "④无该字段行 → None")
151
+
152
+ print("== ⑤ 四个副本与单点结论一致(防再长出口径分叉)==")
153
+ for name, body in (("带冒号", COLON), ("无冒号", NONCOLON)):
154
+ for mark in ("功能名", "生效条件", "执行"):
155
+ single = nodefile.ccg_mark_present(body, mark)
156
+ ok(ccgc._has_ccg_line(body, mark) == single,
157
+ "⑤ccgc._has_ccg_line(%s,%s) 与单点一致" % (name, mark))
158
+ ok(consolidate._has_ccg_line(body, mark) == single,
159
+ "⑤consolidate._has_ccg_line(%s,%s) 与单点一致" % (name, mark))
160
+ ok(bool(_ccg_field(body, mark)) == single or not single,
161
+ "⑤mdcos._ccg_field(%s,%s) 与单点一致" % (name, mark))
162
+ ok(tasks._field_line(body, mark) == (nodefile.ccg_field_value(body, mark) or ""),
163
+ "⑤tasks._field_line(%s,%s) 与单点同值" % (name, mark))
164
+
165
+ print("== ⑥ 写读闭环:无冒号节点 upsert 后取得到新值 ==")
166
+ for enc, upsert in (("ccgc", ccgc._upsert_ccg_line), ("consolidate", consolidate._upsert_ccg_line)):
167
+ after = upsert(NONCOLON, "生效条件", "新条件")
168
+ ok(_ccg_field(after, "生效条件") == "新条件",
169
+ "⑥%s:无冒号节点 upsert 后 mdcos 读出「新条件」" % enc)
170
+ ok(nodefile.ccg_field_value(after, "生效条件") == "新条件",
171
+ "⑥%s:同一次写单点读出「新条件」" % enc)
172
+ ok(nodefile.ccg_completeness(after)["complete"],
173
+ "⑥%s:upsert 后仍判齐" % enc)
174
+ try:
175
+ upsert(NONCOLON, "生效条件", "多行\n注入")
176
+ ok(False, "⑥%s:值含换行应被拒(N208)" % enc)
177
+ except ValueError:
178
+ ok(True, "⑥%s:值含换行被拒(N208 fail-closed)" % enc)
179
+
180
+ print()
181
+ print("ccg_form_parity: PASS=%d FAIL=%d" % (PASS, FAIL))
182
+ if FAILS:
183
+ print("FAILS: " + "; ".join(FAILS))
184
+ return 1 if FAIL else 0
185
+
186
+
187
+ if __name__ == "__main__":
188
+ sys.exit(main())
@@ -23,7 +23,7 @@ import tempfile
23
23
  import threading
24
24
  import time
25
25
 
26
- from . import audit, backfill, ccgc, nodefile, units
26
+ from . import audit, backfill, ccgc, nodefile, tokens, units
27
27
  from .mdcos import MdCGOS
28
28
 
29
29
  PASS = FAIL = 0
@@ -126,6 +126,13 @@ def _import_mcp():
126
126
  def main():
127
127
  global PASS, FAIL
128
128
  tmp = tempfile.mkdtemp(prefix="mdcg_ccgc_")
129
+ # N176 契约:落库准入须令牌验证方——本测试自备哑令牌环境(临时 MDCG_TOKEN_FILE)
130
+ _old_tf = os.environ.get(tokens.TOKEN_FILE_ENV)
131
+ os.environ[tokens.TOKEN_FILE_ENV] = os.path.join(tmp, "tokens.json")
132
+ tk_reviewer = tokens.issue("designer", actor="designer",
133
+ path=os.environ[tokens.TOKEN_FILE_ENV])["token"]
134
+ tk_reviewer_ext = tokens.issue("verify", actor="external-reviewer",
135
+ path=os.environ[tokens.TOKEN_FILE_ENV])["token"]
129
136
  try:
130
137
  root = os.path.join(tmp, "root")
131
138
  os.makedirs(root, exist_ok=True)
@@ -192,8 +199,11 @@ def main():
192
199
  a_defer = ccgc.attest("tgt", ccgc.DEFER, "designer", "agent:a1", cg=cg)
193
200
  ok((not a_defer.ok) and "E042" in a_defer.error, "V7b DEFER 不构成签章")
194
201
  a_ok = ccgc.attest("tgt", ccgc.ACCEPT, "designer", "agent:a1",
195
- evidence="设计者核对原文一致", cg=cg)
202
+ evidence="设计者核对原文一致", cg=cg,
203
+ verifier_token=tk_reviewer)
196
204
  ok(a_ok.ok and bool(a_ok.token), "V7c 编外验证方 ACCEPT → 有效签章")
205
+ ok(a_ok.verifier_identity == "token", "V7d 令牌验证方 identity=token"
206
+ "(N176:self-reported 不再构成落库准入)")
197
207
 
198
208
  # ---------- V8 无章不写 ----------
199
209
  l0 = ccgc.link(r, None, cg=cg, apply=True)
@@ -283,7 +293,7 @@ def main():
283
293
  lz = ccgc.link_pending(cg, "tgt", apply=True, actor="agent:a1")
284
294
  ok(lz.ok and lz.written == 6, "V16f 读回原件+签章 → 落库成功")
285
295
  ok(not os.path.isfile(sv["path"]), "V16g 落库成功即清 pending(暂存非存档)")
286
- ok(not any(ccgc.PENDING_DIR in str(k) for k in (cg.index.get("nodes") or {})),
296
+ ok(not any(ccgc.PENDING_DIR in str(k) for k in (list(cg.index.get("nodes") or {}))),
287
297
  "V16h pending 不进索引(冷区对检索隐身)")
288
298
 
289
299
  # ---------- V17 闸门:只认编外裁决,绝不假装通过 ----------
@@ -396,6 +406,17 @@ def main():
396
406
  ok(c5["state"] == units.HIVE and c5["verdict"] == "ACCEPT" and c5["passed"] is True,
397
407
  "V20g review 取回裁决:ok/verdict/passed 三者分离")
398
408
  ok(bool((c5.get("attest") or {}).get("ok")), "V20h 拿到裁决即自动签章")
409
+ c5b = mcp._ccg_call(cg, {"ccg": {"action": "link", "node_id": "tgt",
410
+ "apply": True}})
411
+ ok((not c5b["ok"]) and "E052" in str((c5b.get("link") or {}).get("errors")),
412
+ "V20n 无令牌自报签章 → link 拒绝(N176 E052,不落 knowledge 层)")
413
+ c5c = mcp._ccg_call(cg, {"ccg": {"action": "attest", "node_id": "tgt",
414
+ "verdict": "ACCEPT",
415
+ "verifier": "external-reviewer",
416
+ "verifier_token": tk_reviewer_ext,
417
+ "evidence": "令牌验证方复核"}})
418
+ ok(c5c["ok"] and (c5c.get("attest") or {}).get("verifier_identity") == "token",
419
+ "V20o 令牌验证方重签章 → identity=token(挂回 pending 待落库)")
399
420
  c7 = mcp._ccg_call(cg, {"ccg": {"action": "review", "node_id": "tgt",
400
421
  "jobs": jobs, "blocking": False}})
401
422
  ok(c7["state"] == units.HIVE and c7["verdict"] is None and c7["passed"] is False,
@@ -421,6 +442,10 @@ def main():
421
442
  os.environ[units.ENV_EXE] = _old_exe
422
443
 
423
444
  finally:
445
+ if _old_tf is None:
446
+ os.environ.pop(tokens.TOKEN_FILE_ENV, None)
447
+ else:
448
+ os.environ[tokens.TOKEN_FILE_ENV] = _old_tf
424
449
  print()
425
450
  print("ccgc: PASS=%d FAIL=%d" % (PASS, FAIL))
426
451
  if FAILS:
@@ -33,7 +33,7 @@ import shutil
33
33
  import sys
34
34
  import tempfile
35
35
 
36
- from . import ccgc, consolidate, crosscheck, vision_evidence
36
+ from . import ccgc, consolidate, crosscheck, tokens, vision_evidence
37
37
  from .mdcos import MdCGOS
38
38
 
39
39
  PASS = FAIL = 0
@@ -181,9 +181,21 @@ def g2_link(tmp):
181
181
  r = ccgc.compile_dialog(DIALOG, "gd_link", "agent:a1",
182
182
  slots=SLOTS, marks=MARKS, cg=a)
183
183
  ok(r.success, "G2a 编译成功")
184
+ # N176 契约:落库准入须令牌验证方——哑令牌临时文件 + 临时 MDCG_TOKEN_FILE
185
+ # (attest→verify_token 走环境变量定位令牌库,绝不触真实令牌库)
186
+ tok_env = os.path.join(tmp, "g2_tokens.json")
187
+ _old_tf = os.environ.get(tokens.TOKEN_FILE_ENV)
188
+ os.environ[tokens.TOKEN_FILE_ENV] = tok_env
189
+ tk = tokens.issue("designer", actor="designer", path=tok_env)["token"]
184
190
  a_ok = ccgc.attest("gd_link", ccgc.ACCEPT, "designer", "agent:a1",
185
- evidence="守卫:候选与对话原文一致", cg=a)
186
- ok(a_ok.ok, "G2b 编外验证方签章")
191
+ evidence="守卫:候选与对话原文一致", cg=a,
192
+ verifier_token=tk)
193
+ if _old_tf is None:
194
+ os.environ.pop(tokens.TOKEN_FILE_ENV, None)
195
+ else:
196
+ os.environ[tokens.TOKEN_FILE_ENV] = _old_tf
197
+ ok(a_ok.ok and a_ok.verifier_identity == "token", "G2b 编外验证方签章"
198
+ "(N176:令牌凭据,self-reported 不构成落库准入)")
187
199
  l = ccgc.link(r, a_ok, cg=a, apply=True)
188
200
  ok(l.ok and l.written == 6, "G2c link 写入六行")
189
201
  fm, _c = _disk(root, "gd_link")
@@ -308,7 +320,8 @@ def _g89_root(tmp):
308
320
  a = MdCGOS(root)
309
321
  a.add("gd_pro",
310
322
  "# 功能名:待提升节点\n# 生效条件:问提升\n# 子功能:说明提升\n"
311
- "# 执行:按热度\n# 不适用条件:问无关\n",
323
+ "# 执行:按热度\n# 验证方式:编译器/静态检查通过\n"
324
+ "# 不适用条件:问无关\n",
312
325
  layer="contextual", merge_count=2)
313
326
  a.flush()
314
327
  return root
@@ -0,0 +1,233 @@
1
+ # -*- coding: utf-8 -*-
2
+ """md_cg · H2:会话视图的读侧归一(「写进去能查出来」)
3
+
4
+ 病灶(端到端实测):写侧落盘值 = `_normalize_session(请求声明值)`
5
+ (`MdCGSecure._attribution` 取 `cg.session`,而 `cg.session` 由 `call_tool`
6
+ → `_declared_session` 归一);读侧 `stg.timeline` 却拿**未归一的原值**做
7
+ 等值比较 ⇒ 同一条记忆「写进去查不出」——DSH 形态的会话 id 在会话根下不
8
+ 存在时写侧落 `anonymous`,读侧按 `session-…` 精确匹配得 `count=0`。
9
+
10
+ 修复(一处归一,改在视图层):`stg._view_session` 只对「具体会话值」这一态
11
+ 消费 `mcp_server._normalize_session`(真源唯一,**不重写实现**),三态语义
12
+ (None / "" / "*" = 跨会话)逐位不变。
13
+
14
+ 覆盖
15
+ ----
16
+ A 端到端:MCP 写一条声明了 DSH 形态 session 的记忆 → 落盘值是归一值,
17
+ 再用**原始声明值**读回来必须命中(H2 的核心断言)
18
+ B 归一真源唯一:读侧消费的确实是 `mcp_server._normalize_session`
19
+ (哨兵替换法——防后续有人在 stg 里重写一把尺,那必然造出第三种不等值)
20
+ C 三态视图不回归:None / "" / "*" 仍跨会话;具体非 DSH 值仍精确匹配
21
+ D fail-soft:会话根不可读(OSError)时读写两侧都保原值,仍能读回
22
+ E 不越界:cg 侧读路径(search)不吃请求 session(issue #35 定稿:身份不可自报)
23
+
24
+ 运行:python -m md_cg.test_h2_session_view_norm
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import os
29
+ import shutil
30
+ import sys
31
+ import tempfile
32
+
33
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
34
+
35
+ # Windows cmd 默认 GBK:中文打印即 UnicodeEncodeError,且崩在断言之后、报告之前。
36
+ if hasattr(sys.stdout, "reconfigure"):
37
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
38
+
39
+ from md_cg.mdcos import MdCGSecure
40
+ from md_cg.security import DEFAULT_SENSITIVITY, Principal
41
+ from md_cg import mcp_server as ms
42
+
43
+ PASS = 0
44
+ FAILS = []
45
+
46
+ # 合成会话 id(uuid4 形态的 DSH 会话;非真实凭据,仅形态样本)
47
+ DSH_ID = "session-11111111-2222-3333-4444-555555555555"
48
+ ENV_KEY = "MDCG_DSH_SESSIONS_ROOT"
49
+
50
+
51
+ def check(name, cond, detail=""):
52
+ global PASS
53
+ if cond:
54
+ PASS += 1
55
+ print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
56
+ else:
57
+ FAILS.append(name)
58
+ print(f" [FAIL] {name} · {str(detail)[:240]}")
59
+
60
+
61
+ def _p(actor, session):
62
+ return Principal(actor=actor, clearance=DEFAULT_SENSITIVITY,
63
+ can_write=True, can_admin=True, role="designer",
64
+ session=session, harness="test-harness")
65
+
66
+
67
+ def _remember(cg, text, session):
68
+ """经 MCP 写入口落一条(gated 路径,与插件自动记忆同一条链)。"""
69
+ return ms.call_tool(cg, "mdcg_remember", {
70
+ "content": text, "layer": "contextual", "session": session,
71
+ "gated": True, "importance": 0.6})
72
+
73
+
74
+ def _timeline(cg, session):
75
+ return ms.call_tool(cg, "stg", {"op": "timeline", "limit": 20, "session": session})
76
+
77
+
78
+ def test_a(root, sessions_root):
79
+ """A 写进去能查出来(核心断言)。"""
80
+ print("\n[A] 端到端:DSH 形态声明 → 落盘归一值 → 原值读回命中")
81
+ old = os.environ.get(ENV_KEY)
82
+ os.environ[ENV_KEY] = sessions_root # 根可读、但无该会话
83
+ try:
84
+ # 前置条件断言:本用例的环境确实触发归一,否则下面的断言是空的
85
+ check("A0 前置:该环境下 DSH 形态归一为 anonymous",
86
+ ms._normalize_session(DSH_ID) == "anonymous",
87
+ ms._normalize_session(DSH_ID))
88
+ cg = MdCGSecure(root, principal=_p("alice", "sess_A"))
89
+ res = _remember(cg, "阿尔法 会话视图归一 端到端写入样本", DSH_ID)
90
+ nid = res.get("written") or res.get("node_id")
91
+ check("A1 写入落盘(ACCEPT 且有节点 id)",
92
+ bool(nid) and res.get("verdict") == "ACCEPT",
93
+ (res.get("verdict"), nid))
94
+ fm = (cg.get(nid) or {}).get("frontmatter") or {}
95
+ check("A2 落盘 session = 归一声明值(写侧口径)",
96
+ fm.get("session") == "anonymous", fm.get("session"))
97
+ tl = _timeline(cg, DSH_ID)
98
+ check("A3 用**原始声明值**读回命中(H2 核心:写进去查得出)",
99
+ tl.get("count") == 1 and [i["id"] for i in tl.get("items", [])] == [nid],
100
+ (tl.get("count"), [i["id"] for i in tl.get("items", [])[:3]]))
101
+ check("A4 返回体 session = 归一后的生效值(诚实回带)",
102
+ tl.get("session") == "anonymous", tl.get("session"))
103
+ finally:
104
+ if old is None:
105
+ os.environ.pop(ENV_KEY, None)
106
+ else:
107
+ os.environ[ENV_KEY] = old
108
+
109
+
110
+ def test_b(root, sessions_root):
111
+ """B 归一真源唯一(哨兵法)。"""
112
+ print("\n[B] 读侧消费 mcp_server 真源(不重写一把尺)")
113
+ old = os.environ.get(ENV_KEY)
114
+ os.environ[ENV_KEY] = sessions_root
115
+ orig = ms._normalize_session
116
+ try:
117
+ cg = MdCGSecure(root, principal=_p("alice", "sess_A"))
118
+ ms._normalize_session = lambda raw: "SENTINEL_" + str(raw)
119
+ out = _timeline(cg, "probe")
120
+ check("B1 具体值经 mcp_server._normalize_session(哨兵可见)",
121
+ out.get("session") == "SENTINEL_probe", out.get("session"))
122
+ star = _timeline(cg, "*")
123
+ check("B2 \"*\" 不经归一(跨会话开关是不变量)",
124
+ star.get("session") is None, star.get("session"))
125
+ blank = _timeline(cg, "")
126
+ check("B3 空串不经归一(缺省语义不变量)",
127
+ blank.get("session") is None, blank.get("session"))
128
+ finally:
129
+ ms._normalize_session = orig
130
+ if old is None:
131
+ os.environ.pop(ENV_KEY, None)
132
+ else:
133
+ os.environ[ENV_KEY] = old
134
+
135
+
136
+ def test_c(root, sessions_root):
137
+ """C 三态视图不回归 + 具体值精确匹配。"""
138
+ print("\n[C] 视图三态与精确匹配不回归")
139
+ old = os.environ.get(ENV_KEY)
140
+ os.environ[ENV_KEY] = sessions_root
141
+ try:
142
+ cg = MdCGSecure(root, principal=_p("alice", "sess_A"))
143
+ _remember(cg, "贝塔 属于 会话 sess_A 的知识点", "sess_A")
144
+ a = _timeline(cg, "sess_A")
145
+ check("C1 具体非 DSH 值精确匹配(原样采用)",
146
+ a.get("count") == 1 and a.get("session") == "sess_A",
147
+ (a.get("count"), a.get("session")))
148
+ z = _timeline(cg, "sess_Z")
149
+ check("C2 未知会话为空(不因缺省而放行)",
150
+ z.get("count") == 0, z.get("count"))
151
+ for label, val in (("缺省", None), ("空串", ""), ("星号", "*")):
152
+ out = _timeline(cg, val)
153
+ check(f"C3 {label}仍为跨会话视图(返回体 session=None 且非空)",
154
+ out.get("session") is None and out.get("count") >= 1,
155
+ (out.get("session"), out.get("count")))
156
+ finally:
157
+ if old is None:
158
+ os.environ.pop(ENV_KEY, None)
159
+ else:
160
+ os.environ[ENV_KEY] = old
161
+
162
+
163
+ def test_d(root, tmp):
164
+ """D fail-soft:根不可读 → 两侧都保原值。"""
165
+ print("\n[D] 会话根不可读(OSError)时 fail-soft 读写一致")
166
+ old = os.environ.get(ENV_KEY)
167
+ os.environ[ENV_KEY] = os.path.join(tmp, "no_such_root_dir")
168
+ try:
169
+ check("D1 根不可读 → 归一保原值",
170
+ ms._normalize_session(DSH_ID) == DSH_ID,
171
+ ms._normalize_session(DSH_ID))
172
+ cg = MdCGSecure(root, principal=_p("alice", "sess_A"))
173
+ res = _remember(cg, "伽马 根不可读 场景写入样本", DSH_ID)
174
+ nid = res.get("written") or res.get("node_id")
175
+ fm = (cg.get(nid) or {}).get("frontmatter") or {}
176
+ check("D2 落盘保原值(不因环境差异丢会话标记)",
177
+ fm.get("session") == DSH_ID, fm.get("session"))
178
+ tl = _timeline(cg, DSH_ID)
179
+ check("D3 同环境下读回命中(读写两侧同尺)",
180
+ tl.get("count") == 1, tl.get("count"))
181
+ finally:
182
+ if old is None:
183
+ os.environ.pop(ENV_KEY, None)
184
+ else:
185
+ os.environ[ENV_KEY] = old
186
+
187
+
188
+ def test_e(root, sessions_root):
189
+ """E 不越界:cg 侧读路径不吃请求 session(issue #35 定稿)。"""
190
+ print("\n[E] 不越界:search 的 session 不构成授权/过滤豁免")
191
+ old = os.environ.get(ENV_KEY)
192
+ os.environ[ENV_KEY] = sessions_root
193
+ try:
194
+ cg = MdCGSecure(root, principal=_p("alice", "sess_A"))
195
+ _remember(cg, "德尔塔 共享档检索样本", "sess_A")
196
+ res_default, _ = cg.search("德尔塔", judge=False, record=False)
197
+ res_dsh, _ = cg.search("德尔塔", session=DSH_ID, judge=False, record=False)
198
+ ids_default = {r[0].get("id") if isinstance(r, tuple) else r.get("id")
199
+ for r in res_default}
200
+ ids_dsh = {r[0].get("id") if isinstance(r, tuple) else r.get("id")
201
+ for r in res_dsh}
202
+ check("E1 传 DSH 形态 session 与缺省结果一致(cg 侧不经 _view_session)",
203
+ ids_default == ids_dsh and bool(ids_default),
204
+ (sorted(ids_default)[:3], sorted(ids_dsh)[:3]))
205
+ finally:
206
+ if old is None:
207
+ os.environ.pop(ENV_KEY, None)
208
+ else:
209
+ os.environ[ENV_KEY] = old
210
+
211
+
212
+ def main():
213
+ tmp = tempfile.mkdtemp(prefix="mdcg_h2_")
214
+ sessions_root = os.path.join(tmp, "sessions")
215
+ os.makedirs(sessions_root, exist_ok=True)
216
+ try:
217
+ test_a(os.path.join(tmp, "root_a"), sessions_root)
218
+ test_b(os.path.join(tmp, "root_b"), sessions_root)
219
+ test_c(os.path.join(tmp, "root_c"), sessions_root)
220
+ test_d(os.path.join(tmp, "root_d"), tmp)
221
+ test_e(os.path.join(tmp, "root_e"), sessions_root)
222
+ finally:
223
+ shutil.rmtree(tmp, ignore_errors=True)
224
+
225
+ print("\n通过 %d / 失败 %d" % (PASS, len(FAILS)))
226
+ if FAILS:
227
+ print("失败项:" + ", ".join(FAILS))
228
+ return 1
229
+ return 0
230
+
231
+
232
+ if __name__ == "__main__":
233
+ sys.exit(main())