@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
@@ -43,63 +43,103 @@ def main():
43
43
 
44
44
  # 派发原语:spec 落验证实例 jobs 目录(非阻塞)
45
45
  vjobs = os.path.join(tmp, "vjobs")
46
- rep = dispatch_verify_job(vjobs, iter_id, subject_fingerprint="fp_main")
46
+ rep = dispatch_verify_job(vjobs, iter_id, subject_fingerprint="e" * 64)
47
47
  check("1b 派发写 spec+status 且非阻塞返回",
48
48
  rep.get("ok") is True
49
49
  and os.path.isfile(os.path.join(rep["dispatched"], "spec.json")))
50
50
 
51
51
  # runner 冒烟:env 注入轻量探针命令 + 身份/冻结/subject_fp
52
- env = dict(os.environ)
53
- env.update({
54
- "HIVE_INSTANCE": "verifier", "HIVE_ROLE": "verifier",
55
- "SUBJECT_FP": "fp_main", "ITER_ID": iter_id,
56
- })
57
- # freeze 的 digest 基于真判据面——runner 内 A3 用同一 digest,须一致:
58
- # runner 在本 repo 内跑,判据面未变 → A3 成立
59
- r = subprocess.run([sys.executable, os.path.join(repo, "hive",
60
- "verify_runner.py"),
61
- iter_id, "--smoke"], env=env, capture_output=True,
62
- text=True, encoding="utf-8", errors="replace",
63
- cwd=tmp) # cwd≠repo:验证 runner 以自身位置锚定
64
- out = json.loads(r.stdout)
65
- check("2a runner 产出 verdict(ok 结构)",
66
- out.get("ok") is True and "valid" in out, r.stdout[:160])
67
- vp = os.path.join(repo, "hive", "interop", iter_id, "verdict.json")
68
- v = json.load(open(vp, encoding="utf-8"))
69
- check("2b 三断言全过 → valid=true 且 verdict=pass",
70
- v.get("valid") is True and v.get("verdict") == "pass",
71
- str(v.get("details"))[:160])
72
- check("2c 套件计数经宽松解析累加(3+4=7)",
73
- v.get("passed") == 7, f"passed={v.get('passed')}")
52
+ # hive/ 是**源码树子系统**(出货包 files 不含它)→ 依赖 runner 的 2a~3c
53
+ # 如实 SKIP,并改为钉住缺件时的**新契约**(2026-09-24 修复):
54
+ # 派发必须透出 warning,冻结凭证必须记录缺失判据面组。
55
+ hive_runner = os.path.join(repo, "hive", "verify_runner.py")
56
+ if not os.path.isfile(hive_runner):
57
+ check("2* 缺 hive runner 时派发透出 warning(不假装能跑)",
58
+ bool(rep.get("warning")), str(rep.get("warning"))[:160])
59
+ _fr = json.load(open(os.path.join(repo, "hive", "interop", iter_id,
60
+ "frozen.json"), encoding="utf-8"))
61
+ check("4* 冻结凭证记录缺失判据面组(不静默少算)",
62
+ isinstance(_fr.get("missing_patterns"), list)
63
+ and bool(_fr["missing_patterns"]),
64
+ str(_fr.get("missing_patterns")))
65
+ print(" SKIP 2a~3c(需要 hive/verify_runner.py,源码树子系统)")
66
+ else:
67
+ env = dict(os.environ)
68
+ env.update({
69
+ "HIVE_INSTANCE": "verifier", "HIVE_ROLE": "verifier",
70
+ "SUBJECT_FP": "e" * 64, "ITER_ID": iter_id,
71
+ })
72
+ # freeze 的 digest 基于真判据面——runner 内 A3 用同一 digest,须一致:
73
+ # runner 在本 repo 内跑,判据面未变 → A3 成立
74
+ r = subprocess.run([sys.executable, hive_runner,
75
+ iter_id, "--smoke"], env=env, capture_output=True,
76
+ text=True, encoding="utf-8", errors="replace",
77
+ cwd=tmp) # cwd≠repo:验证 runner 以自身位置锚定
78
+ out = json.loads(r.stdout)
79
+ check("2a runner 产出 verdict(ok 结构)",
80
+ out.get("ok") is True and "valid" in out, r.stdout[:160])
81
+ vp = os.path.join(repo, "hive", "interop", iter_id, "verdict.json")
82
+ v = json.load(open(vp, encoding="utf-8"))
83
+ check("2b 三断言全过 → valid=true 且 verdict=pass",
84
+ v.get("valid") is True and v.get("verdict") == "pass",
85
+ str(v.get("details"))[:160])
86
+ check("2c 套件计数经宽松解析累加(3+4=7)",
87
+ v.get("passed") == 7, f"passed={v.get('passed')}")
74
88
 
75
- # 角色守卫:非 verifier 拒跑
76
- env2 = dict(env)
77
- env2["HIVE_ROLE"] = "primary"
78
- r2 = subprocess.run([sys.executable, os.path.join(repo, "hive",
79
- "verify_runner.py"),
80
- iter_id], env=env2, capture_output=True,
81
- text=True, encoding="utf-8", errors="replace")
82
- check("2d 角色守卫:primary 拒跑互验(§7.1)",
83
- r2.returncode == 3 and "角色守卫" in r2.stdout,
84
- f"rc={r2.returncode} out={r2.stdout[:120]} err={r2.stderr[:120]}")
89
+ # 角色守卫:非 verifier 拒跑
90
+ env2 = dict(env)
91
+ env2["HIVE_ROLE"] = "primary"
92
+ r2 = subprocess.run([sys.executable, hive_runner, iter_id],
93
+ env=env2, capture_output=True,
94
+ text=True, encoding="utf-8", errors="replace")
95
+ check("2d 角色守卫:primary 拒跑互验(§7.1)",
96
+ r2.returncode == 3 and "角色守卫" in r2.stdout,
97
+ f"rc={r2.returncode} out={r2.stdout[:120]} err={r2.stderr[:120]}")
85
98
 
86
- # 脱敏纵深 + 入库(不 commit)
87
- bad = dict(v)
88
- bad["details"] = [{"leak": r"E:\private\node.log"}]
89
- try:
90
- sanity_check_verdict(bad)
91
- check("3a 落盘前脱敏纵深拦截", False, "违规未拦")
92
- except Exception:
93
- check("3a 落盘前脱敏纵深拦截", True)
94
- w = write_verdict_to_repo(v, repo=repo, do_commit=False)
95
- check("3b verdict 入受版本控制目录(hive/interop/<iter>/)",
96
- w.get("ok") is True
97
- and "hive" in w["path"] and "interop" in w["path"], w.get("path"))
98
- try:
99
- write_verdict_to_repo({**v, "iter_id": "../evil"}, repo=repo)
100
- check("3c iter_id 路径注入拒绝", False, "未抛")
101
- except ValueError:
102
- check("3c iter_id 路径注入拒绝", True)
99
+ # iter_id 白名单(v2 N5/N11,2026-09-25):CLI 直跑 + 自设
100
+ # HIVE_ROLE=verifier 骗过角色守卫(它只查 env 值)时,穿越形态
101
+ # 必须拒跑且不触盘——fail-closed 在任何路径拼接之前,与姊妹入口
102
+ # write_verdict_to_repo(interop.py:316)同模板统一防御。
103
+ probe = f"evil_probe_n5_{os.getpid()}"
104
+ evil_rel = os.path.join("..", "..", "..", probe)
105
+ # REPO/hive/interop/<evil_rel>/frozen.json 三级上跳 → REPO 父目录
106
+ tgt = os.path.join(os.path.dirname(repo), probe)
107
+ r3 = subprocess.run([sys.executable, hive_runner, evil_rel,
108
+ "--smoke"], env=env, capture_output=True,
109
+ text=True, encoding="utf-8", errors="replace")
110
+ check("2e iter_id 穿越形态拒跑且不触盘(N5/N11)",
111
+ r3.returncode == 2 and "iter_id 非法" in r3.stdout
112
+ and not os.path.exists(tgt),
113
+ f"rc={r3.returncode} out={r3.stdout[:120]} "
114
+ f"tgt_exists={os.path.exists(tgt)}")
115
+ evil_abs = os.path.join(tempfile.gettempdir(),
116
+ f"evil_abs_n5_{os.getpid()}")
117
+ r4 = subprocess.run([sys.executable, hive_runner, evil_abs,
118
+ "--smoke"], env=env, capture_output=True,
119
+ text=True, encoding="utf-8", errors="replace")
120
+ check("2f iter_id 绝对路径注入拒跑且不触盘",
121
+ r4.returncode == 2 and "iter_id 非法" in r4.stdout
122
+ and not os.path.exists(evil_abs),
123
+ f"rc={r4.returncode} out={r4.stdout[:120]} "
124
+ f"abs_exists={os.path.exists(evil_abs)}")
125
+
126
+ # 脱敏纵深 + 入库(不 commit)
127
+ bad = dict(v)
128
+ bad["details"] = [{"leak": r"E:\private\node.log"}]
129
+ try:
130
+ sanity_check_verdict(bad)
131
+ check("3a 落盘前脱敏纵深拦截", False, "违规未拦")
132
+ except Exception:
133
+ check("3a 落盘前脱敏纵深拦截", True)
134
+ w = write_verdict_to_repo(v, repo=repo, do_commit=False)
135
+ check("3b verdict 入受版本控制目录(hive/interop/<iter>/)",
136
+ w.get("ok") is True
137
+ and "hive" in w["path"] and "interop" in w["path"], w.get("path"))
138
+ try:
139
+ write_verdict_to_repo({**v, "iter_id": "../evil"}, repo=repo)
140
+ check("3c iter_id 路径注入拒绝", False, "未抛")
141
+ except ValueError:
142
+ check("3c iter_id 路径注入拒绝", True)
103
143
  finally:
104
144
  shutil.rmtree(tmp, ignore_errors=True)
105
145
  shutil.rmtree(os.path.join(repo, "hive", "interop", iter_id),
@@ -84,9 +84,12 @@ def _restore(old):
84
84
  os.environ[k] = v
85
85
 
86
86
 
87
- def _p(actor, session):
88
- return Principal(actor=actor, clearance=DEFAULT_SENSITIVITY,
89
- can_write=True, can_admin=True, role="designer",
87
+ def _p(actor, session, clearance=None, can_admin=True):
88
+ # 会话隔离用例(D/E 段)需非 admin 身份(admin 对绑定档豁免);
89
+ # 其余段维持原口径(designer + 默认密级 + admin)。
90
+ return Principal(actor=actor,
91
+ clearance=clearance or DEFAULT_SENSITIVITY,
92
+ can_write=True, can_admin=can_admin, role="designer",
90
93
  session=session, harness="test-harness")
91
94
 
92
95
 
@@ -101,10 +104,16 @@ def _ids(tl):
101
104
 
102
105
  def _mk(root):
103
106
  """两个会话各写一条(带 temporal → timeline 可见)。"""
104
- cga = MdCGSecure(root, principal=_p("alice", "sess_A"))
107
+ cga = MdCGSecure(root, principal=_p("alice", "sess_A",
108
+ clearance="private", can_admin=False))
105
109
  cga.add("n_a", "阿尔法 属于 会话A 的知识点", layer="knowledge", temporal=1000.0)
106
- cgb = MdCGSecure(root, principal=_p("bob", "sess_B"))
110
+ cga.add("n_pa", "贝塔 会话A 的私有知识点", layer="knowledge",
111
+ sensitivity="private")
112
+ cgb = MdCGSecure(root, principal=_p("bob", "sess_B",
113
+ clearance="private", can_admin=False))
107
114
  cgb.add("n_b", "阿尔法 属于 会话B 的知识点", layer="knowledge", temporal=2000.0)
115
+ cgb.add("n_pb", "贝塔 会话B 的私有知识点", layer="knowledge",
116
+ sensitivity="private")
108
117
  return cgb
109
118
 
110
119
 
@@ -119,7 +128,8 @@ def test_ab(cgb):
119
128
 
120
129
  print("\n[B] timeline 会话视图三态")
121
130
  all_tl = stg.timeline(cgb, limit=100)
122
- check("B1 缺省读遍所有会话", all_tl["count"] == 2, all_tl["count"])
131
+ check("B1 缺省读遍所有会话(可见面=共享档+本会话绑定档)",
132
+ all_tl["count"] == 3, all_tl["count"])
123
133
  check("B2 缺省返回体 session=None(跨会话视图)",
124
134
  all_tl["session"] is None, all_tl["session"])
125
135
  check("B3 items 逐条回带会话归属(看遍≠丢归属)",
@@ -135,11 +145,13 @@ def test_ab(cgb):
135
145
  and cross_tl["session"] is None, cross_tl["session"])
136
146
 
137
147
  a_tl = stg.timeline(cgb, limit=100, session="sess_A")
138
- check("B6 具体值只取本会话", _ids(a_tl) == ["n_a"], _ids(a_tl))
148
+ check("B6 具体值只取本会话(他人 private 经授权面剔除)",
149
+ _ids(a_tl) == ["n_a"], _ids(a_tl))
139
150
  check("B7 返回体 session=生效值(非跨会话)",
140
151
  a_tl["session"] == "sess_A", a_tl["session"])
141
152
  b_tl = stg.timeline(cgb, limit=100, session="sess_B")
142
- check("B8 另一会话只取它的节点", _ids(b_tl) == ["n_b"], _ids(b_tl))
153
+ check("B8 另一会话只取它的节点(含其绑定档)",
154
+ set(_ids(b_tl)) == {"n_b", "n_pb"}, _ids(b_tl))
143
155
 
144
156
  z_tl = stg.timeline(cgb, limit=100, session="sess_Z")
145
157
  check("B9 未知会话为空(不因缺省而放行)", z_tl["count"] == 0, z_tl["count"])
@@ -154,15 +166,27 @@ def test_c(cgb):
154
166
  a_tl = stg.timeline(cgb, limit=100, session="sess_A")
155
167
  check("C3 rebuild 后按会话过滤不静默全空", _ids(a_tl) == ["n_a"], _ids(a_tl))
156
168
  all_tl = stg.timeline(cgb, limit=100)
157
- check("C4 rebuild 后跨会话仍读全", all_tl["count"] == 2, all_tl["count"])
169
+ check("C4 rebuild 后跨会话仍读全(可见面 3)",
170
+ all_tl["count"] == 3, all_tl["count"])
158
171
 
159
172
 
160
173
  def test_d(cgb):
161
- print("\n[D] 候选层会话过滤(search / recall)")
174
+ print("\n[D] 候选层会话过滤(search / recall)——issue #35 定稿:分档判定")
175
+ # 视角=bob(sess_B)。共享档(internal 默认)跨会话可见:传不传 session
176
+ # 都能看到 alice 的共享节点;绑定档(private)按身份(principal.session)
177
+ # 判定,查询参数自报的会话不构成豁免。
162
178
  res, _ = cgb.search("阿尔法", session="sess_A", judge=False, record=False)
163
179
  ids = {_row(r).get("id") for r in res}
164
- check("D1 search 本会话仅命中本会话节点",
165
- "n_a" in ids and "n_b" not in ids, ids)
180
+ check("D1a 共享档(internal 默认)跨会话可见",
181
+ {"n_a", "n_b"} <= ids, ids)
182
+ res_p, _ = cgb.search("贝塔", judge=False, record=False)
183
+ pids = {_row(r).get("id") for r in res_p}
184
+ check("D1b 绑定档(private)仅归属会话(bob 只见自己的)",
185
+ "n_pb" in pids and "n_pa" not in pids, pids)
186
+ res_h, _ = cgb.search("贝塔", session="sess_A", judge=False, record=False)
187
+ hids = {_row(r).get("id") for r in res_h}
188
+ check("D1c 查询参数自报他人会话不越权(仍只见自己的 private)",
189
+ "n_pb" in hids and "n_pa" not in hids, hids)
166
190
 
167
191
  res_x, _ = cgb.search("阿尔法", session="*", judge=False, record=False)
168
192
  ids_x = {_row(r).get("id") for r in res_x}
@@ -174,8 +198,12 @@ def test_d(cgb):
174
198
 
175
199
  pack = cgb.recall("阿尔法", session="sess_A")
176
200
  hid = {_row(h).get("id") for h in (pack.get("pack") or [])}
177
- check("D4 recall(RRF 主链)本会话仅命中本会话",
178
- "n_a" in hid and "n_b" not in hid, hid)
201
+ check("D4a recall(RRF 主链)共享档跨会话",
202
+ {"n_a", "n_b"} <= hid, hid)
203
+ pack_p = cgb.recall("贝塔")
204
+ hid_p = {_row(h).get("id") for h in (pack_p.get("pack") or [])}
205
+ check("D4b recall 绑定档仅归属会话",
206
+ "n_pb" in hid_p and "n_pa" not in hid_p, hid_p)
179
207
 
180
208
  pack_x = cgb.recall("阿尔法", session="*")
181
209
  hid_x = {_row(h).get("id") for h in (pack_x.get("pack") or [])}
@@ -183,24 +211,29 @@ def test_d(cgb):
183
211
 
184
212
 
185
213
  def test_e(cgb):
186
- print("\n[E] 图扩展二次过滤(跨会话串台守卫)")
187
- # 图扩散会绕过 _candidates:这里直接把父类 RRF 结果替换成「两个会话各一条」,
188
- # 验证 MdCGSecure.search_rrf 的二次过滤能按会话把别的会话节点剔掉。
214
+ print("\n[E] 图扩展二次过滤(跨会话串台守卫)——分档口径")
215
+ # 图扩散会绕过 _candidates:直接把父类 RRF 结果替换成「两共享 + 两绑定」,
216
+ # 验证 MdCGSecure.search_rrf 的二次过滤按档判定——视角 bob:共享档
217
+ # (n_a/n_b)互可见,绑定档仅自己的 n_pb 能回来;session 参数三态
218
+ # (他人会话 / "*" / 缺省)不影响绑定档结果(身份不可自报)。
189
219
  crafted = [({"id": "n_a"}, 1.0, None, None),
190
- ({"id": "n_b"}, 0.9, None, None)]
220
+ ({"id": "n_b"}, 0.9, None, None),
221
+ ({"id": "n_pa"}, 0.8, None, None),
222
+ ({"id": "n_pb"}, 0.7, None, None)]
191
223
  orig = MdCGOS.search_rrf
192
224
  MdCGOS.search_rrf = lambda self, *a, **kw: (list(crafted), {"tier": "RRF"})
193
225
  try:
194
226
  kept, _ = cgb.search_rrf("q", session="sess_A")
195
227
  ids = [r[0]["id"] for r in kept]
196
- check("E1 别的会话节点不得顺边回来", ids == ["n_a"], ids)
228
+ check("E1 绑定档按身份:他人 private 不得顺边回来(共享档保留)",
229
+ {"n_a", "n_b", "n_pb"} <= set(ids) and "n_pa" not in ids, ids)
197
230
  kept_x, _ = cgb.search_rrf("q", session="*")
198
- check("E2 \"*\" 跨会话两条都在",
199
- {r[0]["id"] for r in kept_x} == {"n_a", "n_b"},
231
+ check("E2 \"*\" 同口径(绑定档不受查询参数影响)",
232
+ {r[0]["id"] for r in kept_x} == {"n_a", "n_b", "n_pb"},
200
233
  [r[0]["id"] for r in kept_x])
201
234
  kept_all, _ = cgb.search_rrf("q")
202
- check("E3 缺省不过滤(兼容)",
203
- {r[0]["id"] for r in kept_all} == {"n_a", "n_b"},
235
+ check("E3 缺省同口径(兼容)",
236
+ {r[0]["id"] for r in kept_all} == {"n_a", "n_b", "n_pb"},
204
237
  [r[0]["id"] for r in kept_all])
205
238
  finally:
206
239
  MdCGOS.search_rrf = orig
@@ -251,10 +284,12 @@ def test_g(cgb):
251
284
 
252
285
  cross = ms.call_tool(cgb, "stg",
253
286
  {"op": "timeline", "limit": 100, "session": "*"})
254
- check("G4 MCP 面 \"*\" 跨会话读全", cross.get("count") == 2, cross.get("count"))
287
+ check("G4 MCP 面 \"*\" 跨会话读全(可见面 3)",
288
+ cross.get("count") == 3, cross.get("count"))
255
289
 
256
290
  default = ms.call_tool(cgb, "stg", {"op": "timeline", "limit": 100})
257
- check("G5 缺省读全(兼容旧调用)", default.get("count") == 2, default.get("count"))
291
+ check("G5 缺省读全(兼容旧调用,可见面 3)",
292
+ default.get("count") == 3, default.get("count"))
258
293
 
259
294
 
260
295
  def main():
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """propose 尾部增量对账守卫(issue:propose 每条全量重读 inbox+decisions)。
4
+
5
+ 缺陷形态(缺陷报告实测):propose 每条在 strict 排它锁内
6
+ read_jsonl(inbox_log) + read_jsonl(decisions_log) 全量逐条 json.loads
7
+ (O(M+D)/条,批量累计 O(M²)),存量无 payload_hash 还逐条现算 sha1。
8
+ 万条存量单条 ~26ms、批量 O(M²) 分钟级。
9
+
10
+ 本守卫断言(调用计数/复杂度形态,非时间阈值):
11
+ T1 二次 propose 零全量读:同实例第 2 次 propose(无外部写)期间
12
+ mdcos.read_jsonl 对 inbox/decisions 的调用数为 0(旧代码每条各 1 次
13
+ 全量 → 红);
14
+ T2 批量 O(M):500 条连续 propose 全程,首条装载后 read_jsonl 调用数为 0
15
+ (旧代码 2×499 次 → 红);
16
+ T3 增量不漏账(跨进程 append 可见):外部 append 同内容提案(他进程形态)
17
+ → 本实例 propose 同内容 dedup 命中新 pid;
18
+ T4 已裁决优先语义保持:外部裁决(accepted)后 propose 同内容 →
19
+ 返回已裁决 pid + status;
20
+ T5 旧格式兼容:无 payload_hash 存量行 → 同内容 propose 命中(装载期
21
+ 现算一次,不每条重算);
22
+ T6 幂等保形:同内容两次 propose 返回同 pid、不重复入队;
23
+ T7 决策增量可见:外部 append decisions 后 _closed_pids 即含该 pid。
24
+ """
25
+ import os
26
+ import shutil
27
+ import sys
28
+ import tempfile
29
+
30
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
31
+
32
+ import md_cg.mdcos as mdcos_mod # noqa: E402
33
+ from md_cg.fsutil import append_jsonl # noqa: E402
34
+ from md_cg.mdcos import MdCGOS, _sig # noqa: E402
35
+
36
+ PASS = 0
37
+ FAIL = 0
38
+ FAILS = []
39
+
40
+
41
+ def check(name, cond, detail=""):
42
+ global PASS, FAIL
43
+ if cond:
44
+ PASS += 1
45
+ print(f" OK {name}")
46
+ else:
47
+ FAIL += 1
48
+ FAILS.append(name)
49
+ print(f" FAIL {name} {detail}")
50
+
51
+
52
+ def count_full_reads(fn):
53
+ """打桩 mdcos.read_jsonl 计数(propose/_pid_status 的全量读入口)。"""
54
+ n = 0
55
+ real = mdcos_mod.read_jsonl
56
+
57
+ def _spy(path, *a, **kw):
58
+ nonlocal n
59
+ n += 1
60
+ return real(path, *a, **kw)
61
+
62
+ mdcos_mod.read_jsonl = _spy
63
+ try:
64
+ return fn(), n
65
+ finally:
66
+ mdcos_mod.read_jsonl = real
67
+
68
+
69
+ def main():
70
+ root = tempfile.mkdtemp(prefix="mdcg_prop_guard_")
71
+ try:
72
+ cg = MdCGOS(root)
73
+ # T1 二次 propose 零全量读(预热完成首装——inbox 文件由首条 propose
74
+ # 创建,创建后第一次访问做一次全量装载属预期,装载后稳态零全量读)
75
+ print("== T1 稳态 propose 零全量读 ==")
76
+ cg.propose("t1w1", "T1 预热 甲") # 创建 inbox + 装载(不计数)
77
+ cg.propose("t1w2", "T1 预热 乙") # 装载水位已建立(不计数)
78
+ _, n1 = count_full_reads(lambda: cg.propose("t1", "T1 内容 甲"))
79
+ print(f" (稳态首次计数窗 full_reads={n1})")
80
+ _, n2 = count_full_reads(lambda: cg.propose("t1b", "T1 内容 乙"))
81
+ check("T1a 稳态 propose 全量 read_jsonl 调用 == 0(旧默认 2 次/条)",
82
+ n1 == 0 and n2 == 0, f"full_reads={n1},{n2}")
83
+
84
+ # T2 批量 O(M):500 条连续 propose,装载完成后全量读为 0
85
+ print("== T2 批量 500 条 O(M) ==")
86
+ cg2_root = tempfile.mkdtemp(prefix="mdcg_prop_guard2_")
87
+ cg2 = MdCGOS(cg2_root)
88
+ try:
89
+ cg2.propose("warm1", "预热 占位一") # 创建 inbox + 装载(不计)
90
+ cg2.propose("warm2", "预热 占位二") # 水位建立(不计)
91
+ _, nb = count_full_reads(lambda: [
92
+ cg2.propose("b%03d" % i, "批量守卫 %d 各异 %d" % (i, i * 3))
93
+ for i in range(500)])
94
+ check("T2a 500 条批量 propose 全量 read_jsonl 调用 == 0"
95
+ "(旧代码 2×499=998 次,O(M²) 解析)", nb == 0,
96
+ f"full_reads={nb}")
97
+ finally:
98
+ shutil.rmtree(cg2_root, ignore_errors=True)
99
+
100
+ # T3 增量不漏账:外部(他进程形态)append 同内容提案
101
+ print("== T3 跨进程 append 可见(增量不漏账)==")
102
+ content = "T3 同内容 蜂群调度"
103
+ pid_ext = "prop_ext_abc123"
104
+ append_jsonl(cg.inbox_log,
105
+ {"t": 1.0, "pid": pid_ext, "id": "ext_n",
106
+ "content": content, "layer": "knowledge",
107
+ "tags": [], "condition_space": {}, "verify": {},
108
+ "payload_hash": _sig(content), "actor": "ext"})
109
+ r3 = cg.propose("t3", content, info=True)
110
+ check("T3a 外部 append 后同内容 dedup 命中新 pid",
111
+ r3["dedup"] and r3["pid"] == pid_ext, f"r={r3}")
112
+
113
+ # T4 已裁决优先:外部裁决 accepted → propose 返回已裁决 pid
114
+ print("== T4 已裁决优先 ==")
115
+ append_jsonl(cg.decisions_log,
116
+ {"t": 2.0, "pid": pid_ext, "decision": "accept",
117
+ "status": "accepted", "round": 1, "actor": "ext"})
118
+ r4 = cg.propose("t4", content, info=True)
119
+ check("T4a 外部裁决后同内容返回已裁决 pid(accepted 优先)",
120
+ r4["dedup"] and r4["pid"] == pid_ext
121
+ and r4["dup_status"] == "accepted", f"r={r4}")
122
+ check("T4b 外部裁决进入 _closed_pids(增量可见)",
123
+ pid_ext in cg._closed_pids())
124
+
125
+ # T5 旧格式兼容:无 payload_hash 存量行
126
+ print("== T5 旧格式(无 payload_hash)==")
127
+ legacy = "T5 旧格式内容 星云编译"
128
+ append_jsonl(cg.inbox_log,
129
+ {"t": 3.0, "pid": "prop_legacy_001", "id": "lg_n",
130
+ "content": legacy, "layer": "knowledge",
131
+ "tags": [], "verify": {}, "actor": "old"}) # 无 hash
132
+ r5 = cg.propose("t5", legacy, info=True)
133
+ check("T5a 旧格式行同内容命中(装载期现算,兼容旧账)",
134
+ r5["dedup"] and r5["pid"] == "prop_legacy_001", f"r={r5}")
135
+ _, n5 = count_full_reads(lambda: cg.propose("t5b", legacy, info=True))
136
+ check("T5b 旧格式行不触发每条重算全量读(==0)", n5 == 0,
137
+ f"full_reads={n5}")
138
+
139
+ # T6 幂等保形:同内容两次 propose 同 pid
140
+ print("== T6 幂等保形 ==")
141
+ p_a = cg.propose("t6", "T6 幂等内容")
142
+ p_b = cg.propose("t6r", "T6 幂等内容")
143
+ check("T6a 同内容两次 propose 返回同 pid(不重复入队)",
144
+ p_a == p_b and p_a, f"{p_a} vs {p_b}")
145
+ finally:
146
+ shutil.rmtree(root, ignore_errors=True)
147
+
148
+ print("\n" + "=" * 60)
149
+ print(f"结果:PASS {PASS} / FAIL {FAIL}")
150
+ if FAILS:
151
+ for f in FAILS:
152
+ print(f" - {f}")
153
+ return 0 if FAIL == 0 else 1
154
+
155
+
156
+ if __name__ == "__main__":
157
+ sys.exit(main())