@furongjun1999/dsh-memory 0.7.2 → 0.7.4

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 (129) hide show
  1. package/README.md +57 -47
  2. package/codebuddy/CODEBUDDY.md +6 -6
  3. package/docs/README.md +148 -144
  4. package/docs/discipline/harnesses.yaml +16 -8
  5. package/docs/discipline/templates/zcode-user.md.tmpl +49 -0
  6. package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_/345/205/255/345/256/266/350/256/260/345/277/206/347/263/273/347/273/237/345/220/214/345/260/272/345/256/236/346/265/213_v3.1.md +340 -0
  7. package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +2 -2
  8. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +1 -1
  9. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +1 -1
  10. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +1 -1
  11. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +2 -2
  12. package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +5 -5
  13. package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +9 -9
  14. package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +4 -4
  15. package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +1 -1
  16. package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +1 -1
  17. package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +3 -3
  18. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +3 -3
  19. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +7 -7
  20. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +2 -2
  21. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +1 -1
  22. package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +7 -7
  23. package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +10 -10
  24. package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +4 -0
  25. package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +1 -1
  26. package/docs/eval//345/217/221/345/270/20311_/345/244/226/346/212/245/345/205/255/350/277/236/344/277/256/344/270/216/344/270/226/347/225/214/346/250/241/345/236/213/345/272/225/345/272/247_v1.0.md +91 -0
  27. package/docs/eval//345/217/221/345/270/20312_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_v1.0.md +108 -0
  28. package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +2 -2
  29. package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/270/200/346/211/271_v1.0.md +195 -0
  30. package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/272/214/346/211/271B6_/346/234/254/346/234/272/350/267/257/345/276/204/347/233/270/345/257/271/345/214/226_v1.0.md +78 -0
  31. package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +1 -1
  32. package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +3 -3
  33. package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +1 -1
  34. package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +1 -1
  35. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v26.md +196 -0
  36. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v27.md +259 -0
  37. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v28.md +217 -0
  38. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v29.md +270 -0
  39. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v30.md +231 -0
  40. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v31.md +281 -0
  41. package/docs/eval//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_P1_/346/212/275/345/217/226/345/231/250v4_/344/270/211/347/216/207/350/257/273/346/225/260_v1.0.md +231 -0
  42. package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +1 -1
  43. package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
  44. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +1 -1
  45. package/docs/hive//350/234/202/345/267/242/350/256/276/350/256/241_/347/220/206/350/256/272/345/257/271/351/275/220_v0.1.md +17 -17
  46. package/docs/hive//350/234/202/345/267/242/350/277/255/344/273/243_/345/256/217/350/247/202/344/270/216/347/276/244/344/275/223/350/260/203/345/272/246_v0.1.md +1 -1
  47. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +2 -2
  48. package/docs/mdcg//344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/344/275/277/347/224/250/344/270/216/350/277/220/347/273/264_v1.0.md +305 -0
  49. 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 +45 -42
  50. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +29 -8
  51. package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +71 -1
  52. package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +1 -1
  53. package/docs/plans//345/244/232/344/270/273/344/275/223/344/270/226/347/225/214/346/250/241/345/236/213_/345/257/271/351/275/220/350/257/204/344/274/260/344/270/216/350/220/275/345/234/260/350/256/276/350/256/241_v0.1.md +141 -0
  54. package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +2 -2
  55. package/docs/plans//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/350/256/276/350/256/241_v0.1.md +107 -0
  56. 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 +6 -5
  57. package/docs//347/247/244_/350/256/260/345/277/206/347/263/273/347/273/237/350/257/204/346/265/213/350/247/204/350/214/203_v2.1.md +225 -0
  58. package/dsh/cordis-patch-profile-web.example.yml +2 -2
  59. package/lib/bridge.js +9 -0
  60. package/lib/hooks.d.ts +8 -1
  61. package/lib/hooks.js +134 -50
  62. package/lib/lib/hook_audit.d.ts +55 -0
  63. package/lib/lib/hook_audit.js +208 -0
  64. package/lib/lib/mutual.js +5 -1
  65. package/lib/lib/roleplay_web.js +17 -2
  66. package/lib/lib/session_state.d.ts +56 -0
  67. package/lib/lib/session_state.js +124 -0
  68. package/lib/tools.js +12 -1
  69. package/md_cg/auditview.py +248 -0
  70. package/md_cg/bench_e2e_judge.py +4 -2
  71. package/md_cg/branches.py +29 -3
  72. package/md_cg/consistency.py +38 -13
  73. package/md_cg/datapath.py +38 -11
  74. package/md_cg/forgetting.py +15 -2
  75. package/md_cg/freshness.py +32 -11
  76. package/md_cg/ghostref.py +130 -0
  77. package/md_cg/judgment_manifest.py +7 -0
  78. package/md_cg/lifecycle.py +38 -0
  79. package/md_cg/mcp_server.py +122 -12
  80. package/md_cg/mdcg.py +4923 -4822
  81. package/md_cg/mdcos.py +109 -21
  82. package/md_cg/nodefile.py +77 -13
  83. package/md_cg/readcache.py +29 -4
  84. package/md_cg/selfreport.py +12 -3
  85. package/md_cg/sleep.py +129 -13
  86. package/md_cg/sources.py +1 -1
  87. package/md_cg/state_events.py +131 -0
  88. package/md_cg/state_slots.py +221 -0
  89. package/md_cg/stg.py +68 -3
  90. package/md_cg/sustain.py +1436 -1436
  91. package/md_cg/test_auditview.py +277 -0
  92. package/md_cg/test_autonomy_modes.py +2 -1
  93. package/md_cg/test_boot_openblas_guard.py +197 -0
  94. package/md_cg/test_ccg_form_parity.py +95 -4
  95. package/md_cg/test_cons200_scan_selection.py +4 -2
  96. package/md_cg/test_datapath_device_name.py +62 -3
  97. package/md_cg/test_generation_guard.py +5 -2
  98. package/md_cg/test_ghostref.py +182 -0
  99. package/md_cg/test_interop.py +1 -1
  100. package/md_cg/test_issue52_scan_condition_first.py +1 -1
  101. package/md_cg/test_lifecycle_retire_leak.py +408 -0
  102. package/md_cg/test_lock.py +16 -7
  103. package/md_cg/test_m3_h9_semantic_guard.py +1 -1
  104. package/md_cg/test_n230_dirty_replay.py +375 -375
  105. package/md_cg/test_n238_body_text_parity.py +156 -0
  106. package/md_cg/test_n276_probe_hoist.py +199 -0
  107. package/md_cg/test_p4_freshness.py +95 -1
  108. package/md_cg/test_policy_required_ccg.py +3 -1
  109. package/md_cg/test_read_face_semantics.py +6 -3
  110. package/md_cg/test_readcache_prodpath.py +66 -0
  111. package/md_cg/test_sleep.py +58 -1
  112. package/md_cg/test_sleep_p1.py +218 -2
  113. package/md_cg/test_state_event_op.py +514 -0
  114. package/md_cg/test_state_events.py +178 -0
  115. package/md_cg/test_state_slots.py +691 -0
  116. package/md_cg/test_time_core_lint.py +8 -1
  117. package/md_cg/test_writepipe.py +8 -5
  118. package/md_cg/tokens.py +18 -2
  119. package/md_cg/writepipe.py +76 -0
  120. package/package.json +2 -2
  121. package/skills/plugin.json +1 -1
  122. package/src/bridge.ts +9 -0
  123. package/src/hooks.ts +599 -517
  124. package/src/lib/hook_audit.ts +243 -0
  125. package/src/lib/mutual.ts +5 -1
  126. package/src/lib/roleplay_web.ts +17 -2
  127. package/src/lib/session_state.ts +127 -0
  128. package/src/tools.ts +12 -1
  129. package/zcode/AGENTS.md +6 -6
@@ -0,0 +1,56 @@
1
+ /**
2
+ * session_state.ts —— 插件侧「运行期会话状态」单点(观测 → 写归因注入)
3
+ *
4
+ * 为什么有这个模块(B 治本批,2026-10-06)
5
+ * ----------------------------------------
6
+ * DSH 单进程多会话:宿主的会话标识只经 `session/event` 到达插件。此前它只被
7
+ * 用在 hooks 面(自动记忆的 remember / read 带 extra.session);**agent 直调
8
+ * 工具面**(src/tools.ts 的 execute → bridge.callTool)转发时**不带会话**——
9
+ * 而部署侧 `MDCG_SESSION` env 一旦取消/为空,`md_cg/mcp_server.py` 的
10
+ * `_declared_session`(:3823-3850)就以**请求声明**为归因来源
11
+ * (优先级:env > 请求声明 > 进程身份),不传即落回进程身份——md_cg 的
12
+ * `Principal.__init__` 会为假值 session 生成**进程级随机** `sess_<hex>`
13
+ * (md_cg/security.py:117):归属在审计上既读不出是谁、跨进程也对不上,
14
+ * 是静默的归属丢失。
15
+ *
16
+ * 本模块把「观测会话」与「写归因注入」收成**单点**:
17
+ * · hooks 面:每收到 session/event 就 `noteSession(sid)`——调用点仍在 H1
18
+ * 子代理闸**之后**(子代理会话不得污染会话状态的位置不变量保持);
19
+ * · 工具面:写归因调用注入 `currentSession() || UNASSIGNED_SESSION`
20
+ * (判据矩阵见 `attributeSession`,只注入写面;读面一律不注入)。
21
+ *
22
+ * 模块级状态(`lastSession`)是**进程级**的:同一进程内多个会话共享它,
23
+ * 「最近一次观测」即当前活动会话——与 hooks 面的既有口径一致(见
24
+ * test/session-attribution.test.ts ④「取值每步稳定」)。
25
+ */
26
+ /** 会话归属未知时的**显式占位**(H2③,2026-09-30;原定义在 src/hooks.ts,
27
+ * B 治本批迁入本单点——注释要点保真搬迁)。
28
+ *
29
+ * ⚠️ 不可退回「不传 session 键」:md_cg 的 `Principal.__init__` 在 session 为假值时
30
+ * 生成**进程级随机** `sess_<hex>`(md_cg/security.py:117)——插件不传,等于让一个
31
+ * 进程内所有「宿主未给标识」的会话共用一个**不可辨认**的随机桶:归属在审计上既
32
+ * 读不出是谁、跨进程也对不上,是静默的归属丢失。
33
+ * 本常量把这一态写成**显式值**:跨进程一致、可辨认、可审计,且不是伪造的宿主
34
+ * 会话 id(非 DSH 形态,服务端 `_normalize_session` 原样采用、不会被改写成别的桶)。
35
+ * 要读这个桶:`stg(op=timeline, session="unassigned")`。 */
36
+ export declare const UNASSIGNED_SESSION = "unassigned";
37
+ /** 记录一次会话观测(H2③ 观测面单点)。
38
+ *
39
+ * trim 后非空才写入——空串/纯空白**不覆盖**旧值(否则「宿主给了一次空标识」
40
+ * 会把已观测到的真会话抹掉,后续注入退化为 unassigned)。 */
41
+ export declare function noteSession(sid: string): void;
42
+ /** 当前运行期会话标识(空串 = 未观测到;调用方按 `|| UNASSIGNED_SESSION` 兜底)。 */
43
+ export declare function currentSession(): string;
44
+ /** 把**运行期会话**注入**写归因调用**(args 的 session 键不可用时才注入)。
45
+ *
46
+ * 注入条件:`session` 键缺失 / 非 string 类型 / trim 后空串 ⇒ 注入
47
+ * (`null`/`''`/空白一律视为「未声明」——服务端 `_declared_session` 对假值
48
+ * 同样解析为无归属声明,原样转发只会落回进程级随机 sess_* 兜底桶);
49
+ * 显式非空声明一律**不覆盖**(调用方自报优先——插件不替调用方改归属)。
50
+ *
51
+ * 注入值:`currentSession() || UNASSIGNED_SESSION`(未观测到会话时用
52
+ * 'unassigned' 显式占位——与 hooks 面 H2③ 同口径)。
53
+ *
54
+ * 命中且需注入 → 返回**新对象** `{ ...args, session: v }`(绝不 mutate 输入);
55
+ * 否则**原样返回**(同一引用)——未命中的调用零开销、零可观察差异。 */
56
+ export declare function attributeSession(toolName: string, args: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * session_state.ts —— 插件侧「运行期会话状态」单点(观测 → 写归因注入)
3
+ *
4
+ * 为什么有这个模块(B 治本批,2026-10-06)
5
+ * ----------------------------------------
6
+ * DSH 单进程多会话:宿主的会话标识只经 `session/event` 到达插件。此前它只被
7
+ * 用在 hooks 面(自动记忆的 remember / read 带 extra.session);**agent 直调
8
+ * 工具面**(src/tools.ts 的 execute → bridge.callTool)转发时**不带会话**——
9
+ * 而部署侧 `MDCG_SESSION` env 一旦取消/为空,`md_cg/mcp_server.py` 的
10
+ * `_declared_session`(:3823-3850)就以**请求声明**为归因来源
11
+ * (优先级:env > 请求声明 > 进程身份),不传即落回进程身份——md_cg 的
12
+ * `Principal.__init__` 会为假值 session 生成**进程级随机** `sess_<hex>`
13
+ * (md_cg/security.py:117):归属在审计上既读不出是谁、跨进程也对不上,
14
+ * 是静默的归属丢失。
15
+ *
16
+ * 本模块把「观测会话」与「写归因注入」收成**单点**:
17
+ * · hooks 面:每收到 session/event 就 `noteSession(sid)`——调用点仍在 H1
18
+ * 子代理闸**之后**(子代理会话不得污染会话状态的位置不变量保持);
19
+ * · 工具面:写归因调用注入 `currentSession() || UNASSIGNED_SESSION`
20
+ * (判据矩阵见 `attributeSession`,只注入写面;读面一律不注入)。
21
+ *
22
+ * 模块级状态(`lastSession`)是**进程级**的:同一进程内多个会话共享它,
23
+ * 「最近一次观测」即当前活动会话——与 hooks 面的既有口径一致(见
24
+ * test/session-attribution.test.ts ④「取值每步稳定」)。
25
+ */
26
+ /** 会话归属未知时的**显式占位**(H2③,2026-09-30;原定义在 src/hooks.ts,
27
+ * B 治本批迁入本单点——注释要点保真搬迁)。
28
+ *
29
+ * ⚠️ 不可退回「不传 session 键」:md_cg 的 `Principal.__init__` 在 session 为假值时
30
+ * 生成**进程级随机** `sess_<hex>`(md_cg/security.py:117)——插件不传,等于让一个
31
+ * 进程内所有「宿主未给标识」的会话共用一个**不可辨认**的随机桶:归属在审计上既
32
+ * 读不出是谁、跨进程也对不上,是静默的归属丢失。
33
+ * 本常量把这一态写成**显式值**:跨进程一致、可辨认、可审计,且不是伪造的宿主
34
+ * 会话 id(非 DSH 形态,服务端 `_normalize_session` 原样采用、不会被改写成别的桶)。
35
+ * 要读这个桶:`stg(op=timeline, session="unassigned")`。 */
36
+ export const UNASSIGNED_SESSION = 'unassigned';
37
+ /** 最近一次观测到的宿主会话标识(空串 = 未观测到会话)。
38
+ *
39
+ * ⚠️ 本状态是**模块级(进程级)单点**:hooks 面观测写入(src/hooks.ts 的
40
+ * session/event),工具面读它做写归因注入(src/tools.ts 的转发面)——两处必须
41
+ * 同源,否则注入的会话与写入的归属对不上(B 治本批的目的即在此)。
42
+ *
43
+ * 「本实例是否观测过」的**实例级门不在本模块**,在 src/hooks.ts 的
44
+ * installMemoryHooks 内(局部 `observedSession`):即「本实例观测到会话之后」
45
+ * 才用本单点值回落召回过滤。这是**经 owner 裁定的契约字面偏离(dwfq-7b3a555e-1)**
46
+ * ——契约建议的形态是 hooks 侧字面 `|| currentSession()`,但字面形态与硬边界
47
+ * 「test/session-attribution.test.ts 逐字未动且全绿」互斥(该守卫 ② 以
48
+ * 「新建 harness = 未观测」为前提,字面回落会读成前一实例的 sess_B)。保留
49
+ * 实例门的两条理由:
50
+ * ① 「新实例 = 干净状态」是原闭包变量 `lastSession` 的**有意属性**(新装的钩子
51
+ * 在观测到会话前,自动召回不加过滤)——实例隔离语义在测试面被真实保留;
52
+ * ② 与既有冻结守卫 ② 相容(守卫逐字未动且全绿是本批硬边界)。
53
+ * 真机单实例下两者**逐位等价**:唯一差异窗口是「本实例未观测 ∧ 模块单点非空」,
54
+ * 而模块值只由本实例的 hooks 观测写入,单实例下该窗口不可达;HMR 重载场景下新
55
+ * 实例回落 '' 而非上一会话值,属更保守的防御行为(已由 owner 裁定接受)。 */
56
+ let lastSession = '';
57
+ /** 记录一次会话观测(H2③ 观测面单点)。
58
+ *
59
+ * trim 后非空才写入——空串/纯空白**不覆盖**旧值(否则「宿主给了一次空标识」
60
+ * 会把已观测到的真会话抹掉,后续注入退化为 unassigned)。 */
61
+ export function noteSession(sid) {
62
+ const s = sid.trim();
63
+ if (s)
64
+ lastSession = s;
65
+ }
66
+ /** 当前运行期会话标识(空串 = 未观测到;调用方按 `|| UNASSIGNED_SESSION` 兜底)。 */
67
+ export function currentSession() {
68
+ return lastSession;
69
+ }
70
+ /** 写归因面判据:本次调用是否属于「要注入运行期会话」的写归因调用。
71
+ *
72
+ * **恰两条命中路径**(不得放宽、不得收窄出契约外):
73
+ * ① `mdcg_remember` —— md_cg 细粒度写入口(插件侧自动记忆/落图的写通道);
74
+ * ② `cg` 且 `op === 'write'` —— 认知图基元的带审核写路径。
75
+ *
76
+ * 为什么**只**这两个面(判据出处:md_cg/mcp_server.py):
77
+ * · **读面一律不注入**——`mdcg_recall` / `mdcg_search` / `mdcg_get` / `stg`
78
+ * 以及 `cg` 的其它 op,其 `session` 是**视图过滤**:服务端 schema 描述原文
79
+ * 「会话归属过滤(frontmatter.session;…缺省不过滤)」
80
+ * (md_cg/mcp_server.py:218-219 / :251-252 / :908-912 / :1074-1077)。
81
+ * 注入会把文档化的「缺省跨会话」翻转成「本会话视图」——那是功能收窄,
82
+ * 不是本批目标(读面要跨会话视图请显式 `stg(op=timeline, session="*")`)。
83
+ * · **op 特化语义不动**——`cg` 的 `sustain`/`session` 等 op 的 `session` 是
84
+ * 特化语义(resume/note 的**目标会话**),注入即污染其目标参数。
85
+ * · 归因与授权正交(issue #35 定稿):`call_tool` 的请求级 session **只做
86
+ * 归因**(写入归属/`_attribution` 取它),**不**改 `principal.session`
87
+ * (md_cg/mcp_server.py:3261-3264)——绑定档(private/secret)的读授权
88
+ * 锚定连接级身份,调用方自报的会话不构成看他人 private 的授权。
89
+ * 绑定档可见性判定 `MdCGSecure._readable`(md_cg/mdcos.py:5009-5053)
90
+ * 的会话绑定分支恒用 `nsess == self.principal.session`(:5051-5052),
91
+ * can_admin(设计者)豁免(:5045)——故本注入对绑定档无回归。
92
+ *
93
+ * ⚠️ 禁止把本判据放宽成「所有带 session 参数的工具」:那会把上面两类语义
94
+ * (读面视图过滤 / op 特化目标)一并改写,属越权改契约。
95
+ */
96
+ function isWriteAttributionCall(toolName, args) {
97
+ if (toolName === 'mdcg_remember')
98
+ return true;
99
+ return toolName === 'cg' && args['op'] === 'write';
100
+ }
101
+ /** 请求是否已显式声明会话(string 且 trim 后非空 → 保留调用方声明,绝不覆盖)。 */
102
+ function hasDeclaredSession(args) {
103
+ const v = args['session'];
104
+ return typeof v === 'string' && v.trim() !== '';
105
+ }
106
+ /** 把**运行期会话**注入**写归因调用**(args 的 session 键不可用时才注入)。
107
+ *
108
+ * 注入条件:`session` 键缺失 / 非 string 类型 / trim 后空串 ⇒ 注入
109
+ * (`null`/`''`/空白一律视为「未声明」——服务端 `_declared_session` 对假值
110
+ * 同样解析为无归属声明,原样转发只会落回进程级随机 sess_* 兜底桶);
111
+ * 显式非空声明一律**不覆盖**(调用方自报优先——插件不替调用方改归属)。
112
+ *
113
+ * 注入值:`currentSession() || UNASSIGNED_SESSION`(未观测到会话时用
114
+ * 'unassigned' 显式占位——与 hooks 面 H2③ 同口径)。
115
+ *
116
+ * 命中且需注入 → 返回**新对象** `{ ...args, session: v }`(绝不 mutate 输入);
117
+ * 否则**原样返回**(同一引用)——未命中的调用零开销、零可观察差异。 */
118
+ export function attributeSession(toolName, args) {
119
+ if (!isWriteAttributionCall(toolName, args))
120
+ return args;
121
+ if (hasDeclaredSession(args))
122
+ return args;
123
+ return { ...args, session: currentSession() || UNASSIGNED_SESSION };
124
+ }
package/lib/tools.js CHANGED
@@ -5,6 +5,10 @@
5
5
  * 动态拉取意味着灵枢库升级新增工具后,DSH 侧零改动即可获得新能力。
6
6
  */
7
7
  import { defineTool } from '@deepseek-ai/dsh-tools';
8
+ // B 治本批(2026-10-06):写归因注入单点(运行期会话 → 写归因调用)。
9
+ // 运行时导入写 `.js`(本仓口径:type-only 才写 `.ts`,tsconfig 未开
10
+ // allowImportingTsExtensions —— 值导入写 `.ts` 会 TS5097 编译失败)。
11
+ import { attributeSession } from './lib/session_state.js';
8
12
  /** 默认暴露的核心工具集合:**记忆面已基元化**,只注册 `cg` / `stg` 两个认知基元。
9
13
  *
10
14
  * 三层拆分 S4 收敛后,插件工具面真源是 `md_cg.mcp_server`(不再是 pip aeis);
@@ -173,7 +177,14 @@ export async function registerLingshuTools(ctx, bridge, opts) {
173
177
  async execute(args, exec) {
174
178
  if (exec.signal.aborted)
175
179
  throw new Error(`灵枢 ${tool.name} 已取消`);
176
- const result = await bridge.callTool(tool.name, args, exec.signal);
180
+ // B 治本批(2026-10-06):agent 直调工具的转发面把**运行期会话**注入
181
+ // **写归因调用**——env(MDCG_SESSION)取消后,请求声明即归因唯一来源
182
+ // (md_cg/mcp_server.py 的 _declared_session:env > 请求声明 > 进程身份)。
183
+ // 判据单点在 lib/session_state.ts 的 attributeSession:只注入
184
+ // mdcg_remember 与 cg(op=write) 两个写面;读面(视图过滤)/ op 特化语义
185
+ // 一律不动。命中且未显式声明时返回新对象,否则原样透传。
186
+ const forwarded = attributeSession(tool.name, args);
187
+ const result = await bridge.callTool(tool.name, forwarded, exec.signal);
177
188
  if (exec.signal.aborted)
178
189
  throw new Error(`灵枢 ${tool.name} 已取消`);
179
190
  if (result.isError) {
@@ -0,0 +1,248 @@
1
+ # -*- coding: utf-8 -*-
2
+ """L5 证据审计面:只读组装器(路线 C)——「现值+出处链+两存/备择+盲区+退役史」。
3
+
4
+ 多主体世界模型对齐 v0.1 §4 路线 C:「给节点 id 输出『现值+出处链+两存/备择+
5
+ 盲区+退役史』。**纯读、零写路径改动**」。命名约束(设计稿复核修订):**不得
6
+ 复用 `view` 参数名**(已被 roleviews 占用,mdcos._candidates 的 view 面)。
7
+
8
+ 五面数据源(全部只读):
9
+ · 现值 —— `cg.get` 的 fm + 正文(层/生命态/验证态/时效/A1 检验强度/
10
+ A2 幽灵标记/归属面);
11
+ · 出处链 —— fm.derived_from(血缘)+ fm.edges(它引用谁)+ **反查**
12
+ 索引条目 edges(谁引用它,内存扫描免读盘)+ provenance 台账
13
+ `_link.jsonl` 相关行;
14
+ · 两存/备择 —— `cg.check_consistency(..., auto_flywheel=False, exclude=<自身>)`
15
+ 的**只读复用**(与写入链同一判据):同条件空间分歧/对照数/
16
+ 截断如实透出(「两存之为默认、冲突一等」的可见化);
17
+ · 盲区 —— negative/positive evidence 计数、非适用条件、A2 幽灵标记、
18
+ trust.validity 判定(未生效/已过期/未知)、verification_state;
19
+ · 退役史 —— fm.state_history / fm.verification_history(lifecycle/trust
20
+ 的既有 HISTORY_FIELD 快照)+ `evolution.history` 台账。
21
+
22
+ 成本如实:备择面复用一致性检查,大库上有候选截断(截断计数如实带出,hint
23
+ 与写入面同款);本组装器**不落盘、不改任何状态**。超出 cap 的面截断并计数。
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import os
28
+
29
+ __all__ = ["build"]
30
+
31
+ #: 各面截断上限(超限计数并如实带出 truncated)。
32
+ DEFAULT_CAP = 8
33
+
34
+
35
+ def _readable_guard(cg):
36
+ """读隔离谓词(与 linkref.known_ids 同口径):不可调用时全放行。"""
37
+ fn = getattr(cg, "_readable", None)
38
+ return fn if callable(fn) else (lambda _e: True)
39
+
40
+
41
+ def _referenced_by(cg, node_id, cap):
42
+ """反查:谁引用了本节点(索引条目 edges 的内存扫描;免读盘)。
43
+
44
+ **读隔离**(2026-10-05 复核 R3):扫描按 `cg._readable` 过滤——不可读
45
+ 节点的 id 与其引用关系不得经本面泄漏(与 linkref.known_ids /
46
+ evolution.entries 同口径:「不可见即不存在」)。
47
+ """
48
+ # H-4 纪律(test_h4_sustain_snapshot G1a):从 cg.index 取回的 nodes
49
+ # 须先取**快照**再迭代——并发写者 flush 时裸迭代会撞「字典变更」。
50
+ _live = (getattr(cg, "index", None) or {}).get("nodes") or {}
51
+ nodes = dict(_live)
52
+ guard = _readable_guard(cg)
53
+ out, total = [], 0
54
+ for nid, e in nodes.items():
55
+ if nid == node_id:
56
+ continue
57
+ try:
58
+ if not guard(e):
59
+ continue
60
+ except Exception: # noqa: BLE001 —— fail-closed
61
+ continue
62
+ for ed in (e.get("edges") or []):
63
+ if str(ed.get("target")) == node_id:
64
+ total += 1
65
+ if len(out) < cap:
66
+ out.append({"from": nid, "relation": ed.get("relation_type"),
67
+ "confidence": ed.get("confidence"),
68
+ "source": ed.get("source")})
69
+ break
70
+ return {"items": out, "total": total, "truncated": total > len(out)}
71
+
72
+
73
+ def _link_rows(cg, node_id, cap):
74
+ """provenance 台账(`_link.jsonl`)里与本节点相关的行。
75
+
76
+ **schema 对齐 provenance.make_edge**(2026-10-05 复核 R1 修):真字段是
77
+ `child/parent/rel/t/actor`(原实现按 `target/source/relation/at` 匹配
78
+ ⇒ 恒读空,整面死)。「与本节点相关」= child==本节点(它派生自谁)或
79
+ parent==本节点(谁派生自它)。
80
+
81
+ **行级读隔离**(2026-10-05 复验 R4 修):行是否可见取决于**另一端**——
82
+ 「不可见即不存在」的口径下,verify 身份不得经本面看到不可读 secret
83
+ 节点的 id 与其派生关系(复验实证:修复前与 designer 视角逐位相同)。
84
+ 另一端条目不在索引或不可读 ⇒ **整行不出**(fail-closed,与
85
+ `_referenced_by` 的 guard 同口径;自身端必可读——本函数只在本节点
86
+ 已可读[build 的 cg.get 成功]之后被调用)。
87
+ """
88
+ try:
89
+ from . import provenance
90
+ path = provenance.ledger_file(getattr(cg, "root", None))
91
+ except Exception: # noqa: BLE001
92
+ return {"items": [], "total": 0, "truncated": False}
93
+ if not path or not os.path.exists(path):
94
+ return {"items": [], "total": 0, "truncated": False}
95
+ _live = (getattr(cg, "index", None) or {}).get("nodes") or {}
96
+ nodes = dict(_live) # H-4 快照纪律(迭代前取快照)
97
+ guard = _readable_guard(cg)
98
+ out, total = [], 0
99
+ try:
100
+ with open(path, encoding="utf-8", errors="replace") as f:
101
+ for line in f:
102
+ line = line.strip()
103
+ if not line:
104
+ continue
105
+ try:
106
+ import json
107
+ rec = json.loads(line)
108
+ except Exception: # noqa: BLE001
109
+ continue
110
+ if not isinstance(rec, dict):
111
+ continue
112
+ c, p = str(rec.get("child")), str(rec.get("parent"))
113
+ if c == node_id:
114
+ other = p
115
+ elif p == node_id:
116
+ other = c
117
+ else:
118
+ continue
119
+ e_other = nodes.get(other)
120
+ if e_other is None:
121
+ continue # 另一端不在索引(含已删)→ 不出
122
+ try:
123
+ if not guard(e_other):
124
+ continue # 另一端不可读 → 整行不出
125
+ except Exception: # noqa: BLE001 —— fail-closed
126
+ continue
127
+ total += 1
128
+ if len(out) < cap:
129
+ out.append({k: rec.get(k) for k in
130
+ ("child", "parent", "rel", "t", "actor", "note")})
131
+ except Exception: # noqa: BLE001 —— 只读面不抛
132
+ pass
133
+ return {"items": out, "total": total, "truncated": total > len(out)}
134
+
135
+
136
+ def build(cg, node_id, cap=DEFAULT_CAP) -> dict:
137
+ """组装五面只读视图。node_id 不存在 ⇒ {"ok": False, "error": "node_not_found"}。
138
+
139
+ cap 防御(2026-10-05 复核 Y2):非数/None 回落缺省、钳 [1, 64]——
140
+ 直调面与 MCP 面同语义(MCP 面的钳制在分发层,直调 API 不得裸抛 TypeError)。
141
+ """
142
+ try:
143
+ cap = int(cap)
144
+ except (TypeError, ValueError):
145
+ cap = DEFAULT_CAP
146
+ cap = max(1, min(64, cap))
147
+ nid = str(node_id or "").strip()
148
+ if not nid:
149
+ return {"ok": False, "error": "node_not_found", "node_id": node_id}
150
+ rec = cg.get(nid)
151
+ if not rec:
152
+ return {"ok": False, "error": "node_not_found", "node_id": nid}
153
+ fm = rec.get("frontmatter") or {}
154
+ content = rec.get("content") or ""
155
+ from . import evolution, lifecycle, nodefile, trust
156
+
157
+ # ---- 面一:现值 ----
158
+ try:
159
+ _validity = trust.validity(fm)
160
+ except Exception as exc: # noqa: BLE001
161
+ _validity = "%s: %s" % (type(exc).__name__, exc)
162
+ present = {
163
+ "layer": fm.get("layer"),
164
+ "lifecycle_state": lifecycle.state_of(fm),
165
+ "verification_state": trust.state_of(fm),
166
+ "validity": _validity,
167
+ "importance": fm.get("importance"),
168
+ "sensitivity": fm.get("sensitivity"),
169
+ "created_at": fm.get("created_at"),
170
+ "tags": fm.get("tags"),
171
+ "verification_basis": fm.get("verification_basis"),
172
+ nodefile.CHECK_STRENGTH_FIELD: fm.get(nodefile.CHECK_STRENGTH_FIELD),
173
+ nodefile.UNCERTAIN_REFS_FIELD: fm.get(nodefile.UNCERTAIN_REFS_FIELD),
174
+ "writer": fm.get("writer"), "harness": fm.get("harness"),
175
+ "session": fm.get("session"),
176
+ "content_chars": len(content),
177
+ }
178
+
179
+ # ---- 面二:出处链 ----
180
+ _edges = list(fm.get("edges") or [])
181
+ sources = {
182
+ "derived_from": fm.get("derived_from"),
183
+ "derived_relation": fm.get("derived_relation"),
184
+ "refers_to": {"items": _edges[:cap], "total": len(_edges),
185
+ "truncated": len(_edges) > cap},
186
+ "referenced_by": _referenced_by(cg, nid, cap),
187
+ "link_ledger": _link_rows(cg, nid, cap),
188
+ }
189
+
190
+ # ---- 面三:两存/备择(一致性只读复用;不建单、不落盘)----
191
+ alternatives = {"comparable": None, "verdict": None, "conflicts": [],
192
+ "missing": [], "truncated": None, "scanned": None}
193
+ try:
194
+ # log_write=False(复核 R2 修):只读消费面——判定照常、台账不写
195
+ #(consistency.log 原为无条件追加,审计会变成隐式写)。
196
+ cvd = cg.check_consistency(
197
+ content, layer=fm.get("layer") or "knowledge",
198
+ condition_space=fm.get("condition_space"),
199
+ non_applicable_conditions=fm.get("non_applicable_conditions"),
200
+ tags=fm.get("tags"), exclude=nid, auto_flywheel=False,
201
+ log_write=False)
202
+ if isinstance(cvd, dict):
203
+ cf = list(cvd.get("conflicts") or [])
204
+ alternatives = {
205
+ "verdict": cvd.get("verdict"), "reason": cvd.get("reason"),
206
+ "conflict_strength": cvd.get("conflict_strength"),
207
+ "comparable": cvd.get("comparable"),
208
+ "scanned": cvd.get("scanned"), "truncated": cvd.get("truncated"),
209
+ "conflicts": cf[:cap],
210
+ "conflicts_total": len(cf), "conflicts_truncated": len(cf) > cap,
211
+ "missing": list(cvd.get("missing") or [])[:cap],
212
+ "hint": cvd.get("hint"),
213
+ }
214
+ except Exception as exc: # noqa: BLE001 —— 只读面不抛
215
+ alternatives["error"] = "%s: %s" % (type(exc).__name__, exc)
216
+
217
+ # ---- 面四:盲区 ----
218
+ blindspots = {
219
+ "negative_evidence": fm.get("negative_evidence", 0),
220
+ "positive_evidence": fm.get("positive_evidence", 0),
221
+ "non_applicable_conditions": fm.get("non_applicable_conditions"),
222
+ nodefile.UNCERTAIN_REFS_FIELD: fm.get(nodefile.UNCERTAIN_REFS_FIELD),
223
+ "verification_state": trust.state_of(fm),
224
+ "validity": _validity,
225
+ "note": ("未决不进二值:DEFER/unresolved 痕迹在『两存/备择』与 "
226
+ "negative_evidence 面;本面只呈现,不下判断。"),
227
+ }
228
+
229
+ # ---- 面五:退役史 ----
230
+ _sh = list(fm.get(lifecycle.HISTORY_FIELD) or [])
231
+ _vh = list(fm.get(trust.HISTORY_FIELD) or [])
232
+ _ev = []
233
+ try:
234
+ _ev = list((evolution.history(cg, nid, limit=cap) or {}).get("entries") or [])
235
+ except Exception: # noqa: BLE001
236
+ pass
237
+ history = {
238
+ lifecycle.HISTORY_FIELD: _sh[-cap:],
239
+ trust.HISTORY_FIELD: _vh[-cap:],
240
+ "evolution": _ev[:cap],
241
+ "totals": {lifecycle.HISTORY_FIELD: len(_sh),
242
+ trust.HISTORY_FIELD: len(_vh), "evolution_scanned": len(_ev)},
243
+ }
244
+
245
+ return {"ok": True, "node_id": nid, "cap": cap,
246
+ "present": present, "sources": sources,
247
+ "alternatives": alternatives, "blindspots": blindspots,
248
+ "history": history}
@@ -40,7 +40,8 @@
40
40
  python -X utf8 -m md_cg.bench_e2e_judge --quick # 冒烟 20题×level 0,1×无LLM
41
41
  python -X utf8 -m md_cg.bench_e2e_judge # 全量 120题×4级×3臂+LLM两轮
42
42
  python -X utf8 -m md_cg.bench_e2e_judge --skip-llm # 只跑确定性两臂
43
- 环境:DEEPSEEK_API_KEY(LLM 臂);数据落 --data-root(默认 D:/program/test/e2e_judge)
43
+ 环境:DEEPSEEK_API_KEY(LLM 臂);数据落 --data-root(默认 <仓根父目录>/test/e2e_judge,
44
+ 可用 MDCG_E2E_JUDGE_ROOT 覆盖)
44
45
  """
45
46
  from __future__ import annotations
46
47
 
@@ -64,7 +65,8 @@ from md_cg.mdcos import MdCGOS # noqa: E402
64
65
  from md_cg import nodefile # noqa: E402
65
66
 
66
67
  DATA_SRC = os.path.join(HERE, "data", "benchmarks", "locomo-zh-500")
67
- DEFAULT_ROOT = r"D:\program\test\e2e_judge"
68
+ DEFAULT_ROOT = os.environ.get("MDCG_E2E_JUDGE_ROOT") or os.path.join(
69
+ os.path.dirname(HERE), "test", "e2e_judge")
68
70
  FUSION_KW = dict(paths=("lexical", "bucket", "entity", "graph", "fuzzy"),
69
71
  fusion="sum")
70
72
  LEVELS_DEFAULT = (0, 1, 2, 4)
package/md_cg/branches.py CHANGED
@@ -111,6 +111,12 @@ def fork(cg, node_ids, branch_id=None, note=None) -> dict:
111
111
  condition_space=fm.get("condition_space"),
112
112
  importance=float(fm.get("importance", 0.5)),
113
113
  verification_basis=fm.get("verification_basis") or "test",
114
+ # A1(2026-10-05,复核补):断言质量字段随副本携带(与
115
+ # verification_basis 同族;缺省 None 时 add 不落键,零行为变化)。
116
+ check_strength=fm.get("check_strength"),
117
+ # A2(2026-10-05):幽灵引用标记随副本携带(正文属性——副本
118
+ # 正文与主支一致则标记有效;缺省 None 不落键)。
119
+ uncertain_refs=fm.get("uncertain_refs"),
114
120
  non_applicable_conditions=fm.get("non_applicable_conditions"),
115
121
  consistency=False,
116
122
  derived_from=[nid], relation="split_from",
@@ -156,8 +162,16 @@ def rewrite(cg, node_id, content, tags=None, importance=None,
156
162
  if isinstance(content, dict) or content is None:
157
163
  return {"ok": False, "error": "content 须为字符串"}
158
164
  for k in ("branch_id", "branched_from", "branch_note", "derived_from",
159
- "relation", "merged_from_branch", "override", "consistency"):
160
- extra.pop(k, None) # 归属/血缘/写法闸不可经 extra 篡改
165
+ "relation", "merged_from_branch", "override", "consistency",
166
+ "uncertain_refs"):
167
+ extra.pop(k, None) # 归属/血缘/写法闸/检测产物不可经 extra 篡改
168
+ # A2(2026-10-05,复核 yellow-1 修):标记按**新正文**重算——本函数是
169
+ # 「改正文的唯一正路」,携带旧标记会与正文脱节(干净正文带陈旧标记 /
170
+ # 幽灵正文漏标,双向都错);与 mdcg.add「正文属性、按当次检测重算」
171
+ # 的声明同口径。
172
+ from . import ghostref as _ghostref
173
+ _ur = _ghostref.find_ghost_phrases(
174
+ str(content or ""), known=set((cg.index.get("nodes") or {}).keys()))
161
175
  cg.add(nid, str(content or ""),
162
176
  layer=fm.get("layer") or e.get("layer") or "knowledge",
163
177
  tags=list(tags if tags is not None else (fm.get("tags") or [])),
@@ -165,6 +179,10 @@ def rewrite(cg, node_id, content, tags=None, importance=None,
165
179
  else (fm.get("importance") or 0.5)),
166
180
  condition_space=fm.get("condition_space"),
167
181
  verification_basis=fm.get("verification_basis") or "test",
182
+ # A1(2026-10-05,复核补):断言质量字段随重写携带(同上)。
183
+ check_strength=fm.get("check_strength"),
184
+ # A2(2026-10-05):幽灵引用标记按新正文重算(见上)。
185
+ uncertain_refs=_ur or None,
168
186
  non_applicable_conditions=fm.get("non_applicable_conditions"),
169
187
  override=True, consistency=False,
170
188
  derived_from=list(fm.get("derived_from") or []),
@@ -203,13 +221,21 @@ def merge(cg, branch_id, reason=None) -> dict:
203
221
  chain = list(old_fm.get("derived_from") or [])
204
222
  if nid not in chain:
205
223
  chain.append(nid)
206
- cg.add(orig, (src or {}).get("content") or "",
224
+ # A2(2026-10-05,复核 yellow-2① 修):主支正文 = 分支正文 ⇒ 标记按
225
+ # 该正文重算(此前不携带 = 幽灵正文回写后无标记,方向与「宁标勿漏」
226
+ # 相反;fork/rewrite 已接线,此处补齐同族)。
227
+ from . import ghostref as _ghostref
228
+ _mcontent = (src or {}).get("content") or ""
229
+ _mur = _ghostref.find_ghost_phrases(
230
+ _mcontent, known=set((cg.index.get("nodes") or {}).keys()))
231
+ cg.add(orig, _mcontent,
207
232
  layer=old_fm.get("layer") or e.get("layer") or "knowledge",
208
233
  tags=list(old_fm.get("tags") or e.get("tags") or []),
209
234
  condition_space=src_fm.get("condition_space"),
210
235
  importance=float(old_fm.get("importance", 0.5)),
211
236
  verification_basis=old_fm.get("verification_basis") or "test",
212
237
  non_applicable_conditions=old_fm.get("non_applicable_conditions"),
238
+ uncertain_refs=_mur or None,
213
239
  override=True, consistency=False,
214
240
  derived_from=chain, relation="merged_from",
215
241
  merged_from_branch=bid, actor="branch_merge")
@@ -51,6 +51,7 @@ import os
51
51
  import time
52
52
 
53
53
  from .mdcg import expand_query_terms_weighted
54
+ from . import nodefile
54
55
 
55
56
  # --------------------------------------------------------------------------
56
57
  # 常量(全部可审计、可调)
@@ -250,21 +251,28 @@ def _ban_hit(content, neg_texts):
250
251
  return False
251
252
 
252
253
 
253
- # 生效条件:content 为真时按行过滤,跳过 strip 后以 "#" 开头且含全角或半角冒号的行,其余原行以换行连接返回;content 假值按空串返回 "";
254
+ # 生效条件:委托 nodefile._strip_ccg_segments(content, nodefile.CCG_MARKS)(剥除算法单点)——命中 CCG 要素标题行(判据单点 nodefile._ccg_heading_rest,`^#\s*<要素>` 冒号可有可无)即整段剥除:行内带值(`# 生效条件:v`/前缀式)只剥该行,裸标题(`# 不适用条件`)连同其后首个非空、非标题行(该字段的值行,取值口径同 nodefile.ccg_field_value)一并剥除;其余原行以换行连接返回;content 假值按空串返回 "";
254
255
  def _body_text(content):
255
- """去掉 CCG 声明行(`# 字段:值`)后的正文。
256
+ """去掉 CCG 声明**整段字段**后的正文。
256
257
 
257
258
  自否定看的是「正文/生效条件是否与不适用条件矛盾」,不能把节点自己声明的
258
259
  `# 不适用条件:X` 当成 X 出现在正文里——否则**每个**声明了不适用条件的
259
260
  正常节点都会被误判为自相矛盾(真实 CCG 条目普遍带该字段)。
261
+
262
+ N238:判据复用**单点** `nodefile._ccg_heading_rest`(与写入闸门
263
+ data/policy.json 同一行语义、`mdcos._ccg_field` / `ccg_mark_present` 同源),
264
+ 并连同**无冒号形态的值行**一起剥。此前本地判据是「以 # 开头**且含冒号**」,
265
+ 裸标题 `# 不适用条件` 与其值行双双残留正文:同内容写成带冒号时
266
+ `_body_text == ''`(不命中),写成无冒号时 body 含禁令短语 ⇒ `_ban_hit`
267
+ 恒中 ⇒ self_negation / strength=1.0 ⇒ REJECT(writepipe 下合法节点被拦入
268
+ review_queue 并自动建飞轮工单)。「冒号可有可无」已由
269
+ `nodefile.ccg_mark_present` / `ccg_field_value` 定案(test_ccg_form_parity
270
+ 验收 PASS),本处是漏网的分叉。
271
+
272
+ A3 追认:剥除算法本身也已收单点 `nodefile._strip_ccg_segments`(六要素全剥)
273
+ ——本处不再自持循环,避免与 `nodefile.positive_body` 各写一份而再次漂移。
260
274
  """
261
- out = []
262
- for line in (content or "").splitlines():
263
- s = line.strip()
264
- if s.startswith("#") and (":" in s or ":" in s):
265
- continue
266
- out.append(line)
267
- return "\n".join(out)
275
+ return nodefile._strip_ccg_segments(content, nodefile.CCG_MARKS)
268
276
 
269
277
 
270
278
  # 生效条件:text 假值时按空串处理,返回把任意连续空白(半角/全角空格、\t、\r、\n)折叠为单个半角空格并去掉首尾空白后的字符串;纯空白文本返回空串 "";
@@ -484,16 +492,21 @@ def _recursive_reflect(cg, seeds, tw_pos, tw_neg, max_depth=MAX_DEPTH,
484
492
  # 主入口:三级决策
485
493
  # --------------------------------------------------------------------------
486
494
 
487
- # 生效条件:以 cg.index.nodes 为既有节点、content(假值按 "")经 _new_terms 得 pos/neg 并算 tw_pos/tw_neg(non_applicable_conditions 入参先按 mdcos._is_null_condition 剔除空值语义哨兵——⑧ 漏斗单点);选面(cons200;收口轮①加面内例外):快照级预筛剔「tw_pos 与 tw_neg 皆空(新节点未声明可展开条件)时的非纪律候选」,且**剔除永不触及修前扫描面**(过滤后索引序前 limit 内候选一律保留——面内例外;「修后检出 ⊇ 修前」由此成为与「条目 tags 与盘面 fm.tags 同源」无关的结构保证),候选超 limit 时精比面=相关面(_sift_score 序)前 limit ∪ 保底面(过滤后索引序前 limit ∩ 候选),记 scanned(候选数)/kept(精比数)/truncated(scanned>kept)/hint;按循环中 hard(自否定或纪律命中)→ divergences(同条件槽且 concl < CONCLUSION_SAME)→ strength ≥ CLASH_HIGH → strength ≥ CLASH_LOW → comparable==0 且 (pos or neg) → 否则 ACCEPT 的顺序定 verdict;DEFER 且 int(depth)>0 且 emo["bias"] != "approaching" 时调 _recursive_reflect 补 recursion,auto_flywheel 且 verdict == REJECT 时加 unresolved_id,最后 log 并返回 rec;
495
+ # 生效条件:以 cg.index.nodes 为既有节点、content(假值按 "")经 _new_terms 得 pos/neg 并算 tw_pos/tw_neg(non_applicable_conditions 入参先按 mdcos._is_null_condition 剔除空值语义哨兵——⑧ 漏斗单点);选面(cons200;收口轮①加面内例外):快照级预筛剔「tw_pos 与 tw_neg 皆空(新节点未声明可展开条件)时的非纪律候选」,且**剔除永不触及修前扫描面**(过滤后索引序前 limit 内候选一律保留——面内例外;「修后检出 ⊇ 修前」由此成为与「条目 tags 与盘面 fm.tags 同源」无关的结构保证),候选超 limit 时精比面=相关面(_sift_score 序)前 limit ∪ 保底面(过滤后索引序前 limit ∩ 候选),记 scanned(候选数)/kept(精比数)/truncated(scanned>kept)/hint;按循环中 hard(自否定或纪律命中)→ divergences(同条件槽且 concl < CONCLUSION_SAME)→ strength ≥ CLASH_HIGH → strength ≥ CLASH_LOW → comparable==0 且 (pos or neg) → 否则 ACCEPT 的顺序定 verdict;DEFER 且 int(depth)>0 且 emo["bias"] != "approaching" 时调 _recursive_reflect 补 recursion,auto_flywheel 且 verdict == REJECT 时加 unresolved_id,最后 log_write 为真时 log 并返回 rec;N276:精比环的索引代际探针循环外单探一次(环内 cg.get(nid, probe=False);无探活面载体回落 cg.get(nid),行为零变);
488
496
  def check(cg, content, layer=None, condition_space=None,
489
497
  non_applicable_conditions=None, tags=None, exclude=None,
490
498
  limit=MAX_SCAN, depth=MAX_DEPTH, auto_flywheel=False,
491
- query=None):
499
+ query=None, log_write=True):
492
500
  """节点间自动冲突检测(L0 情绪 → L1 反思 → L2 递归反思)。
493
501
 
494
502
  返回完整判据(可审计):
495
503
  verdict / reason / conflict_strength / emotional / conflicts[] /
496
504
  recursion{} / missing[] / unresolved_id(若触发飞轮)
505
+
506
+ log_write(路线 C,2026-10-05):缺省 True=原行为(每次判定落
507
+ `_consistency.jsonl` 台账)。**只读消费面**(如 auditview 的证据审计)
508
+ 须传 False——判定结果照常返回,但不写台账(复核 R2:审计面承诺
509
+ 「纯读、不落盘」,而 log() 无条件追加会把每次审计变成一次写)。
497
510
  + 选面读数 scanned / kept / truncated(超限时另附 hint)
498
511
 
499
512
  verdict:
@@ -619,9 +632,20 @@ def check(cg, content, layer=None, condition_space=None,
619
632
  scanned = len(cand) # 预筛后候选数(截断前)
620
633
  kept = len(scan_face) # 实际精比数(相关面 ∪ 保底面)
621
634
  truncated = scanned > kept
635
+ # N276(第 32 轮性能面):scan_face 精比环内逐节点 cg.get 会各自重探索引
636
+ # 签名(快照 stat + listdir + 逐分片 stat,单点实测 ~38µs;本库 200 次/
637
+ # 调用全同签名重探)——循环外单探一次、环内 probe=False;语义窗口=本批
638
+ # 读期间「他进程写入」不被探知(毫秒级,与读缓存已声明的进程内一致性
639
+ # 边界同款);无探活面的载体(测试桩)回落原 cg.get(nid),行为零变。
640
+ _probe = getattr(cg, "_maybe_reload_index", None)
641
+ if callable(_probe):
642
+ _probe()
643
+ else:
644
+ _probe = None
622
645
  seeds = []
623
646
  for nid, e in scan_face:
624
- node = cg.get(nid) or {}
647
+ node = (cg.get(nid, probe=False) if _probe is not None
648
+ else cg.get(nid)) or {}
625
649
  fm = node.get("frontmatter") or {}
626
650
  body = node.get("content") or ""
627
651
  e_pos, e_neg = _declared(fm, body)
@@ -769,7 +793,8 @@ def check(cg, content, layer=None, condition_space=None,
769
793
  if auto_flywheel and verdict in FLYWHEEL_TRIGGERS:
770
794
  rec["unresolved_id"] = _fire_flywheel(cg, query or content, verdict,
771
795
  reason, missing, allc)
772
- log(cg, rec)
796
+ if log_write:
797
+ log(cg, rec)
773
798
  return rec
774
799
 
775
800