@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
package/md_cg/refindex.py CHANGED
@@ -42,6 +42,7 @@ MAX_CHECK = 2000 # 巡检节点上限(超出报 truncated,不静默
42
42
  STATUSES = ("ok", "stale", "dangling", "unresolved", "error")
43
43
 
44
44
 
45
+ # 生效条件:给定 fp 时返回 os.path.abspath(fp or '')(fp 为空/None 则返回当前目录的绝对路径),作为水位键以绝对路径保证不同 root 下同名文件不互相覆盖。
45
46
  def _src_key(fp: str) -> str:
46
47
  """水位的键 = 源文件绝对路径。
47
48
 
@@ -51,6 +52,7 @@ def _src_key(fp: str) -> str:
51
52
  return os.path.abspath(fp or "")
52
53
 
53
54
 
55
+ # 生效条件:无 required 形参,任何调用都返回 round(time.time(), 1),把时间戳压到 1 位小数以稳定 `_refindex.json` 字节数。
54
56
  def _now() -> float:
55
57
  """时间戳压到 1 位小数:让 `_refindex.json` 字节数稳定(重跑不涨),
56
58
  同时保留足够的「多久以前」信息(float 的最短 repr 保证小数位固定为 1)。"""
@@ -61,6 +63,7 @@ def _now() -> float:
61
63
  # 提取器注册表(统一调度:调用方只说 kind,不说「用哪个模块」)
62
64
  # --------------------------------------------------------------------------
63
65
 
66
+ # 生效条件:kind == 'code_ref' 返回 codeindex、kind == 'doc_ref' 返回 docindex,其他 kind 抛 ValueError(提示支持 REF_KEYS)。
64
67
  def _mod(kind: str):
65
68
  from . import codeindex, docindex
66
69
  if kind == "code_ref":
@@ -70,6 +73,7 @@ def _mod(kind: str):
70
73
  raise ValueError(f"未知 ref kind:{kind!r}(支持 {REF_KEYS})")
71
74
 
72
75
 
76
+ # 生效条件:无 required 形参,调用即返回 {'code_ref': {'suffixes': tuple(codeindex.SUFFIX)}, 'doc_ref': {'suffixes': tuple(docindex.SUFFIX)}}。
73
77
  def registry() -> dict:
74
78
  """后缀 → kind 的注册表(code / doc 各一份提取器)。"""
75
79
  from . import codeindex, docindex
@@ -79,6 +83,7 @@ def registry() -> dict:
79
83
  }
80
84
 
81
85
 
86
+ # 生效条件:path 的小写后缀在 codeindex.EXTRACTORS 中返回 'code_ref',在 docindex.SUFFIX 中返回 'doc_ref',无后缀或均不匹配返回 ''。
82
87
  def kind_of_path(path: str) -> str:
83
88
  """按后缀判 kind;无提取器返回 ''(由调用方决定是报错还是跳过)。"""
84
89
  from . import codeindex, docindex
@@ -92,6 +97,7 @@ def kind_of_path(path: str) -> str:
92
97
  return ""
93
98
 
94
99
 
100
+ # 生效条件:source 为待提取文本,kind 非空或 path 后缀能推出 kind 时返回 _mod(k).extract(source, path),推不出 kind 时抛 ValueError。
95
101
  def extract(source: str, path: str = "", kind: str = ""):
96
102
  """统一提取入口:按 kind(或从 path 推断)分发到对应 extractor。"""
97
103
  k = kind or kind_of_path(path)
@@ -101,20 +107,24 @@ def extract(source: str, path: str = "", kind: str = ""):
101
107
  return _mod(k).extract(source, path)
102
108
 
103
109
 
110
+ # 生效条件:kind 为 'code_ref'/'doc_ref' 时返回 _mod(kind).node_id(item),其他 kind 由 _mod 抛 ValueError。
104
111
  def node_id_of(item: dict, kind: str) -> str:
105
112
  return _mod(kind).node_id(item)
106
113
 
107
114
 
115
+ # 生效条件:kind 为 'code_ref'/'doc_ref' 时返回 _mod(kind).render(item),其他 kind 由 _mod 抛 ValueError。
108
116
  def render_of(item: dict, kind: str) -> str:
109
117
  return _mod(kind).render(item)
110
118
 
111
119
 
120
+ # 生效条件:node 的 frontmatter 中 REF_KEYS 命中且值为非空 dict 时返回 {'ref': ref, 'ref_kind': kind},否则返回 {'ref': None, 'ref_kind': ''}。
112
121
  def ref_fields(node) -> dict:
113
122
  """节点 → 检索结果要带的两字段(读侧只加字段,不改召回逻辑)。"""
114
123
  kind, ref = ref_of(node)
115
124
  return {"ref": ref, "ref_kind": kind} if ref else {"ref": None, "ref_kind": ""}
116
125
 
117
126
 
127
+ # 生效条件:node 的 frontmatter 按 REF_KEYS 顺序取到第一个非空 dict 时返回 (k, r),否则返回 ('', None)。
118
128
  def ref_of(node) -> tuple:
119
129
  """从节点 frontmatter 取 ref:返回 (kind, ref) 或 ('', None)。"""
120
130
  fm = (node or {}).get("frontmatter") or {}
@@ -129,14 +139,17 @@ def ref_of(node) -> tuple:
129
139
  # 索引水位(_refindex.json):增量 + 截断留痕
130
140
  # --------------------------------------------------------------------------
131
141
 
142
+ # 生效条件:以 root 为必填实参构造,实例化即置 self.root=root、self.path=os.path.join(root, LEDGER_FILE)、self._d=None;
132
143
  class Ledger:
133
144
  """`<root>/_refindex.json`:每个源文件的 (size, mtime) 水位 + 节点区间。"""
134
145
 
146
+ # 生效条件:当传入 root 时,self.root 取该 root,self.path 为 os.path.join(root, LEDGER_FILE),self._d 置为 None;
135
147
  def __init__(self, root: str):
136
148
  self.root = root
137
149
  self.path = os.path.join(root, LEDGER_FILE)
138
150
  self._d = None
139
151
 
152
+ # 生效条件:当 self._d is not None 时直接返回 self._d;否则读取 self.path 的 JSON,仅当 obj 是 dict 且 obj.get("schema") == SCHEMA 且 obj.get("files") 是 dict 时用 obj,否则(含 OSError/ValueError、结构不符)回落为 {"schema": SCHEMA, "updated_at": 0.0, "files": {}} 并缓存返回;
140
153
  def load(self) -> dict:
141
154
  if self._d is not None:
142
155
  return self._d
@@ -152,6 +165,7 @@ class Ledger:
152
165
  self._d = d or {"schema": SCHEMA, "updated_at": 0.0, "files": {}}
153
166
  return self._d
154
167
 
168
+ # 生效条件:传入 rel、fp 时,若 self.load()["files"].get(_src_key(fp)) 缺失或为假值、或 os.stat(fp) 抛 OSError、或条目 e.get("size") != st.st_size,则返回 False;否则返回 abs(float(e.get("mtime") or 0.0) - st.st_mtime) < 1e-6(mtime 缺失或假值时按 0.0);
155
169
  def is_fresh(self, rel: str, fp: str) -> bool:
156
170
  """源文件自上次索引后未变(size + mtime 双等)→ 可跳过不重切。"""
157
171
  e = self.load()["files"].get(_src_key(fp))
@@ -165,6 +179,7 @@ class Ledger:
165
179
  return False
166
180
  return abs(float(e.get("mtime") or 0.0) - st.st_mtime) < 1e-6
167
181
 
182
+ # 生效条件:当 rel、fp、kind、nodes 传入且 os.stat(fp) 成功时,向 self.load()["files"][_src_key(fp)] 写条目,其中 root 为 root if root else os.path.dirname(key)、path 为 rel、kind 为 kind、size/mtime 取 st、nodes 为每项 n.get("id")/n.get("lineno")/n.get("end")/n.get("hash");os.stat(fp) 抛 OSError 时不写入;
168
183
  def record(self, rel: str, fp: str, kind: str, nodes,
169
184
  root: str = None) -> None:
170
185
  """记一个源文件的水位(节点区间用于判 stale)。
@@ -190,9 +205,11 @@ class Ledger:
190
205
  ],
191
206
  }
192
207
 
208
+ # 生效条件:当传入 fp 时,self.load()["files"].pop(_src_key(fp), None),即删除对应键(不存在也静默);
193
209
  def drop(self, fp: str) -> None:
194
210
  self.load()["files"].pop(_src_key(fp), None)
195
211
 
212
+ # 生效条件:当传入 root、kind、seen 时,对 self.load()["files"] 中满足 os.path.abspath(e.get("root") or "") == os.path.abspath(root) 且 e.get("kind") == kind 且键 k 不在 seen 的条目删除,返回删除数量;
196
213
  def reconcile(self, root: str, kind: str, seen) -> int:
197
214
  """一次**完整**索引后对账:本 (root, kind) 下没被扫到的旧条目剪掉。
198
215
 
@@ -209,6 +226,7 @@ class Ledger:
209
226
  files.pop(k, None)
210
227
  return len(dead)
211
228
 
229
+ # 生效条件:对 load()["files"] 中「条目 root(为假值时用 os.path.dirname(键) 兜底)不是目录」的条目逐一 pop 并返回删除条数,无匹配时返回 0。
212
230
  def prune(self) -> int:
213
231
  """剪掉「源大域已不存在」的条目(整个目录被搬走/删除)。
214
232
 
@@ -222,6 +240,7 @@ class Ledger:
222
240
  files.pop(k, None)
223
241
  return len(dead)
224
242
 
243
+ # 生效条件:当 kind、root、files、indexed、truncated 传入时,self.load()["last_index"] 被设为含 ts=_now()、kind、root、files、indexed、truncated=bool(truncated)、truncated_reason=reason or "" 的字典;reason 为假值(默认 ""/None)时 truncated_reason 回落 "";
225
244
  def note_index(self, *, kind: str, root: str, files: int, indexed: int,
226
245
  truncated: bool, reason: str = "") -> None:
227
246
  """记「最近一次索引」结果——截断在这里留痕,供 diagnose 看见。"""
@@ -231,12 +250,14 @@ class Ledger:
231
250
  "truncated_reason": reason or "",
232
251
  }
233
252
 
253
+ # 生效条件:无参数调用即生效,取 self.load() 结果把 updated_at 置为 _now(),再以 atomic_write 把 json.dumps(..., ensure_ascii=False, indent=1, sort_keys=True) 写入 self.path,无返回值。
234
254
  def save(self) -> None:
235
255
  d = self.load()
236
256
  d["updated_at"] = _now()
237
257
  atomic_write(self.path, json.dumps(d, ensure_ascii=False,
238
258
  indent=1, sort_keys=True))
239
259
 
260
+ # 生效条件:无参数调用即生效,返回含 path、schema、load()["files"] 条目数、nodes 总数(各条目 nodes 列表长度之和)、updated_at、exists=os.path.isfile(self.path) 的 out;age_s 在 updated_at 为假值(0.0)时为 None,否则为 max(0.0, time.time()-up);仅当 load() 的 last_index 为 dict 时才并入 out["last_index"]。
240
261
  def summary(self) -> dict:
241
262
  d = self.load()
242
263
  files = d.get("files") or {}
@@ -260,6 +281,7 @@ class Ledger:
260
281
  # 统一 index_dir:调度 + 水位 + 落盘(供 op=index_code / op=index_doc / heal 共用)
261
282
  # --------------------------------------------------------------------------
262
283
 
284
+ # 生效条件:root 为源大域根、kind 为 'code_ref'/'doc_ref' 时经 _mod(kind) 调度底层 index_dir 并返回 (items, errors, stats);ledger 非空时逐文件 record,incremental 为真时跳过 ledger.is_fresh 为真的文件,且 stats 未截断时执行 reconcile。
263
285
  def index_dir(root: str, *, kind: str, patterns=None, max_files: int = 500,
264
286
  max_items: int = 2000, incremental: bool = False,
265
287
  ledger: "Ledger" = None, skip_dirs=None):
@@ -279,12 +301,14 @@ def index_dir(root: str, *, kind: str, patterns=None, max_files: int = 500,
279
301
  seen = set() # 本次真正走过的源文件(用于对账)
280
302
  if ledger is not None:
281
303
  if incremental:
304
+ # 生效条件:当 rel、fp 传入时,ok = ledger.is_fresh(rel, fp);若 ok 为真则将 _src_key(fp) 加入 seen 并返回 ok,若 ok 为假则直接返回 False;
282
305
  def fresh(rel, fp): # noqa: E306
283
306
  ok = ledger.is_fresh(rel, fp)
284
307
  if ok:
285
308
  seen.add(_src_key(fp))
286
309
  return ok
287
310
 
311
+ # 生效条件:当 rel、fp、got 传入时,将 _src_key(fp) 加入 seen,并以 root=root 调用 ledger.record(rel, fp, kind, [{"id": node_id_of(it, kind), "lineno": it.get("lineno"), "end": it.get("end"), "hash": it.get("hash")} for it in got]);
288
312
  def on_file(rel, fp, got): # noqa: E306
289
313
  seen.add(_src_key(fp))
290
314
  ledger.record(rel, fp, kind,
@@ -308,6 +332,7 @@ def index_dir(root: str, *, kind: str, patterns=None, max_files: int = 500,
308
332
  return items, errors, stats
309
333
 
310
334
 
335
+ # 生效条件:it['path'] 非空时返回其首段 path.split('/')[0] 作为 domain 键,path 为空返回 'orphan'。
311
336
  def _domain_of(it: dict) -> str:
312
337
  """条目 → 路由域键(供 `tags` 的 `domain:` 显式声明)。
313
338
 
@@ -323,6 +348,7 @@ def _domain_of(it: dict) -> str:
323
348
  return path.split("/")[0] or "orphan"
324
349
 
325
350
 
351
+ # 生效条件:kind == 'code_ref' 时按 codeindex.node_id/render 写入 cg(tags 含 'code'、code_ref=_code_ref(it, root)),kind == 'doc_ref' 时按 docindex 写入(tags 含 'doc'、doc_ref=_doc_ref(it, root)、密级取自 docindex.sensitivity_for(it['path'], sensitivity)),其他 kind 抛 ValueError,返回 (ids, sens)。
326
352
  def add_items(cg, items, *, kind: str, root: str, layer=None, sensitivity=None,
327
353
  layer_of=None):
328
354
  """把索引条目写进认知图(code / doc 的落盘细节收在这里,唯一实现)。
@@ -370,15 +396,21 @@ def add_items(cg, items, *, kind: str, root: str, layer=None, sensitivity=None,
370
396
  return ids, sens
371
397
 
372
398
 
373
- def _code_ref(it: dict, root: str) -> dict:
399
+ # 生效条件:把入参 root 原样写入返回 dict 的 'root',path/name/kind/lineno/end/lang/hash 按 it.get 取值(缺省 None),precise 取 bool(it.get('precise', True)),render_version 取传入值(传入 None 时延迟 import codeindex 取 codeindex.RENDER_VERSION,保证与 render 契约**同源**、无第二处硬编码)。
400
+ def _code_ref(it: dict, root: str, render_version=None) -> dict:
401
+ if render_version is None: # 直接调用点的兜底:与 render 产物同源
402
+ from . import codeindex
403
+ render_version = codeindex.RENDER_VERSION
374
404
  return {
375
405
  "path": it.get("path"), "name": it.get("name"),
376
406
  "kind": it.get("kind"), "lineno": it.get("lineno"), "end": it.get("end"),
377
407
  "lang": it.get("lang"), "precise": bool(it.get("precise", True)),
378
408
  "hash": it.get("hash"), "root": root,
409
+ "render_version": render_version,
379
410
  }
380
411
 
381
412
 
413
+ # 生效条件:把入参 root 原样写入返回 dict 的 'root',path/heading/heading_path/level/lineno/end/anchor/hash/lang 按 it.get 取值(缺省 None),precise 取 bool(it.get('precise', True))。
382
414
  def _doc_ref(it: dict, root: str) -> dict:
383
415
  return {
384
416
  "path": it.get("path"), "heading": it.get("heading"),
@@ -393,6 +425,7 @@ def _doc_ref(it: dict, root: str) -> dict:
393
425
  # 回读(唯一实现:op=ref 与 check_refs 共用)
394
426
  # --------------------------------------------------------------------------
395
427
 
428
+ # 生效条件:传入 ref 为假值(如 None/{})时按 {} 处理,rel 取 ref.get("path") or "";root 与 ref.get("root") 均为假值时返回含 ref/path/status:"unresolved"/ok:False/error:"ref 未记录 root..." 的 out;否则用 root or ref.get("root") 与 rel 拼 fp,os.path.isfile(fp) 为假时返回 status:"dangling"、stale:True,读取抛 OSError/UnicodeDecodeError 时返回 status:"error";读取成功时 lineno 取 int(ref.get("lineno") or 1)(假值回落 1)、end 取 int(ref.get("end") or lineno)(假值回落 lineno),ref.get("hash") 为 None 时 match=None、ok=True、status:"ok",ref.get("hash") 为真值且等于 region_hash 时 ok=True/status:"ok"、不等时 ok=False/status:"stale",ref.get("hash") 为假值但非 None(如 ""/0/False)时 ok=False/status:"stale";with_text 为真时 out["text"] 取 lines[max(0,lineno-1):max(max(0,lineno-1),end)] 的 join;
396
429
  def probe_ref(ref: dict, *, root: str = None, with_text: bool = False) -> dict:
397
430
  """只读探测单个 ref 的状态(不回读整篇,除非 with_text)。"""
398
431
  from . import codeindex
@@ -433,6 +466,7 @@ def probe_ref(ref: dict, *, root: str = None, with_text: bool = False) -> dict:
433
466
  return out
434
467
 
435
468
 
469
+ # 生效条件:ref 经 probe_ref(root=root, with_text=True) 后 status 为 'ok'/'stale' 时返回 ok=True 及 text/total_lines/hash/hash_match/stale/precise,status 为 'unresolved'/'error'/'dangling' 时返回 ok=False 与 error。
436
470
  def read_ref(ref: dict, *, root: str = None, ref_kind: str = "ref") -> dict:
437
471
  """按 ref 回读源区间——`op=ref` 与 `check_refs` 的唯一实现。"""
438
472
  p = probe_ref(ref, root=root, with_text=True)
@@ -452,6 +486,7 @@ def read_ref(ref: dict, *, root: str = None, ref_kind: str = "ref") -> dict:
452
486
  }
453
487
 
454
488
 
489
+ # 生效条件:cg 的 index['nodes'] 非空时汇总 stale/dangling/unresolved/errors 并返回 ok =(无 stale 且无 dangling);only_tagged 为真时只探测 tags 含 'code'/'doc' 的节点,ledger 非空时先走 (size, mtime) 快路径。
455
490
  def check_refs(cg, *, ledger: "Ledger" = None, max_nodes: int = MAX_CHECK,
456
491
  only_tagged: bool = True) -> dict:
457
492
  """漂移 / 悬空巡检(只读、不抛)。
@@ -468,6 +503,7 @@ def check_refs(cg, *, ledger: "Ledger" = None, max_nodes: int = MAX_CHECK,
468
503
  stale, dangling, unresolved, errors = [], [], [], []
469
504
  covered = set()
470
505
 
506
+ # 生效条件:当 nid、ref、kind、rel 传入时,p = probe_ref(ref) 后按 p["status"] 分派:为 "dangling" 时把含 node_id/ref_kind/path/lineno/end/error 的 row 加入 dangling,为 "stale" 时补 hash_expected/hash 加入 stale,为 "unresolved" 时加入 unresolved,为 "error" 时加入 errors;其他状态不加入;
471
507
  def _probe_one(nid, ref, kind, rel):
472
508
  p = probe_ref(ref)
473
509
  row = {"node_id": nid, "ref_kind": kind, "path": rel,
@@ -509,6 +545,7 @@ def check_refs(cg, *, ledger: "Ledger" = None, max_nodes: int = MAX_CHECK,
509
545
  kind, rel)
510
546
 
511
547
  # 回退:ledger 未覆盖的索引节点
548
+ # 生效条件:当 nid 传入时,若 only_tagged 为假值立即返回 True;否则取 (nodes.get(nid) or {}).get("tags") or [],仅当其中存在 "code" 或 "doc" 返回 True,否则返回 False;
512
549
  def _candidate(nid):
513
550
  if not only_tagged:
514
551
  return True
@@ -556,11 +593,13 @@ def check_refs(cg, *, ledger: "Ledger" = None, max_nodes: int = MAX_CHECK,
556
593
  # `op=ref action=prune`(悬空)使用,避免两处各写一套口径。
557
594
  # --------------------------------------------------------------------------
558
595
 
596
+ # 生效条件:返回 os.path.normcase(os.path.abspath(str(p or ''))),即 p 为 None/空串时返回当前目录的归一绝对路径。
559
597
  def _norm_root(p) -> str:
560
598
  """root 归一:同一目录的大小写/分隔符差异不得影响「同一大域」判定。"""
561
599
  return os.path.normcase(os.path.abspath(str(p or "")))
562
600
 
563
601
 
602
+ # 生效条件:a 与 b 都非空且 _norm_root(a) == _norm_root(b) 时返回 True,否则(含 TypeError/ValueError)返回 False。
564
603
  def _same_root(a, b) -> bool:
565
604
  try:
566
605
  return bool(a) and bool(b) and _norm_root(a) == _norm_root(b)
@@ -568,10 +607,12 @@ def _same_root(a, b) -> bool:
568
607
  return False
569
608
 
570
609
 
610
+ # 生效条件:返回 str(p or '').replace('\\', '/').lstrip('./'),即 p 为 None/空串时返回 ''。
571
611
  def _norm_rel(p) -> str:
572
612
  return str(p or "").replace("\\", "/").lstrip("./")
573
613
 
574
614
 
615
+ # 生效条件:cg 具备可调用的 forget 方法时对 plan 中每个 nid 调 cg.forget(nid, why),返回 (成功 id 列表, 被拦下/失败的 {node_id, error} 列表);cg 无 forget 时返回 ([], plan 中每 nid 一条错误)。
575
616
  def _forget_many(cg, plan, why: str) -> tuple:
576
617
  """逐条软删(进 trash/、写删除清单、可 restore);受保护节点拦下不删。
577
618
 
@@ -598,6 +639,7 @@ def _forget_many(cg, plan, why: str) -> tuple:
598
639
  return done, blocked
599
640
 
600
641
 
642
+ # 生效条件:cg 具备可调用的 _unstage 时对 ghosts 逐个调用并收集成功 id(单条异常跳过),cg 无该能力时返回 [](不假装成功)。
601
643
  def _drop_ghosts(cg, ghosts) -> list:
602
644
  """摘除幽灵条目的索引记录(节点文件已不存在,没有可软删的实体)。
603
645
 
@@ -617,6 +659,7 @@ def _drop_ghosts(cg, ghosts) -> list:
617
659
  return out
618
660
 
619
661
 
662
+ # 生效条件:items 中同 kind 的节点若其 ref['path'] 命中本次 items 的文件、id 不在本次产出内且 ref['root'] 与入参 root 同一(_same_root),则列入清退计划;dry_run 为真只返回计划,否则经 _forget_many 软删;items 为空时返回 count 0。
620
663
  def prune_orphans(cg, *, kind: str, root: str, items, dry_run: bool = False,
621
664
  reason: str = "") -> dict:
622
665
  """清退「同 root + 同 path,但已不在本次产出里」的**过期代**节点。
@@ -675,6 +718,7 @@ def prune_orphans(cg, *, kind: str, root: str, items, dry_run: bool = False,
675
718
  "skipped_protected": blocked[:20], "reason": why}
676
719
 
677
720
 
721
+ # 生效条件:cg 中带 'code'/'doc' 标签且 ref 带 root 的节点经 probe_ref 判为 'dangling' 时列入清退计划;only_roots 为空时另收集 cg.root 下取不到对应节点 .md 的幽灵条目经 _drop_ghosts 摘除;dry_run 为真只返回计划。
678
722
  def prune_dangling(cg, *, only_roots=None, dry_run: bool = False,
679
723
  max_nodes: int = MAX_CHECK, reason: str = "") -> dict:
680
724
  """清退**悬空**节点:ref 指向的源文件已删除,回读必然失败。
@@ -745,6 +789,7 @@ def prune_dangling(cg, *, only_roots=None, dry_run: bool = False,
745
789
  "skipped_protected": blocked[:20], "reason": why}
746
790
 
747
791
 
792
+ # 生效条件:cg 节点按 ref['root'] 与 kind 分组后逐组以 index_dir(incremental=False, ledger=ledger) 重切、再以 add_items(layer_of=原 layer) 重建,返回 {'ok','roots','groups','indexed','errors','truncated'};only_roots 非 None 时只处理其中列出的 root。
748
793
  def rebuild(cg, *, ledger: "Ledger" = None, only_roots=None, max_files: int = 500,
749
794
  max_items: int = 2000) -> dict:
750
795
  """按 ref 记录的 root 重建索引(sustain.heal 的修复动作)。
@@ -786,4 +831,4 @@ def rebuild(cg, *, ledger: "Ledger" = None, only_roots=None, max_files: int = 50
786
831
  out["errors"].append(f"{root} [{kind}]:{exc}")
787
832
  if ledger is not None:
788
833
  ledger.save()
789
- return out
834
+ return out
package/md_cg/refine.py CHANGED
@@ -94,25 +94,30 @@ SPEC = {
94
94
 
95
95
  # ---- 通用工具 -------------------------------------------------------------
96
96
 
97
+ # 生效条件:x 为 str 时返回 MdCGOS(x),否则原样返回 x;
97
98
  def _as_cg(x):
98
99
  return MdCGOS(x) if isinstance(x, str) else x
99
100
 
100
101
 
102
+ # 生效条件:cg 具 "root" 属性时以该值为根、否则以 str(cg) 为根,与模块常量 REFINE_LOG 拼接返回;
101
103
  def _log_path(cg) -> str:
102
104
  root = cg.root if hasattr(cg, "root") else str(cg)
103
105
  return os.path.join(root, REFINE_LOG)
104
106
 
105
107
 
108
+ # 生效条件:read_jsonl(_log_path(cg)) 返回假值时按空列表处理,仅保留 action 字段等于 "refine" 的记录;
106
109
  def _read_log(cg) -> list:
107
110
  return [r for r in (read_jsonl(_log_path(cg)) or [])
108
111
  if r.get("action") == "refine"]
109
112
 
110
113
 
114
+ # 生效条件:cg 无 index 或 index["nodes"] 为假值时以空字典查找;nodes.get(nid) 为假值时返回空字典 {};
111
115
  def _entry(cg, nid):
112
116
  nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
113
117
  return nodes.get(nid) or {}
114
118
 
115
119
 
120
+ # 生效条件:取 _entry(cg, nid) 的 path(假值回落空串)的父目录 basename;该 basename 为假值时返回 "(root)";
116
121
  def _family(cg, nid):
117
122
  """家族 = 节点所在 `cond_*` 目录名(无则 `(root)`)。"""
118
123
  p = str(_entry(cg, nid).get("path") or "").replace("\\", "/")
@@ -120,11 +125,13 @@ def _family(cg, nid):
120
125
  return d or "(root)"
121
126
 
122
127
 
128
+ # 生效条件:对传入的 parts 逐项 str 后以 "|" 连接并 UTF-8 编码,返回 sha1 的 hexdigest;
123
129
  def _sha(*parts) -> str:
124
130
  h = hashlib.sha1("|".join(str(p) for p in parts).encode("utf-8"))
125
131
  return h.hexdigest()
126
132
 
127
133
 
134
+ # 生效条件:遍历 cg.index 的 nodes,仅收 str(nid) 以 prefix 开头且条目 protected 为假值的 nid 进 ids(排序后返回),protected 为真值的计入 protected 计数;
128
135
  def _pool(cg, prefix) -> tuple:
129
136
  """抽检池:id 以 prefix 开头、非受保护节点(与 induce 的池口径一致)。"""
130
137
  nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
@@ -140,6 +147,7 @@ def _pool(cg, prefix) -> tuple:
140
147
  return ids, protected
141
148
 
142
149
 
150
+ # 生效条件:cg 的 id 池经 prefix(假值回落 PREFIX_DEFAULT)过滤后,n 为 None 用 SAMPLE_N、否则 int(n),seed 假值回落 SAMPLE_SEED;pool 为空或 n<=0 时返回 ([], meta),否则按家族分层最大余数分配并在层内按 _sha(seed, id) 升序取前 k 个返回 (picked, meta);
143
151
  def sample_ids(cg, n=None, seed=None, prefix=None) -> tuple:
144
152
  """确定性分层抽样(家族=层,家族内按 sha1(seed|id) 排序)。
145
153
 
@@ -180,6 +188,7 @@ def sample_ids(cg, n=None, seed=None, prefix=None) -> tuple:
180
188
  return picked, meta
181
189
 
182
190
 
191
+ # 生效条件:对 cg 中 nid 对应节点(cg.get(nid) 为假值时用空字典)生成字段;content 长度大于 EXCERPT_MAX 时 body 截断且 body_truncated 为 True,否则 body 为全 content;
183
192
  def _item(cg, nid) -> dict:
184
193
  node = cg.get(nid) or {}
185
194
  fm = node.get("frontmatter") or {}
@@ -203,6 +212,7 @@ def _item(cg, nid) -> dict:
203
212
 
204
213
  # ---- 预演:与 induce 同源的聚类口径 ---------------------------------------
205
214
 
215
+ # 生效条件:pool 中能取到 grams 的 nid 进入 cache;按 keys 顺序贪心,与当前 a 的 _jaccard >= float(min_jaccard) 且未分配的后续 b 并入组,组大小达到 int(min_cluster) 才成组,返回 (cache, keys, groups);
206
216
  def _cluster(cg, pool, min_jaccard, min_cluster) -> tuple:
207
217
  """贪心聚类(与 `consolidate.induce_memories` 同源)。返回 (cache, keys, groups)。"""
208
218
  from . import subgraph
@@ -228,6 +238,7 @@ def _cluster(cg, pool, min_jaccard, min_cluster) -> tuple:
228
238
  return cache, keys, groups
229
239
 
230
240
 
241
+ # 生效条件:terms 为空时对每个 t 返回 df[t]=0;否则对 keys 中每个 k 的 cache[k]["pos"] 统计各 term 出现次数,返回 df;
231
242
  def _term_df(cache, keys, terms) -> dict:
232
243
  """词面在抽检样本内的文档频率(不含语义推断)。"""
233
244
  df = {t: 0 for t in terms}
@@ -241,6 +252,7 @@ def _term_df(cache, keys, terms) -> dict:
241
252
  return df
242
253
 
243
254
 
255
+ # 生效条件:x 转为 cg;min_jaccard 为 None 取 MIN_JACCARD、否则 float(min_jaccard),min_cluster 为 None 取 MIN_CLUSTER、否则 int(min_cluster);ids 为真值时 pool 为显式 ids 去重排序且 meta 标记 explicit_ids=True,否则 pool/meta 来自 sample_ids(cg, n=n, seed=seed, prefix=prefix);对 pool 聚类后,require_conditions 为真且某组 common 为空时跳过该组并累计 skipped_no_cond;返回含 candidates 与 sample_adequacy 的只读预演字典;
244
256
  def preview(x, ids=None, n=None, seed=None, prefix=None,
245
257
  min_jaccard=None, min_cluster=None, require_conditions=True) -> dict:
246
258
  """提炼预演(只读):抽样 → 同源聚类 → 共性条件 + 来源 + 泛化度标记。"""
@@ -316,6 +328,7 @@ def preview(x, ids=None, n=None, seed=None, prefix=None,
316
328
 
317
329
  # ---- 工单 ---------------------------------------------------------------
318
330
 
331
+ # 生效条件:x 转为 cg;prefix 假值回落 PREFIX_DEFAULT,seed 假值回落 SAMPLE_SEED;ids 为真值时 sample 为显式 ids 去重排序且 real_pool=len(sample)、skipped=0、families=0,否则 sample/meta 来自 sample_ids(cg, n=n, seed=seed, prefix=prefix) 并取 meta["pool"]/meta["skipped_protected"]/meta["families"];items 为 sample 的 _item,preview 以 sample 为显式 ids 调用,返回只读工单;
319
332
  def plan(x, ids=None, n=None, seed=None, prefix=None,
320
333
  min_jaccard=None, min_cluster=None) -> dict:
321
334
  """抽检工单(只读):确定性样本 + 原文摘录 + 字段缺口 + 口径声明 + 预演。"""
@@ -366,6 +379,7 @@ def plan(x, ids=None, n=None, seed=None, prefix=None,
366
379
  }
367
380
 
368
381
 
382
+ # 生效条件:x 转为 cg;target 为 None 取 CALIBRATE_TARGET、否则 int(target),cap 为 None 取 CALIBRATE_CAP、否则 int(cap),初始 n=min(SAMPLE_N if n0 is None else int(n0), cap);循环以 n 调 preview(cg, n=n, seed=seed, prefix=prefix, min_jaccard=min_jaccard, min_cluster=min_cluster) 并记录 step,直到 pv["clusters"] >= target 则 chosen=step 并 break,或 n >= min(cap, pool or cap) 则 break;chosen 为 None 时 blocked=True 并按 steps 汇总 recommend,否则 recommend 基于 chosen;返回只读校准报告;
369
383
  def calibrate(x, target=None, cap=None, n0=None, seed=None, prefix=None,
370
384
  min_jaccard=None, min_cluster=None) -> dict:
371
385
  """样本量校准(只读):20 条起步有界爬坡,直到产出 `target` 个候选或触顶。
@@ -425,6 +439,7 @@ def calibrate(x, target=None, cap=None, n0=None, seed=None, prefix=None,
425
439
 
426
440
  # ---- 留痕与扩批闸门 ------------------------------------------------------
427
441
 
442
+ # 生效条件:candidates 中取 concept_id 为真值的 id 列表;verdicts 中为 dict 且 concept_id 非空且首次出现的条目计入 reviewed 并累加其中 faithful/added_info 的真值计数;ids 非空时按 reviewed < len(ids) → awaiting_human_review、added 非零 → added_info_detected、faithful_rate < GATE_MIN_PASS_RATE → faithful_rate_below_gate、否则 ok 且 expand_allowed=True;ids 为空时 reason="no_candidates" 且 expand_allowed=False;
428
443
  def _verdict_stats(verdicts, candidates) -> dict:
429
444
  ids = [c.get("concept_id") for c in candidates if c.get("concept_id")]
430
445
  seen, faithful, added = set(), 0, 0
@@ -457,6 +472,7 @@ def _verdict_stats(verdicts, candidates) -> dict:
457
472
  "expand_allowed": expand, "reason": reason}
458
473
 
459
474
 
475
+ # 生效条件:x 转为 cg;以 ids/n/seed/prefix/min_jaccard/min_cluster 调 plan 得到 p,batch 假值(含 None/空串)回落当前时间字符串;从 p["preview"]["candidates"] 与 verdicts 经 _verdict_stats 得 stats;把含 batch/actor/prefix/seed/sample/verdicts/gate/note 的 record 追加写入 _log_path(cg),返回 rep;
460
476
  def apply(x, batch=None, actor=None, verdicts=None, note=None,
461
477
  ids=None, n=None, seed=None, prefix=None,
462
478
  min_jaccard=None, min_cluster=None) -> dict:
@@ -493,6 +509,7 @@ def apply(x, batch=None, actor=None, verdicts=None, note=None,
493
509
  return rep
494
510
 
495
511
 
512
+ # 生效条件:x 转为 cg;batch 为真值时只保留 _read_log(cg) 中 batch 字段相等的记录,过滤后为空时返回 batches=0、expand_allowed=False、reason="no_batch";否则取最后一条记录,以其 verdicts 与 candidates(假值转空列表)经 _verdict_stats 复算并返回 expand_allowed/reason 等;
496
513
  def gate(x, batch=None) -> dict:
497
514
  """扩批闸门:读留痕复算通过率(不写盘)。"""
498
515
  cg = _as_cg(x)
@@ -517,6 +534,7 @@ def gate(x, batch=None) -> dict:
517
534
  "闸门未放行,禁止扩大批次;不得盲跑全量。")}
518
535
 
519
536
 
537
+ # 生效条件:batch 为真值时只保留该批次记录,total 先记为过滤后条数;limit 非 None 且 >=0 时以 recs[-int(limit):] 截尾(limit=0 因 [-0:] 等价 [0:] 仍返回全部记录),limit 为 None 或负数时不截断;
520
538
  def history(x, limit=100, batch=None) -> dict:
521
539
  cg = _as_cg(x)
522
540
  recs = _read_log(cg)
@@ -531,6 +549,7 @@ def history(x, limit=100, batch=None) -> dict:
531
549
 
532
550
  # ---- CLI(真实库抽检/预演用;MCP 侧走 maintain action) -------------------
533
551
 
552
+ # 生效条件:解析 argv(None 时由 argparse 取 sys.argv)得到 root/n/seed/prefix/min-jaccard/min-cluster/report 等参数;a.calibrate 为真时调 calibrate 并打印 target/cap/pool/blocked/sample_adequacy/recommend 及每步摘要;否则 a.preview 为真选 preview、为假选 plan,调用后打印 thin 摘要与候选并返回 0;
534
553
  def _main(argv=None) -> int:
535
554
  import argparse
536
555
  import json
@@ -583,4 +602,4 @@ def _main(argv=None) -> int:
583
602
 
584
603
  if __name__ == "__main__":
585
604
  import sys
586
- sys.exit(_main())
605
+ sys.exit(_main())
@@ -0,0 +1,89 @@
1
+ # -*- coding: utf-8 -*-
2
+ """md_cg · 角色化读取视图(第四阶段 6.1,计划文档 §4)
3
+
4
+ 【为什么】同一认知图对不同消费角色应呈现不同侧面:主代理要「结论与
5
+ 修正历史」,验证端要「判据面与环境陷阱」,回执审计要「命令与预期输出」。
6
+ 三者的候选资格不同,但都建立在同一节点集上——视图是读取面的资格过滤,
7
+ 不是数据的第二副本(与超边的派生视图正交:超边是图结构增补,视图是
8
+ 候选资格过滤;计划 §3)。
9
+
10
+ 【设计】
11
+ - ROLE_VIEWS:声明式规则表(唯一真源),每个 view 声明四维资格:
12
+ content_kinds —— content_kind 必须落在集合内(None=不限)
13
+ layers —— layer 必须落在集合内(None=不限)
14
+ roles —— 非 None 时作为正向白名单(role 必须命中,
15
+ 短路返回;receipt 用)
16
+ include_work —— False 时 role 落在工作角色(tool-output/
17
+ command/edit)即一票否决(与候选池默认
18
+ include_work=False 同口径)
19
+ - matches(fm, view):单点谓词。fm 接受扁平 entry 或 frontmatter
20
+ 字典(两者对 role/layer/content_kind 三键同名)。
21
+ - 非法 view 一律 ValueError(fail-closed,不静默回落)——合法性
22
+ 判定单点在本模块;与节点缺字段 fail-open(缺 content_kind 按
23
+ "text" 对待)是两回事,后者是读取面惯例。
24
+
25
+ 【边界】
26
+ - 零内部依赖(不 import 本包其它模块,防循环导入)。规则表内的
27
+ 工作角色白名单与 mdcos.WORK_ROLES 字面一致,一致性由
28
+ test_role_views 守卫断言钉住(防两处漂移)。
29
+ - 初版规则是类型启发,bench_role_views.py 的对照校准是法定修正
30
+ 通道(改规则必须复跑对照,计划 §7.3)。
31
+ """
32
+
33
+ # 规则表内的工作角色白名单,字面与 mdcos.WORK_ROLES 保持一致
34
+ # (一致性由 test_role_views 守卫断言钉住,防两处漂移)。
35
+ _WORK_ROLES = ("tool-output", "command", "edit")
36
+
37
+ ROLE_VIEWS = {
38
+ # 主代理视图:结论与修正历史(正文/工作结论落在 knowledge 层)
39
+ "main": {
40
+ "content_kinds": ("text", "work_done"),
41
+ "layers": ("knowledge",),
42
+ "roles": None,
43
+ "include_work": False,
44
+ },
45
+ # 验证端视图:判据面与环境陷阱(marks 条目 + 知识正文,含 contextual)
46
+ "verifier": {
47
+ "content_kinds": ("ccg_marks", "text"),
48
+ "layers": ("knowledge", "contextual"),
49
+ "roles": None,
50
+ "include_work": False,
51
+ },
52
+ # 回执审计视图:恰是默认候选池剔除的工作角色节点(include_work
53
+ # 默认 False 的补集),roles 白名单短路命中即保留
54
+ "receipt": {
55
+ "content_kinds": None,
56
+ "layers": None,
57
+ "roles": _WORK_ROLES,
58
+ "include_work": True,
59
+ },
60
+ }
61
+
62
+
63
+ def views():
64
+ """合法视图名(排序稳定,供错误信息与文档引用)。"""
65
+ return tuple(sorted(ROLE_VIEWS))
66
+
67
+
68
+ def matches(fm, view):
69
+ """节点元数据是否满足视图资格(单点谓词)。
70
+
71
+ fm:扁平 entry 或 frontmatter 字典(role/layer/content_kind 同名键)。
72
+ view:ROLE_VIEWS 键之一;非法值 ValueError(fail-closed)。
73
+ """
74
+ spec = ROLE_VIEWS.get(view)
75
+ if spec is None:
76
+ raise ValueError(
77
+ "非法 view:%r(合法三值:%s)" % (view, ", ".join(views())))
78
+ if spec["roles"] is not None: # 正向白名单视图(receipt)
79
+ return fm.get("role") in spec["roles"]
80
+ ck = fm.get("content_kind")
81
+ if spec["content_kinds"] is not None \
82
+ and (ck or "text") not in spec["content_kinds"]:
83
+ return False
84
+ if spec["layers"] is not None \
85
+ and fm.get("layer") not in spec["layers"]:
86
+ return False
87
+ if not spec["include_work"] and fm.get("role") in _WORK_ROLES:
88
+ return False
89
+ return True