@furongjun1999/dsh-memory 0.4.8 → 0.4.10

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 (232) hide show
  1. package/README.md +54 -26
  2. package/codebuddy/CODEBUDDY.md +196 -195
  3. package/codebuddy/README.md +13 -1
  4. package/codebuddy/mcp.json +9 -0
  5. package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -0
  6. package/docs/README.md +1 -1
  7. package/docs/discipline/harnesses.yaml +18 -7
  8. package/docs/discipline/templates/full.md.tmpl +4 -3
  9. package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
  10. package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
  11. package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
  12. package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
  13. package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
  14. package/docs/hive//345/244/232/347/253/257harness/351/200/232/344/277/241/345/245/221/347/272/246_v0.1.md +42 -0
  15. package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -0
  16. 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 +102 -0
  17. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S1b/344/270/216/345/217/254/345/233/236/346/235/203/350/241/241_v0.1.md +57 -0
  18. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S7/345/200/222/346/216/222/345/200/231/351/200/211/345/261/202_v0.1.md +126 -0
  19. 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 +503 -0
  20. package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -0
  21. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
  22. package/docs/mdcg//344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/345/245/221/347/272/246_v0.1.md +82 -0
  23. package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -0
  24. 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 +47 -45
  25. package/docs/mdcg//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/{206_v0.4.md → 206_v0.5.md} +92 -4
  26. 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 +172 -0
  27. package/docs/mdcg//347/216/257/344/272/214_/347/231/275/347/256/261/345/241/253/345/205/205/346/265/201/346/260/264/347/272/277_v0.1.md +31 -0
  28. package/docs/mdcg//350/267/250/347/253/257/351/252/214/350/257/201/344/270/216/345/220/214/346/255/245/345/215/217/350/256/256_v0.1.md +93 -0
  29. package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +2 -2
  30. package/docs/theory//347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +3 -3
  31. package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +2 -2
  32. 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 +434 -433
  33. package/docs//347/201/265/346/236/242/350/207/252/346/210/221/346/224/271/350/277/233/345/267/245/344/275/234/350/256/241/345/210/222_/345/244/226/351/203/250/347/240/224/347/251/266/347/263/273/345/210/227/345/220/270/346/224/266_v1_20260919.md +286 -0
  34. package/dsh/README.md +33 -0
  35. package/dsh/cordis.yml.example +13 -7
  36. package/dsh/hive-mcp-probe.mjs +94 -0
  37. package/dsh/hive-mcp.example.yml +62 -0
  38. package/dsh/update-lingshu.bat +11 -0
  39. package/dsh/update-lingshu.ps1 +337 -0
  40. package/lib/hooks.d.ts +3 -0
  41. package/lib/hooks.js +17 -23
  42. package/lib/index.d.ts +4 -2
  43. package/lib/index.js +29 -6
  44. package/lib/lib/datapath.d.ts +76 -1
  45. package/lib/lib/datapath.js +199 -13
  46. package/lib/lib/mdcg_client.d.ts +40 -3
  47. package/lib/lib/mdcg_client.js +46 -22
  48. package/lib/lib/mutual.js +4 -4
  49. package/lib/lib/token_store.js +4 -5
  50. package/md_cg/audit.py +17 -2
  51. package/md_cg/autonomy.py +86 -15
  52. package/md_cg/backfill.py +36 -1
  53. package/md_cg/backfill_bigdomain.py +34 -0
  54. package/md_cg/bench6_arms.py +28 -1
  55. package/md_cg/bench6_common.py +10 -1
  56. package/md_cg/bench6_competitors.py +6 -1
  57. package/md_cg/bench_axis_domain.py +9 -1
  58. package/md_cg/bench_blind_comp.py +7 -1
  59. package/md_cg/bench_en_atoms_public.py +9 -0
  60. package/md_cg/bench_governance.py +348 -0
  61. package/md_cg/bench_lme_zh.py +16 -1
  62. package/md_cg/bench_locomo.py +2 -1
  63. package/md_cg/bench_locomo_zh.py +16 -1
  64. package/md_cg/bench_locomo_zh_public.py +4 -1
  65. package/md_cg/bench_longmem.py +2 -1
  66. package/md_cg/bench_membench.py +27 -1
  67. package/md_cg/bench_p0.py +4 -1
  68. package/md_cg/bench_progressive.py +13 -1
  69. package/md_cg/bench_role_views.py +238 -0
  70. package/md_cg/bench_task_ab.py +8 -1
  71. package/md_cg/bench_task_ab_llm.py +13 -1
  72. package/md_cg/bench_unified_en.py +6 -1
  73. package/md_cg/bench_zh_mad.py +20 -1
  74. package/md_cg/blindspot_tickets.py +123 -0
  75. package/md_cg/branches.py +12 -1
  76. package/md_cg/build_postings.py +73 -0
  77. package/md_cg/ccgc.py +67 -2
  78. package/md_cg/census.py +5 -1
  79. package/md_cg/chain.py +24 -3
  80. package/md_cg/codeindex.py +134 -17
  81. package/md_cg/coldverify.py +265 -0
  82. package/md_cg/comment_gate.py +338 -0
  83. package/md_cg/cond_compose.py +190 -0
  84. package/md_cg/cond_facts.py +155 -0
  85. package/md_cg/cond_template.json +107 -0
  86. package/md_cg/condition_anchor.py +143 -0
  87. package/md_cg/conformance.py +69 -4
  88. package/md_cg/consistency.py +24 -1
  89. package/md_cg/consolidate.py +53 -2
  90. package/md_cg/corpus.py +4 -0
  91. package/md_cg/crosscheck.py +42 -2
  92. package/md_cg/crypto.py +35 -1
  93. package/md_cg/d_meta.py +310 -0
  94. package/md_cg/datapath.py +201 -26
  95. package/md_cg/docindex.py +122 -1
  96. package/md_cg/eval_common.py +29 -1
  97. package/md_cg/evidence.py +27 -1
  98. package/md_cg/evolution.py +21 -1
  99. package/md_cg/export.py +11 -1
  100. package/md_cg/forgetting.py +23 -1
  101. package/md_cg/fsutil.py +18 -1
  102. package/md_cg/hotcache.py +214 -0
  103. package/md_cg/hyperedge.py +251 -0
  104. package/md_cg/identity.py +18 -1
  105. package/md_cg/insight.py +17 -1
  106. package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
  107. package/md_cg/lexicon/build_standard_en.py +171 -168
  108. package/md_cg/lexicon/expand_en_zh.py +6 -0
  109. package/md_cg/lifecycle.py +12 -1
  110. package/md_cg/linkref.py +281 -0
  111. package/md_cg/links.py +29 -1
  112. package/md_cg/mcp_server.py +362 -43
  113. package/md_cg/md_whitebox.py +53 -1
  114. package/md_cg/mdcg.py +1003 -27
  115. package/md_cg/mdcos.py +558 -36
  116. package/md_cg/metacognition.py +37 -2
  117. package/md_cg/migrate.py +4 -0
  118. package/md_cg/migrate_aeis.py +221 -213
  119. package/md_cg/migrate_roleplay.py +8 -0
  120. package/md_cg/migrate_wisdom_graph.py +14 -1
  121. package/md_cg/mreview/__main__.py +3 -0
  122. package/md_cg/mreview/bundle.py +8 -0
  123. package/md_cg/mreview/candidates.py +9 -0
  124. package/md_cg/mreview/govern.py +21 -1
  125. package/md_cg/mreview/locate.py +34 -0
  126. package/md_cg/mreview/pipeline.py +29 -1
  127. package/md_cg/mreview/ruleset.py +16 -1
  128. package/md_cg/nodefile.py +233 -3
  129. package/md_cg/pooling.py +23 -1
  130. package/md_cg/postings.py +298 -0
  131. package/md_cg/predict.py +89 -9
  132. package/md_cg/progressive.py +3 -0
  133. package/md_cg/protect.py +14 -1
  134. package/md_cg/protocol.py +372 -0
  135. package/md_cg/provenance.py +262 -0
  136. package/md_cg/reach.py +453 -0
  137. package/md_cg/refindex.py +47 -2
  138. package/md_cg/refine.py +20 -1
  139. package/md_cg/roleviews.py +89 -0
  140. package/md_cg/routing.py +76 -0
  141. package/md_cg/scrub.py +63 -2
  142. package/md_cg/security.py +26 -1
  143. package/md_cg/self_state.py +64 -1
  144. package/md_cg/selfreport.py +151 -0
  145. package/md_cg/semantic/canonical.py +5 -0
  146. package/md_cg/semantic/en_normalizer.py +364 -355
  147. package/md_cg/semantic/zh_en_atoms.py +139 -136
  148. package/md_cg/signer.py +41 -1
  149. package/md_cg/sources.py +583 -547
  150. package/md_cg/statushdr.py +179 -0
  151. package/md_cg/stg.py +59 -18
  152. package/md_cg/subgraph.py +23 -0
  153. package/md_cg/sustain.py +56 -1
  154. package/md_cg/tasks.py +26 -2
  155. package/md_cg/test_autonomy.py +26 -0
  156. package/md_cg/test_bench_governance.py +102 -0
  157. package/md_cg/test_blindspot_tickets.py +166 -0
  158. package/md_cg/test_ccgc.py +10 -0
  159. package/md_cg/test_codeindex.py +338 -0
  160. package/md_cg/test_comment_gate.py +187 -0
  161. package/md_cg/test_cond_compose_anchors.py +76 -0
  162. package/md_cg/test_condition_anchor.py +82 -0
  163. package/md_cg/test_d_meta.py +412 -0
  164. package/md_cg/test_datapath_root.py +188 -0
  165. package/md_cg/test_gain_gate.py +47 -1
  166. package/md_cg/test_hot_cold.py +187 -0
  167. package/md_cg/test_hyperedge.py +245 -0
  168. package/md_cg/test_linkref.py +306 -0
  169. package/md_cg/test_md_access_parity.py +15 -3
  170. package/md_cg/test_mr_m1.py +108 -18
  171. package/md_cg/test_mr_m3.py +8 -1
  172. package/md_cg/test_p26_refindex.py +49 -20
  173. package/md_cg/test_p27_docindex.py +236 -2
  174. package/md_cg/test_p2_mcp.py +1 -1
  175. package/md_cg/test_p31_insight.py +24 -0
  176. package/md_cg/test_p44_md_whitebox.py +14 -1
  177. package/md_cg/test_protocol.py +243 -0
  178. package/md_cg/test_reach.py +378 -0
  179. package/md_cg/test_reach_keys.py +201 -0
  180. package/md_cg/test_reach_meta_exits.py +145 -0
  181. package/md_cg/test_read_clip.py +8 -4
  182. package/md_cg/test_retr_s1.py +340 -0
  183. package/md_cg/test_retr_s1b.py +209 -0
  184. package/md_cg/test_retr_s3.py +194 -0
  185. package/md_cg/test_retr_s4.py +163 -0
  186. package/md_cg/test_retr_s5.py +200 -0
  187. package/md_cg/test_retr_s6.py +157 -0
  188. package/md_cg/test_retr_s7.py +385 -0
  189. package/md_cg/test_retr_s8_time.py +316 -0
  190. package/md_cg/test_retr_s9_edges.py +286 -0
  191. package/md_cg/test_retr_s9_entity_ctx.py +175 -0
  192. package/md_cg/test_review_conformance.py +59 -2
  193. package/md_cg/test_role_views.py +354 -0
  194. package/md_cg/test_subproc_encoding.py +188 -0
  195. package/md_cg/test_trust.py +361 -0
  196. package/md_cg/test_units_poll.py +71 -0
  197. package/md_cg/test_v14_fixes.py +397 -0
  198. package/md_cg/test_validity_filter.py +280 -0
  199. package/md_cg/test_wisdom_md_store.py +7 -3
  200. package/md_cg/test_writepipe.py +5 -1
  201. package/md_cg/theory.py +16 -1
  202. package/md_cg/tokens.py +40 -8
  203. package/md_cg/tool_face.py +13 -2
  204. package/md_cg/trust.py +943 -0
  205. package/md_cg/twophase.py +12 -1
  206. package/md_cg/units.py +132 -10
  207. package/md_cg/vision_evidence.py +24 -1
  208. package/md_cg/weights.py +24 -1
  209. package/md_cg/whitebox.py +32 -1
  210. package/md_cg/whitebox_kb/data/verify_cache.json +21210 -365
  211. package/md_cg/whitebox_kb/data/verify_savings.jsonl +5078 -0
  212. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  213. package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
  214. package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
  215. package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
  216. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
  217. package/md_cg/writelimit.py +18 -4
  218. package/md_cg/writepipe.py +178 -7
  219. package/package.json +2 -2
  220. package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
  221. package/skills/skills/designer-perspective/scripts/designer.py +17 -1
  222. package/skills/skills/designer-perspective/tests/selftest.py +3 -1
  223. package/src/hooks.ts +17 -21
  224. package/src/index.ts +33 -6
  225. package/src/lib/datapath.ts +211 -13
  226. package/src/lib/mdcg_client.ts +64 -25
  227. package/src/lib/mutual.ts +411 -411
  228. package/src/lib/token_store.ts +4 -5
  229. package/zcode/AGENTS.md +196 -195
  230. package/zcode/README.md +4 -0
  231. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
  232. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
@@ -27,6 +27,7 @@ import json
27
27
  import os
28
28
  import time
29
29
 
30
+ from . import trust as _trust
30
31
  from .fsutil import FileLock, append_jsonl, atomic_write, read_jsonl
31
32
 
32
33
  LEDGER_NAME = "_link.jsonl"
@@ -54,16 +55,19 @@ class ProvenanceError(Exception):
54
55
  # 路径 / 规范化
55
56
  # --------------------------------------------------------------------------
56
57
 
58
+ # 生效条件:path 为真值时返回 path;否则 os.environ.get(LEDGER_ENV) 为非空真值时返回该环境变量值;否则返回 os.path.join(root, LEDGER_NAME)。
57
59
  def ledger_file(root: str, path: str = None) -> str:
58
60
  """台账路径:显式 → MDCG_LINK_FILE → <root>/_link.jsonl。"""
59
61
  return path or os.environ.get(LEDGER_ENV) or os.path.join(root, LEDGER_NAME)
60
62
 
61
63
 
64
+ # 生效条件:传入 root(path 为真值则以其为准,否则由 ledger_file 的回落决定落点)时返回 ledger_file(root, path) 结果拼接 FAIL_SUFFIX。
62
65
  def fail_log_file(root: str, path: str = None) -> str:
63
66
  """降级留痕路径(台账写不进时的「本该建的边」)。"""
64
67
  return ledger_file(root, path) + FAIL_SUFFIX
65
68
 
66
69
 
70
+ # 生效条件:value 为 None 返回 [];否则 list/tuple/set 逐个元素、其他类型视作单元素,元素经 str(x).strip() 后非空且未出现过才保留(重复只留首次)。
67
71
  def as_list(value) -> list:
68
72
  """把单值 / 序列统一成去重、去空白的字符串列表。"""
69
73
  if value is None:
@@ -77,6 +81,7 @@ def as_list(value) -> list:
77
81
  return out
78
82
 
79
83
 
84
+ # 生效条件:rel 为假值(None/空串)或 str(rel).strip().lower() 后为空白的串(如 " ")时 r 回落 default(默认常量 DEFAULT_RELATION);r 不在 RELATIONS 内(含回落后的 default 本身非法)即抛 ProvenanceError,否则返回该小写串。
80
85
  def normalize_relation(rel, default: str = DEFAULT_RELATION) -> str:
81
86
  """严格校验关系名;非法抛 `ProvenanceError`(显式 API 用)。"""
82
87
  r = str(rel or "").strip().lower()
@@ -87,6 +92,7 @@ def normalize_relation(rel, default: str = DEFAULT_RELATION) -> str:
87
92
  return r
88
93
 
89
94
 
95
+ # 生效条件:normalize_relation(rel, default) 抛 ProvenanceError(含 rel 为假值/仅空白且 default 非法时)则原样返回 default;否则返回该调用的返回值。
90
96
  def coerce_relation(rel, default: str = DEFAULT_RELATION) -> str:
91
97
  """宽松兜底:非法关系名回退默认值(**写路径用,保证永不阻断写入**)。"""
92
98
  try:
@@ -95,6 +101,7 @@ def coerce_relation(rel, default: str = DEFAULT_RELATION) -> str:
95
101
  return default
96
102
 
97
103
 
104
+ # 生效条件:child 或 parent 为假值或仅空白使 str(x or "").strip() 为空、或二者 strip 后相等(自环)时返回 None;否则返回含 schema/t/child/parent/rel/batch/actor 的 dict,note 为真值时才附上并截断到 200 字符。
98
105
  def make_edge(child, parent, *, relation=DEFAULT_RELATION, batch=None,
99
106
  actor="system", note=None, t=None):
100
107
  """构造一条派生边;自环 / 空端点返回 None(**不产生无意义边**)。"""
@@ -110,6 +117,7 @@ def make_edge(child, parent, *, relation=DEFAULT_RELATION, batch=None,
110
117
  return edge
111
118
 
112
119
 
120
+ # 生效条件:对 as_list(parents) 的每个父项调用 make_edge,只保留返回非 None 的边,全部被丢弃时返回空列表。
113
121
  def edges_for(child, parents, **kw) -> list:
114
122
  """`(child, [parents]) → [edge]`:空端点 / 自环自动丢弃。"""
115
123
  out = []
@@ -124,6 +132,7 @@ def edges_for(child, parents, **kw) -> list:
124
132
  # 写:台账追加(record 为写路径唯一入口,永不抛)
125
133
  # --------------------------------------------------------------------------
126
134
 
135
+ # 生效条件:edges 为 None/空/全为假元素时返回 0;否则在 FileLock(p, timeout=_LOCK_TIMEOUT) 内逐条 append_jsonl,抛 OSError 时转抛 ProvenanceError,成功返回 len(edges)。
127
136
  def append(root: str, edges, *, path: str = None) -> int:
128
137
  """台账追加(加锁串行,防 Windows 并发交错丢边)。IO 失败抛 `ProvenanceError`。"""
129
138
  edges = [e for e in (edges or []) if e]
@@ -140,6 +149,7 @@ def append(root: str, edges, *, path: str = None) -> int:
140
149
  return len(edges)
141
150
 
142
151
 
152
+ # 生效条件:给定 root/path/child/parents/relation/reason/code 即恒返回 {'ok': False, 'added': 0, 'edges': [], 'degraded': True, 'degrade_code': code, 'reason': reason},其中写 fail_log_file 台账的异常被 except Exception 吞掉。
143
153
  def _degrade(root: str, path, child, parents, relation, reason,
144
154
  code: str) -> dict:
145
155
  """降级留痕:把「本该建的边」记进 .fail 台账(自身也 best-effort)。"""
@@ -154,6 +164,7 @@ def _degrade(root: str, path, child, parents, relation, reason,
154
164
  "degrade_code": code, "reason": reason}
155
165
 
156
166
 
167
+ # 生效条件:edges_for 抛异常时经 _degrade(code='bad_edge') 返回;edges 为空时返回 {'ok': True, 'added': 0, 'edges': [], 'reason': 'no_parents'};append 抛 ProvenanceError 或其他异常时经 _degrade(code='ledger_io') 返回;其余返回 {'ok': True, 'added': n, 'edges': edges, 'ledger': …},恒不向外抛。
157
168
  def record(root: str, child, parents, *, relation=DEFAULT_RELATION,
158
169
  batch=None, actor="system", note=None, path: str = None) -> dict:
159
170
  """写路径建链入口:**永不抛**(G8 硬约束:建链失败不得阻断节点写入)。
@@ -185,6 +196,7 @@ def record(root: str, child, parents, *, relation=DEFAULT_RELATION,
185
196
  # 读:台账 / 索引 / 悬空巡检
186
197
  # --------------------------------------------------------------------------
187
198
 
199
+ # 生效条件:遍历 read_jsonl(ledger_file(root, path)),仅收录 isinstance(r, dict) 且 r.get("child") 与 r.get("parent") 均为真值(键缺失或值为假即丢弃)的记录。
188
200
  def load(root: str, *, path: str = None) -> list:
189
201
  """读台账(跳过坏行;只取有端点的记录)。"""
190
202
  out = []
@@ -194,6 +206,7 @@ def load(root: str, *, path: str = None) -> list:
194
206
  return out
195
207
 
196
208
 
209
+ # 生效条件:rows 中每行按 (r.get("child"), r.get("parent"), r.get("rel")) 三元组判重,仅首次出现的行保留,按原顺序返回去重列表。
197
210
  def _dedupe(rows):
198
211
  out, seen = [], set()
199
212
  for r in rows:
@@ -205,6 +218,7 @@ def _dedupe(rows):
205
218
  return out
206
219
 
207
220
 
221
+ # 生效条件:child/parent/relation/batch 各为真值时才做对应等值过滤(假值或 None 不过滤),去重后 limit 为真值时返回 out[:int(limit)],limit 为 None 或 0 时返回全部 out。
208
222
  def edges(root: str, *, child=None, parent=None, relation=None, batch=None,
209
223
  limit: int = None, path: str = None) -> list:
210
224
  """按端点 / 关系 / 批次过滤台账边(只读,去重,保持写入顺序)。"""
@@ -223,6 +237,7 @@ def edges(root: str, *, child=None, parent=None, relation=None, batch=None,
223
237
  return out[:int(limit)] if limit else out
224
238
 
225
239
 
240
+ # 生效条件:以 (getattr(cg, "index", None) or {}).get("nodes") or {} 遍历(缺失时视作空);prefix 为真值时仅保留 str(nid).startswith(prefix) 的节点,输出经 _dedupe 去重。
226
241
  def index_edges(cg, *, prefix: str = None) -> list:
227
242
  """从**索引快照**恢复派生边(零读文件)——台账丢失/未重建时的只读兜底。"""
228
243
  nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
@@ -238,11 +253,13 @@ def index_edges(cg, *, prefix: str = None) -> list:
238
253
  return _dedupe(out)
239
254
 
240
255
 
256
+ # 生效条件:给定 cg 即返回 _dedupe(load(cg.root, path=path) + index_edges(cg)),台账记录在前、按边去重。
241
257
  def all_edges(cg, *, path: str = None) -> list:
242
258
  """台账 ∪ 索引声明(台账优先,按边去重)。"""
243
259
  return _dedupe(load(cg.root, path=path) + index_edges(cg))
244
260
 
245
261
 
262
+ # 生效条件:include_index 为真值时取 all_edges(cg, path=path)、为假时取 load(cg.root, path=path),对 child/parent 不在 cg 索引节点键集合中的边记为 dangling 并列出 missing;返回 ok=not dangling,dangling 仅取 int(limit)(默认 20)项,不写盘。
246
263
  def check(cg, *, path: str = None, limit: int = 20,
247
264
  include_index: bool = True) -> dict:
248
265
  """只读巡检:检出**悬空派生边**(端点不在索引内)。**不删边、不写盘**。
@@ -273,6 +290,7 @@ def check(cg, *, path: str = None, limit: int = 20,
273
290
  "note": "只读巡检:悬空边仅检出并报告,不自动删除(关系事实由人处置)"}
274
291
 
275
292
 
293
+ # 生效条件:apply 为假值(默认 False)时只返回 dry_run 报表(written=0、sample=es[:5]);apply 为真值时把 index_edges(cg) 的边写入 ledger_file(cg.root, path) 并返回 written=len(es)。
276
294
  def rebuild_ledger(cg, *, apply: bool = False, path: str = None) -> dict:
277
295
  """按 frontmatter 重建台账——**只重放已声明的边,不发明任何边**。
278
296
 
@@ -292,6 +310,7 @@ def rebuild_ledger(cg, *, apply: bool = False, path: str = None) -> dict:
292
310
  "written": len(es), "ledger": p}
293
311
 
294
312
 
313
+ # 生效条件:check(cg, path=path, limit=3) 成功时返回 edges/dangling/ledger/exists/sample(悬空边 child->parent);该调用抛任何异常时被 except Exception 吞掉并返回 {}。
295
314
  def summary(cg, *, path: str = None) -> dict:
296
315
  """轻量摘要(只读;失败不抛,避免拖垮 health_os / 常驻循环)。"""
297
316
  try:
@@ -304,6 +323,7 @@ def summary(cg, *, path: str = None) -> dict:
304
323
  return {}
305
324
 
306
325
 
326
+ # 生效条件:root 为真值时 ledger 字段取 ledger_file(root),否则取常量 LEDGER_NAME;其余自描述字段(SCHEMA、RELATIONS、DEFAULT_RELATION、FM_FIELD/FM_REL_FIELD 等)恒定返回。
307
327
  def catalog(root: str = None) -> dict:
308
328
  """自描述(供 MCP catalog / 人工核对)。"""
309
329
  return {
@@ -319,3 +339,245 @@ def catalog(root: str = None) -> dict:
319
339
  "distinct_from": ("links.py = 跨节点信任 P_trust(_links.json);"
320
340
  "本层 = 节点派生关系(_link.jsonl)"),
321
341
  }
342
+
343
+
344
+ # --------------------------------------------------------------------------
345
+ # 读:三元组反查原语(阶段二 4.2 · find_entity_contexts 式)
346
+ # --------------------------------------------------------------------------
347
+ # 术语映射(**同一件事,勿新造第二套字段**):三元组 `subject / predicate /
348
+ # object` 在本层就是派生边的 `child / rel / parent`——即节点写入时声明的
349
+ # `derived_from` 关系。不另建 subject/predicate/object 参数字面量,理由有二:
350
+ # ① `cg` 工具面是**扁平 schema**,`subject` 已被 `identity`(`subject:<id>`
351
+ # 主体语义)与 `link.evidence`(证据主体)占用,同名异义会把两处口径搅在一起;
352
+ # ② `edges()` 已是唯一谓词载体(child/parent/relation/batch 四键),反查只是它的
353
+ # **只读超集**——另造一套参数必然分叉。
354
+ # 与 `edges()` 的三处**有意**差异(不是漂移):
355
+ # · 时间轴缺省 `observed`(边只有记录时刻 `t`,见 FIND_DEFAULT_AXIS);
356
+ # · 默认排序 `desc`(按 `t` 新→旧)且 `limit` 缺省 50、上限 500(分页原语,
357
+ # 不给「静默全量倾倒」);
358
+ # · 加 `aggregation`(分页前全集分桶)与 `expand_nodes`(端点摘要,索引级零读盘)。
359
+
360
+ #: 排序方向(封闭枚举,拒收未知名——与 `trust.TIME_OPERATORS` 同风格)
361
+ ORDERINGS = ("desc", "asc")
362
+ #: 聚合维度(封闭枚举):按谓词 / 对象端 / 主体端分桶
363
+ AGGREGATIONS = ("by_relation", "by_parent", "by_child")
364
+ #: 聚合维度 → 边字段(谓词在边上叫 `rel`;聚合名沿用三元组术语命名)
365
+ _AGG_FIELD = {"by_relation": "rel", "by_parent": "parent", "by_child": "child"}
366
+ #: 反查缺省时间轴:派生边只有一个时刻字段 `t`(写入时刻,观察轴),
367
+ #: **效力轴字段根本不存在**。与 `mdcg.search` 缺省 `effective` **有意不同**:
368
+ #: 那里 `effective_from/until` 是可选声明(多数节点没写),沿用 fail-open 不会
369
+ #: 出错;这里若缺省 `effective`,则「给了时间条件却恒不过滤」——把静默 no-op
370
+ #: 当成了「没有匹配」,属无法复算的错答。
371
+ FIND_DEFAULT_AXIS = "observed"
372
+ DEFAULT_FIND_LIMIT = 50
373
+ MAX_FIND_LIMIT = 500
374
+ #: `expand_nodes=True` 时透出的索引字段白名单(只读索引快照,**零读节点文件**)
375
+ EXPAND_FIELDS = ("layer", "tags", "importance", "writer", "session",
376
+ "derived_from", "derived_relation", "derived_batch",
377
+ "temporal", "time_window", "condition_space")
378
+
379
+
380
+ # 生效条件:value 为 None 或 str(value).strip() 为空时返回 "desc";小写后命中 ORDERINGS 返回该值;否则抛 ProvenanceError。
381
+ def _ordering_of(value) -> str:
382
+ """排序方向归一 → `"desc"` / `"asc"`;未知 → `ProvenanceError`(fail-closed)。"""
383
+ if value is None or not str(value).strip():
384
+ return "desc"
385
+ v = str(value).strip().lower()
386
+ if v not in ORDERINGS:
387
+ raise ProvenanceError(f"未知 ordering {value!r}(允许:{ORDERINGS})")
388
+ return v
389
+
390
+
391
+ # 生效条件:value 为 None 或 str(value).strip() 为空时返回 None(= 不聚合);小写后命中 AGGREGATIONS 返回该值;否则抛 ProvenanceError。
392
+ def _aggregation_of(value):
393
+ """聚合维度归一 → `None` / `AGGREGATIONS` 之一;未知 → `ProvenanceError`。"""
394
+ if value is None or not str(value).strip():
395
+ return None
396
+ v = str(value).strip().lower()
397
+ if v not in AGGREGATIONS:
398
+ raise ProvenanceError(f"未知 aggregation {value!r}(允许:{AGGREGATIONS})")
399
+ return v
400
+
401
+
402
+ # 生效条件:axis 为 "observed" 且 edge.get("t") 可经 trust.parse_time 解析时返回 (t, t, False);axis 非 observed 或 t 不可解析/缺失时返回 (None, None, True)。
403
+ def _edge_window(edge, axis: str):
404
+ """边的轴窗口 → `(start, end, missing)`(与 `trust.time_window_of` **同形**)。
405
+
406
+ observed 轴:`t`(写入时刻)→ `(t, t, False)`;`t` 缺失(`index_edges` 兜底边
407
+ 不带时间)→ `(None, None, True)`。其余轴一律 `(None, None, True)`——边没有效力轴
408
+ 声明可读,如实报「不可判定」,由**轴策略**处置(observed fail-closed 剔除并计入
409
+ `axis_missing`;effective fail-open 保留),不在这里悄悄换轴。
410
+ """
411
+ if str(axis) == "observed":
412
+ t = _trust.parse_time((edge or {}).get("t"))
413
+ if t is None:
414
+ return None, None, True
415
+ return t, t, False
416
+ return None, None, True
417
+
418
+
419
+ # 生效条件:start_operator 与 end_operator 均为 None 时返回 "overlap",否则返回 "endpoint"。
420
+ def _mode_of(start_operator, end_operator) -> str:
421
+ """时间过滤模式(与 `trust.window_match` 的显式分叉口径同源,不另立判据)。"""
422
+ return "endpoint" if (start_operator is not None or end_operator is not None) \
423
+ else "overlap"
424
+
425
+
426
+ # 生效条件:nid 不在 (cg.index or {}).get("nodes") or {} 的 dict 条目中(含 cg.index 缺失、条目非 dict)时返回 {'id': nid, 'present': False};否则返回 {'id','present':True} 并附 EXPAND_FIELDS 中值非 None 的字段。
427
+ def _node_digest(cg, nid) -> dict:
428
+ """端点摘要(只读索引快照,**零读节点文件**);端点缺失 → `present=False`。"""
429
+ nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
430
+ e = nodes.get(nid)
431
+ if not isinstance(e, dict):
432
+ return {"id": nid, "present": False}
433
+ d = {"id": nid, "present": True}
434
+ for k in EXPAND_FIELDS:
435
+ if e.get(k) is not None:
436
+ d[k] = e[k]
437
+ return d
438
+
439
+
440
+ # 生效条件:child/parent/relation/batch 各为真值(str(x).strip() 非空)时归一为过滤条件,relation 经 normalize_relation 非法即抛 ProvenanceError;ordering/aggregation 经 _ordering_of/_aggregation_of 非法即抛;offset 为负或 limit<=0 或 limit>MAX_FIND_LIMIT 即抛;时间五参经 trust.check_time_args 校验,why 非空即抛 ProvenanceError;返回 {'ok': True, 'readonly': True, 'op': 'edges', 'triple', 'total', 'matched', 'returned', 'offset', 'limit', 'ordering', 'aggregation', 'edges', 'aggregates', 'nodes_expanded', 'time_filter', 'ledger', 'distinct_from', 'note'},其中 total=谓词过滤后条数、matched=时间过滤后条数(dropped+matched==total)、edges=按 t 排序后 offset:offset+limit 切片(expand_nodes 时每边附 child_node/parent_node 摘要)、aggregates 为分页前全集分桶(未请求为 None)。
441
+ def find_edges(cg, *, child=None, parent=None, relation=None, batch=None,
442
+ start_time=None, end_time=None, start_operator=None,
443
+ end_operator=None, time_axis=None, ordering=None, offset=0,
444
+ limit=None, aggregation=None, expand_nodes: bool = False,
445
+ path: str = None) -> dict:
446
+ """三元组反查(阶段二 4.2):按**任意端 / 谓词 / 时间**反查派生边(只读)。
447
+
448
+ `subject/predicate/object` ≡ `child/rel/parent`(见本节术语映射注释)。
449
+
450
+ 谓词(`child` / `parent` / `relation` / `batch`)与 `edges()` **同源同义**:
451
+ 给了就等值过滤、不给就不过滤。时间条件走 `trust.check_time_args`
452
+ (**与检索共用的唯一校验点**,本层不另写一套),比较语义由
453
+ `trust.window_match` 提供(不给 operator = 区间重叠;给 operator = 端点比较)。
454
+
455
+ fail-closed 清单(宁可报错,不静默降级):
456
+ · `relation` 非 `RELATIONS`(经 `normalize_relation`);
457
+ · `ordering` / `aggregation` 非各自枚举;
458
+ · `offset < 0`;`limit <= 0` 或 `> MAX_FIND_LIMIT`(**不把 0/负数当「全部」**);
459
+ · 时间五参非法(未知轴 / 未知算子 / 给了算子缺时间 / start > end)。
460
+
461
+ 分页与聚合的次序是刻意的:**聚合基于分页前全集**(`matched`),
462
+ 否则「先切页再聚合」会给出随 offset 漂移的分桶——不可复算。
463
+ 审计块 `time_filter.dropped + matched == total` 由本函数保证。
464
+ """
465
+ # ---- 谓词归一(空/空白 = 不约束,与 edges() 同口径) ------------------
466
+ c_f = str(child).strip() if child is not None and str(child).strip() else None
467
+ p_f = str(parent).strip() if parent is not None and str(parent).strip() else None
468
+ b_f = str(batch).strip() if batch is not None and str(batch).strip() else None
469
+ r_f = normalize_relation(relation) if (relation is not None
470
+ and str(relation).strip()) else None
471
+ # ---- 排序 / 分页 / 聚合 入参校验 --------------------------------------
472
+ ord_v = _ordering_of(ordering)
473
+ agg_v = _aggregation_of(aggregation)
474
+ try:
475
+ off = int(offset or 0)
476
+ except (TypeError, ValueError) as exc:
477
+ raise ProvenanceError(f"offset 非法:{offset!r}") from exc
478
+ if off < 0:
479
+ raise ProvenanceError(f"offset 不能为负:{off}")
480
+ lim = DEFAULT_FIND_LIMIT if limit is None else int(limit)
481
+ if lim <= 0:
482
+ raise ProvenanceError(f"limit 必须为正整数(0/负数不当「全部」):{limit!r}")
483
+ if lim > MAX_FIND_LIMIT:
484
+ raise ProvenanceError(f"limit 超上限 {MAX_FIND_LIMIT}:{lim}")
485
+ # ---- 时间算子:复用唯一校验点,缺省轴按本层语义补 observed -----------
486
+ enabled, axis0, why = _trust.check_time_args(
487
+ start_time=start_time, end_time=end_time,
488
+ start_operator=start_operator, end_operator=end_operator,
489
+ time_axis=time_axis)
490
+ if why:
491
+ raise ProvenanceError(why)
492
+ axis = FIND_DEFAULT_AXIS if time_axis is None else axis0
493
+ q_s = _trust.parse_time(start_time) if enabled else None
494
+ q_e = _trust.parse_time(end_time) if enabled else None
495
+
496
+ rows = all_edges(cg, path=path)
497
+ cand = []
498
+ for e in rows:
499
+ if c_f and e.get("child") != c_f:
500
+ continue
501
+ if p_f and e.get("parent") != p_f:
502
+ continue
503
+ if r_f and e.get("rel") != r_f:
504
+ continue
505
+ if b_f and e.get("batch") != b_f:
506
+ continue
507
+ cand.append(e)
508
+ total = len(cand)
509
+
510
+ # ---- 时间过滤(候选层:与 trust.filter_by_time 同策略) ---------------
511
+ dropped = missing = 0
512
+ kept = []
513
+ for e in cand:
514
+ if not enabled:
515
+ kept.append(e)
516
+ continue
517
+ cs, ce, miss = _edge_window(e, axis)
518
+ if miss:
519
+ if axis == "observed": # 观察轴 fail-closed
520
+ dropped += 1
521
+ missing += 1
522
+ continue
523
+ kept.append(e) # 效力轴 fail-open(无效力声明可读)
524
+ continue
525
+ if _trust.window_match(cs, ce, q_s, q_e, start_operator, end_operator):
526
+ kept.append(e)
527
+ else:
528
+ dropped += 1
529
+ if dropped + len(kept) != total: # 审计不变式(可复算)
530
+ raise ProvenanceError(
531
+ f"审计不变式破缺:dropped({dropped}) + kept({len(kept)}) != total({total})")
532
+
533
+ # ---- 排序(t 缺失按 0 计,确定性次级键防抖) --------------------------
534
+ kept.sort(key=lambda e: (float(e.get("t") or 0.0),
535
+ str(e.get("child") or ""),
536
+ str(e.get("parent") or "")),
537
+ reverse=(ord_v == "desc"))
538
+ page = kept[off:off + lim]
539
+ if expand_nodes:
540
+ page = [dict(e) for e in page]
541
+ for e in page:
542
+ e["child_node"] = _node_digest(cg, e.get("child"))
543
+ e["parent_node"] = _node_digest(cg, e.get("parent"))
544
+
545
+ # ---- 聚合(分页前全集;无分页漂移) ----------------------------------
546
+ aggregates = None
547
+ if agg_v:
548
+ field = _AGG_FIELD[agg_v]
549
+ buckets = {}
550
+ for e in kept:
551
+ k = str(e.get(field) or "")
552
+ b = buckets.get(k)
553
+ if b is None:
554
+ b = buckets[k] = {"key": k, "count": 0, "t_min": None,
555
+ "t_max": None, "sample": []}
556
+ b["count"] += 1
557
+ t = e.get("t")
558
+ if t is not None:
559
+ t = float(t)
560
+ b["t_min"] = t if b["t_min"] is None else min(b["t_min"], t)
561
+ b["t_max"] = t if b["t_max"] is None else max(b["t_max"], t)
562
+ if len(b["sample"]) < 3:
563
+ b["sample"].append(f"{e.get('child')}->{e.get('parent')}"
564
+ f"({e.get('rel')})")
565
+ aggregates = sorted(buckets.values(), key=lambda b: (-b["count"], b["key"]))
566
+
567
+ return {"ok": True, "readonly": True, "op": "edges",
568
+ "triple": {"child": c_f, "relation": r_f, "parent": p_f, "batch": b_f},
569
+ "total": total, "matched": len(kept), "returned": len(page),
570
+ "offset": off, "limit": lim, "ordering": ord_v,
571
+ "aggregation": agg_v, "aggregates": aggregates,
572
+ "nodes_expanded": bool(expand_nodes),
573
+ "edges": page,
574
+ "time_filter": _trust.time_filter_meta(
575
+ axis=axis, mode=_mode_of(start_operator, end_operator),
576
+ start=start_time, end=end_time,
577
+ start_operator=start_operator, end_operator=end_operator,
578
+ dropped=dropped, axis_missing=missing, applied=bool(enabled)),
579
+ "ledger": ledger_file(cg.root, path),
580
+ "distinct_from": "links.py = 跨节点信任 P_trust(_links.json)",
581
+ "note": ("只读:台账 ∪ 索引声明(台账优先,按边去重);"
582
+ "聚合基于分页前全集;边时刻字段为 t(观察轴),"
583
+ "无效力轴字段——真实时间条件请用缺省 time_axis=observed")}