@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/twophase.py CHANGED
@@ -56,10 +56,12 @@ R_MISMATCH = "content_mismatch" # 落了别的正文 → interrupted(
56
56
  R_UNREADABLE = "node_unreadable" # 读节点失败 → interrupted(不猜测)
57
57
 
58
58
 
59
+ # 生效条件:给定 cg 且其 root 属性可参与 os.path.join 时,恒返回 os.path.join(cg.root, LOG_FILE),无分支与早退。
59
60
  def log_path(cg) -> str:
60
61
  return os.path.join(cg.root, LOG_FILE)
61
62
 
62
63
 
64
+ # 生效条件:text 为假值(None/"")时按空串处理,否则用 text 本身,返回去尾换行后的字符串(仅剥 "\n")。
63
65
  def _norm(text: str) -> str:
64
66
  """正文口径归一(**对账正确性的前提**):`nodefile.dumps` 写盘时会给正文补尾
65
67
  换行,`loads` 读回即带它。若 intent 按入参原文、对账按读回内容各算一次指纹,
@@ -69,10 +71,12 @@ def _norm(text: str) -> str:
69
71
  return (text or "").rstrip("\n")
70
72
 
71
73
 
74
+ # 生效条件:给定 cg 与 rec 时,把 rec 以 JSONL 追加写入 log_path(cg),无返回值、无分支。
72
75
  def _append(cg, rec: dict) -> None:
73
76
  append_jsonl(log_path(cg), rec)
74
77
 
75
78
 
79
+ # 生效条件:pr 取自 cg.principal 或(假值时)回落 cg.session——pr 为真字符串时 meta["session"]=pr;否则 pr 非 None 时把 pr.session 记入 session、把 pr.writer 或(假值时)pr.unit 记入 writer;actor 为真时 meta["actor"]=actor,最后返回 meta。
76
80
  def _ident(cg, actor=None) -> dict:
77
81
  """归因字段(尽力而为):principal 缺省时留空,不编造。"""
78
82
  pr = getattr(cg, "principal", None) or getattr(cg, "session", None)
@@ -91,6 +95,7 @@ def _ident(cg, actor=None) -> dict:
91
95
  return meta
92
96
 
93
97
 
98
+ # 生效条件:给定 cg、node_id、content 时生成 iid 与 content_hash(对 _norm(content) 计算),先写 phase="intent" 记录再返回 tok;layer 为真才写入 rec["layer"],actor 与 **meta 分别经 _ident、rec.update 合入。
94
99
  def begin(cg, node_id: str, content: str, layer: str = None,
95
100
  actor: str = None, **meta) -> dict:
96
101
  """记录写入**意图**(先行持久化)→ 返回令牌 tok(供 `commit` 配对)。
@@ -112,6 +117,7 @@ def begin(cg, node_id: str, content: str, layer: str = None,
112
117
  return tok
113
118
 
114
119
 
120
+ # 生效条件:nid 取 node_id,node_id 为假值(None/"")时回落 (tok or {}).get("node_id"),iid 取 (tok or {}).get("iid")(tok 为假值时两项均为 None);status 用实参或默认 STATUS_COMMITTED;tok 为真且 tok.get("content_hash") 为真才写入 content_hash;reason 为真才写入 rec["reason"];追加记录后返回 rec。
115
121
  def commit(cg, tok: dict, status: str = STATUS_COMMITTED, node_id: str = None,
116
122
  reason: str = None, **meta) -> dict:
117
123
  """记录写入**结果**(与 intent 按 iid 配对)。返回结果记录。"""
@@ -131,6 +137,7 @@ def commit(cg, tok: dict, status: str = STATUS_COMMITTED, node_id: str = None,
131
137
  # ----------------------------------------------------------------------
132
138
  # 读侧:盘点 / 对账
133
139
 
140
+ # 生效条件:log_path(cg) 不是文件时直接返回空列表;否则逐行读取,空行跳过,json.loads 抛异常的行跳过,返回成功解析的记录列表。
134
141
  def records(cg) -> list:
135
142
  """读全部账本记录(append-only,坏行跳过不炸)。"""
136
143
  out = []
@@ -149,6 +156,7 @@ def records(cg) -> list:
149
156
  return out
150
157
 
151
158
 
159
+ # 生效条件:遍历 records(cg),记录 r.get("iid") 为假值(缺键或空串)时跳过;否则按 iid 建槽,仅当槽内 intent/outcome 仍为 None 时分别填入首个 phase=="intent" / phase=="outcome" 的记录,返回 pairs。
152
160
  def _pair(cg) -> dict:
153
161
  """把账本配成 {iid: {"intent":…, "outcome":…}}(同时计入无 iid 的脏行)。"""
154
162
  pairs = {}
@@ -164,6 +172,7 @@ def _pair(cg) -> dict:
164
172
  return pairs
165
173
 
166
174
 
175
+ # 生效条件:遍历 _pair(cg) 取有 intent 且 outcome 为 None 的槽(展开 intent 字段)组成 out,按 r.get("t") or 0 升序排序后返回 out[:max(0, int(limit))]——limit 为 0 或负数时切片为空列表,默认值 200 仅在未传参时生效。
167
176
  def pending(cg, limit: int = 200) -> list:
168
177
  """未结清的写入意图(有 intent 无 outcome)——只读,不改盘。
169
178
 
@@ -177,6 +186,7 @@ def pending(cg, limit: int = 200) -> list:
177
186
  return out[:max(0, int(limit))]
178
187
 
179
188
 
189
+ # 生效条件:intent 的 id 为假值或 cg.get(nid) 抛异常时返回 (STATUS_INTERRUPTED, R_UNREADABLE);取回 node 为假值时返回 (STATUS_INTERRUPTED, R_NODE_MISSING);intent 的 content_hash 为真且与 _norm(node.get("content") or "") 的指纹不等时返回 (STATUS_INTERRUPTED, R_MISMATCH);否则返回 (STATUS_COMMITTED, R_NODE_OK)。
180
190
  def _judge(cg, intent: dict):
181
191
  """判定一笔未结清意图的真实结局 → `(status, code)`。**不猜测**:读不到就如实说。"""
182
192
  nid = intent.get("id")
@@ -195,6 +205,7 @@ def _judge(cg, intent: dict):
195
205
  return STATUS_COMMITTED, R_NODE_OK
196
206
 
197
207
 
208
+ # 生效条件:以 limit 调 pending(cg, limit=limit) 得到未结清意图并统计 unpaired/committed/interrupted;apply 为真时对每笔 intent 调 commit 补写 outcome(reconciled=True、actor="twophase:reconcile"),apply 为假值时只报告不写账本,rep["applied"]=bool(apply);limit 为 0 或负数时 pending 返回空、details 为空列表。
198
209
  def reconcile(cg, apply: bool = True, limit: int = 2000) -> dict:
199
210
  """启动/巡检对账:把半途写入**补账或如实标记**。
200
211
 
@@ -218,4 +229,4 @@ def reconcile(cg, apply: bool = True, limit: int = 2000) -> dict:
218
229
  "content_hash": intent.get("content_hash")},
219
230
  status=status, reason=code, reconciled=True,
220
231
  actor="twophase:reconcile")
221
- return rep
232
+ return rep
package/md_cg/units.py CHANGED
@@ -14,7 +14,10 @@ crosscheck 同款 verdicts 通道回填)。理由:使用者的复核是昂
14
14
  蜂巢契约(真源 hive/README.md + hive/hive_mcp/mcp_server.py;**复制契约不 import**,
15
15
  保持 md_cg 对 hive 零依赖,与 ccgc「同款语义就地实现防 import 环」惯例一致):
16
16
  jobs 目录 = $HIVE_JOBS_DIR | <repo>/hive/jobs
17
- serve 判活 = jobs/_serve.json 的 ts(毫秒)距今 < 5s (同 _serve_alive)
17
+ serve 判活 = **三层**:jobs/_serve.json 的 ts(毫秒)距今 < FRESH_S(15s)
18
+ ∧ pid 存活 ∧ 该 pid 是本程序
19
+ (同 hive/serve_start.serve_alive;四路口径由
20
+ hive/test_serve_entry.py 机械守卫)
18
21
  job 目录 = jobs/<job_id>/{spec.json,status.json,result.json,kill}
19
22
  job_id = h<java_ms>_<uuid6> (同 _submit)
20
23
  result.json = {"ok":true,"content":...} | {"ok":false,"error":...}
@@ -45,8 +48,16 @@ SERVE_FILE, SPEC_FILE = "_serve.json", "spec.json"
45
48
  STATUS_FILE, RESULT_FILE, KILL_FILE = "status.json", "result.json", "kill"
46
49
  LOG_NAME = "_units.jsonl"
47
50
  TERMINAL_STATES = ("done", "error", "timeout", "killed")
48
- FRESH_S, DEFAULT_TIMEOUT_S, DEFAULT_POLL_S = 5.0, 120, 1.0
49
- DEFAULT_MAX_TOKENS = 2048
51
+ #: serve 心跳新鲜窗口(秒)。**须与 hive/serve_start.FRESH_S 同值**——本模块
52
+ #: 只持有「判活的第 4 处实现」(复制契约不 import hive),阈值/判据结构若与
53
+ #: 权威漂移,会在通道选择面复现 v13 的「假存活」病类(v14 缺陷 E:
54
+ #: 旧值 5.0 vs 权威 15,serve 崩溃后 ≤5s 窗口内判「存活」→ 选 hive 通道 →
55
+ #: 提交的 job 永远无人处理)。守卫:hive/test_serve_entry.py。
56
+ FRESH_S, DEFAULT_TIMEOUT_S, DEFAULT_POLL_S = 15.0, 120, 1.0
57
+ #: 复核委派的 completion 预算。**必须对齐统一默认(文档:最大输出 200000)**——
58
+ #: 该模型 reasoning 与正文**共享 completion 预算**,小预算会把正文吃光并静默返回空正文:
59
+ #: 实测 2048 → reasoning 2048 / content 空;16384 → 时好时坏;200000 → 正常出裁决。
60
+ DEFAULT_MAX_TOKENS = 200000
50
61
 
51
62
  ENV_JOBS_DIR, ENV_EXE = "HIVE_JOBS_DIR", "HIVE_EXE"
52
63
  ENV_MODEL, ENV_API_KEY = "MDCG_UNIT_MODEL", "HIVE_API_KEY"
@@ -61,16 +72,19 @@ _VERDICT_MAP = {
61
72
  }
62
73
 
63
74
 
75
+ # 生效条件:无入参,恒返回模块 `__file__` 绝对路径上溯两级的目录(md_cg 的上一级),与 cwd 无关。
64
76
  def repo_root() -> str:
65
77
  """仓库根(md_cg 的上一级);不用 cwd——cwd 由宿主决定,不可作判据。"""
66
78
  return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
67
79
 
68
80
 
81
+ # 生效条件:explicit 为真值时 d=explicit;explicit 为空串等假值而 ENV_JOBS_DIR 有真值时 d=该环境变量;两者皆假值时 d=repo_root()/hive/jobs;最终返回 os.path.abspath(d)。
69
82
  def jobs_dir(explicit: str = "") -> str:
70
83
  d = explicit or os.environ.get(ENV_JOBS_DIR) or os.path.join(repo_root(), "hive", "jobs")
71
84
  return os.path.abspath(d)
72
85
 
73
86
 
87
+ # 生效条件:ENV_EXE 有真值时原样返回该值;ENV_EXE 缺失或为空串时返回 repo_root()/hive/target/release/ 下按 os.name=="nt" 取 hive.exe、否则取 hive 的拼接路径。
74
88
  def exe_path() -> str:
75
89
  exe = os.environ.get(ENV_EXE)
76
90
  if exe:
@@ -79,13 +93,81 @@ def exe_path() -> str:
79
93
  "hive.exe" if os.name == "nt" else "hive")
80
94
 
81
95
 
96
+ # 生效条件:explicit 为真值时取 explicit,否则取 ENV_MODEL,两者皆假值时按空串,返回该结果 strip() 后的字符串(可为空串)。
82
97
  def model_name(explicit: str = "") -> str:
83
98
  """复核模型(LLM 委托型执行器的 spec.model 必填)。"""
84
99
  return (explicit or os.environ.get(ENV_MODEL) or "").strip()
85
100
 
86
101
 
102
+ # 生效条件:pid 为 int 且大于 0 时(否则 False),os.name 为 "nt" 时返回 _tasklist_row(pid) 是否非 None,非 "nt" 时 os.kill(pid, 0) 不抛 OSError 返回 True、抛 OSError 返回 False。
103
+ def pid_alive(pid) -> bool:
104
+ """该 pid **号**是否存在(Windows tasklist 精确列比对 / unix `kill -0`)。
105
+
106
+ 只回答「这个号有没有进程」——**不足以判定「serve 还在跑」**(见
107
+ `pid_is_self_program`)。实现与 hive/serve_start.pid_alive 同口径。
108
+ """
109
+ if not isinstance(pid, int) or pid <= 0:
110
+ return False
111
+ if os.name == "nt":
112
+ return _tasklist_row(pid) is not None
113
+ try:
114
+ os.kill(pid, 0)
115
+ return True
116
+ except OSError:
117
+ return False
118
+
119
+
120
+ # 生效条件:对传入 pid 执行 tasklist /FO CSV 后,在其 stdout 中遇到的第一个按 '","' 切分、列数≥2 且第 2 列 strip 再 strip('"') 后等于 str(pid) 的行即返回 [映像名, pid 字符串],无此行或 subprocess.run 抛 OSError 时返回 None。
121
+ def _tasklist_row(pid):
122
+ """Windows:查该 pid 的 tasklist 行 → [映像名, pid 字符串];查不到返回 None。
123
+
124
+ 按列精确比对,**不用子串包含**——子串会让 pid=441 被 4410 命中(假存活)。
125
+ """
126
+ try:
127
+ # 显式 utf-8 + replace:只消费 ASCII 的 pid 列,但**不依赖 locale**——
128
+ # locale 口径与「后代写 UTF-8」不一致时读线程会崩(见 test_subproc_encoding.py)。
129
+ r = subprocess.run(["tasklist", "/FI", f"PID eq {pid}", "/NH", "/FO", "CSV"],
130
+ capture_output=True, text=True,
131
+ encoding="utf-8", errors="replace")
132
+ except OSError:
133
+ return None
134
+ for line in (r.stdout or "").splitlines():
135
+ cols = line.split('","')
136
+ if len(cols) >= 2 and cols[1].strip().strip('"') == str(pid):
137
+ return [cols[0].strip().strip('"'), cols[1].strip().strip('"')]
138
+ return None
139
+
140
+
141
+ # 生效条件:exe_path() 的 basename 小写非空且 pid 为 int 大于 0 时(否则 False),"nt" 下要求 _tasklist_row(pid) 非空且映像名小写等于该 basename,非 "nt" 下要求 /proc/<pid>/cmdline 首个 b"\x00" 前 token 的 basename 小写等于它(读取抛 OSError 则 False)。
142
+ def pid_is_self_program(pid) -> bool:
143
+ """该 pid 是否**就是本程序**(同映像名)——pid 号会被无关进程复用。
144
+
145
+ 与 hive/serve_start.pid_is_self_program 同口径(复制契约)。零依赖边界:
146
+ 拿不到映像名返回 False(宁可放行启动,也不误报「已有 serve 在跑」)。
147
+ """
148
+ want = os.path.basename(exe_path()).lower()
149
+ if not want or not isinstance(pid, int) or pid <= 0:
150
+ return False
151
+ if os.name == "nt":
152
+ row = _tasklist_row(pid)
153
+ return bool(row) and row[0].lower() == want
154
+ try:
155
+ with open(f"/proc/{pid}/cmdline", "rb") as f:
156
+ first = f.read().split(b"\x00")[0]
157
+ except OSError:
158
+ return False
159
+ return os.path.basename(first.decode("utf-8", "replace")).lower() == want
160
+
161
+
162
+ # 生效条件:以 jobs_dir(jobs) 下的 SERVE_FILE 为心跳路径——该路径不被 isfile 命中时返回 exists/alive 均 False 的未启动 reason;命中但 open/json.load 抛 OSError 或 ValueError 时返回不可读 reason;解析成功后按**三层判据**(age < fresh_s ∧ pid_alive(pid) ∧ pid_is_self_program(pid))定 alive,回填 age_s/pid/raw 与逐层明细及失败原因。
87
163
  def serve_state(jobs: str = "", fresh_s: float = FRESH_S) -> dict:
88
- """serve 判活:只认 _serve.json 心跳新鲜度(同 _serve_alive),不试端口/进程名。"""
164
+ """serve 判活:**三层**(心跳新鲜 ∧ pid 存活 ∧ 该 pid 是本程序)。
165
+
166
+ 与 `hive/serve_start.serve_alive` 同口径(同值 FRESH_S=15s、同三层判据)。
167
+ v14 缺陷 E:旧实现只判 ts 新鲜度且阈值 5s——双漂移,serve 崩溃后
168
+ ≤5s 内判「存活」会让 `probe()` 选 hive 通道(提交的 job 永远无人处理),
169
+ 这正是 v13「假存活」病类从守卫面搬到了**通道选择面**。
170
+ """
89
171
  jd = jobs_dir(jobs)
90
172
  p = os.path.join(jd, SERVE_FILE)
91
173
  out = {"jobs_dir": jd, "heartbeat": p, "exists": os.path.isfile(p),
@@ -100,13 +182,28 @@ def serve_state(jobs: str = "", fresh_s: float = FRESH_S) -> dict:
100
182
  out["reason"] = "心跳不可读:%s: %s" % (type(exc).__name__, exc)
101
183
  return out
102
184
  age = time.time() - ((hb.get("ts") or 0) / 1000.0)
103
- out.update({"age_s": round(age, 3), "pid": hb.get("pid"), "raw": hb})
104
- out["alive"] = age < float(fresh_s)
105
- out["reason"] = "" if out["alive"] else "心跳过期 %.1fs(阈值 %ss)——serve 可能已退出" % (
106
- age, fresh_s)
185
+ pid = hb.get("pid")
186
+ fresh_ok = age < float(fresh_s)
187
+ pid_ok = pid_alive(pid)
188
+ ident_ok = pid_is_self_program(pid)
189
+ out.update({"age_s": round(age, 3), "pid": pid, "raw": hb,
190
+ "fresh": bool(fresh_ok), "pid_alive": bool(pid_ok),
191
+ "pid_is_self_program": bool(ident_ok)})
192
+ out["alive"] = bool(fresh_ok and pid_ok and ident_ok)
193
+ if out["alive"]:
194
+ out["reason"] = ""
195
+ elif not fresh_ok:
196
+ out["reason"] = "心跳过期 %.1fs(阈值 %ss)——serve 可能已退出" % (age, fresh_s)
197
+ elif not pid_ok:
198
+ out["reason"] = "心跳新鲜但 pid=%s 已不存在——serve 已退出" % (pid,)
199
+ else:
200
+ out["reason"] = ("心跳新鲜且 pid=%s 存活,但该 pid 不属于本程序"
201
+ "(非 %s)——pid 号被无关进程复用"
202
+ % (pid, os.path.basename(exe_path())))
107
203
  return out
108
204
 
109
205
 
206
+ # 生效条件:jobs 仅经 jobs_dir(jobs) 用于填充指引里的 jobs 路径;reason 为假值时文案首行取“serve 未存活”,否则取 reason 原文;返回模板固定嵌入 exe_path() 的目录与路径、ENV_API_KEY、ENV_MODEL、jd、jd 下的 SERVE_FILE 与 int(FRESH_S)。
110
207
  def setup_hint(jobs: str = "", reason: str = "") -> str:
111
208
  """不可用时给使用者看的**配置指引**(含降级指引)——提示即责任,须可照着做。"""
112
209
  jd = jobs_dir(jobs)
@@ -123,6 +220,7 @@ def setup_hint(jobs: str = "", reason: str = "") -> str:
123
220
  ENV_MODEL, exe_path(), jd, os.path.join(jd, SERVE_FILE), int(FRESH_S)))
124
221
 
125
222
 
223
+ # 生效条件:先取 st=serve_state(jobs, fresh_s) 与 mdl=model_name(model);st["alive"] 为真且 mdl 非空时返回 state=HIVE;否则 allow_degrade 为真时返回 state=SUBAGENT 且 channel 取 channel 或 "harness-subagent";否则返回 state=CONFIGURE、transport=None 并附 setup_hint(jobs, why)。
126
224
  def probe(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
127
225
  allow_degrade: bool = False, channel: str = "") -> dict:
128
226
  """三级能力探测:hive(可派发)→ configure(提示配置)→ subagent(显式降级)。
@@ -147,6 +245,7 @@ def probe(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
147
245
  "hint": setup_hint(jobs, why)}
148
246
 
149
247
 
248
+ # 生效条件:state/transport/model/jobs_dir/serve/channel/hint 全部直接取自 probe(jobs, model, fresh_s, allow_degrade, channel) 的对应键,本函数不另做判活或模型判断,仅在 chain 文案中固定引用 int(FRESH_S) 与 ENV_MODEL。
150
249
  def plan(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
151
250
  allow_degrade: bool = False, channel: str = "") -> dict:
152
251
  """优先级链自描述(供 cg op=ccg action=units 与审计查看)。"""
@@ -155,7 +254,8 @@ def plan(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
155
254
  "jobs_dir": p["jobs_dir"], "serve": p["serve"], "channel": p["channel"],
156
255
  "chain": [
157
256
  {"order": 1, "transport": HIVE, "action": "派发 reflect/verify 单元并等待 result.json",
158
- "when": "jobs/_serve.json 心跳 < %ss 且 %s 已配置" % (int(FRESH_S), ENV_MODEL)},
257
+ "when": ("jobs/_serve.json 心跳 < %ss 且(%s 已配置 **或调用方显式传 model**)"
258
+ % (int(FRESH_S), ENV_MODEL))},
159
259
  {"order": 2, "transport": CONFIGURE, "action": "返回配置指引(不自动拉起、不假装通过)",
160
260
  "when": "蜂巢不可用"},
161
261
  {"order": 3, "transport": SUBAGENT, "action": "返回复核请求包,由 harness 端子代理执行并回填",
@@ -166,11 +266,13 @@ def plan(jobs: str = "", model: str = "", fresh_s: float = FRESH_S,
166
266
 
167
267
  # ---------------------------------------------------------------- 派发与收取
168
268
 
269
+ # 生效条件:无入参,恒返回 "h"+int(time.time()*1000)+"_"+uuid.uuid4().hex 前 6 位组成的字符串。
169
270
  def _job_id() -> str:
170
271
  """同 _submit:serve 侧按 'h' 前缀识别任务目录。"""
171
272
  return "h%d_%s" % (int(time.time() * 1000), uuid.uuid4().hex[:6])
172
273
 
173
274
 
275
+ # 生效条件:cg.root 与 cg.cg.root 都取不到真值时不写、直接返回 None;取到 root 时向 root/LOG_NAME 追加一行 rec(副本,setdefault ts)的 JSON,写入抛 OSError 时被吞掉静默返回 None。
174
276
  def _log(cg, rec: dict) -> None:
175
277
  """留痕 _units.jsonl(对齐 _crosscheck.jsonl / _backfill.jsonl 纪律)。"""
176
278
  root = getattr(cg, "root", None) or getattr(getattr(cg, "cg", None), "root", None)
@@ -185,6 +287,7 @@ def _log(cg, rec: dict) -> None:
185
287
  pass
186
288
 
187
289
 
290
+ # 生效条件:role 不在 ROLES 时返回 ok=False 的未知角色 error;否则 model_name(model) 为空时返回 ok=False 的未配置复核模型;否则 str(prompt or "").strip() 为空时返回 ok=False 的 prompt 为空;否则在 jobs_dir(jobs)/_job_id() 下写 spec.json 与 status.json(context_files 为真、temperature 非 None、extra 为真时才并入 spec),OSError 时返回 ok=False 的写入失败,全部成功返回 ok=True 与 job_id/job_dir/spec/model。
188
291
  def submit(*, prompt: str, role: str = REFLECT, model: str = "", system_prompt: str = "",
189
292
  context_files=None, timeout_s: int = DEFAULT_TIMEOUT_S,
190
293
  max_tokens: int = DEFAULT_MAX_TOKENS, temperature=None, jobs: str = "",
@@ -226,6 +329,7 @@ def submit(*, prompt: str, role: str = REFLECT, model: str = "", system_prompt:
226
329
  "unit_role": role, "model": mdl}
227
330
 
228
331
 
332
+ # 生效条件:jobs_dir(jobs)/str(job_id or "") 不是目录时返回 state=missing、terminal=False 的目录不存在 error;是目录时读 STATUS_FILE(读失败则 status 置 None)并用其 state 覆盖 state/terminal(state 属 TERMINAL_STATES 才 terminal=True);RESULT_FILE 被 isfile 命中则 terminal=True、state=st or "done",解析抛 OSError/ValueError 时提前返回该 error;解析为 dict 时取 ok/content/error/usage/model,且 ok 为真而 content 去空白为空时把 ok 改 False 并写空正文 error,解析为非 dict 时 ok=True 且 content 为原值。
229
333
  def poll(job_id: str, jobs: str = "") -> dict:
230
334
  """读 job 终态视图:**以 result.json 出现为终态主判据**,status.json 仅作辅助。"""
231
335
  d = os.path.join(jobs_dir(jobs), str(job_id or ""))
@@ -260,11 +364,19 @@ def poll(job_id: str, jobs: str = "") -> dict:
260
364
  out["error"] = res.get("error") or out["error"]
261
365
  out["usage"] = res.get("usage")
262
366
  out["model"] = res.get("model")
367
+ # 空正文**不得报成功**:reasoning 与正文共享 completion 预算,预算不足时
368
+ # job 仍自称 ok 但 content 为空 → 若不拦,会被误读成「复核单元无答复」
369
+ # (假死锁)。run() 早已有同等校验,此处补齐,消除两处口径不一致。
370
+ if out["ok"] and not str(out["content"] or "").strip():
371
+ out["ok"] = False
372
+ out["error"] = ("空正文:job 自称 ok 但 content 为空(多为 reasoning 吃尽"
373
+ "完成预算;提高 max_tokens,勿把预算不足当通道不可用)")
263
374
  else:
264
375
  out["ok"], out["content"] = True, res
265
376
  return out
266
377
 
267
378
 
379
+ # 生效条件:循环 poll(job_id, jobs),结果 terminal 为真即补 waited_s 后返回;否则 time.time()-t0 >= float(timeout_s) 时返回 terminal=False、timeout=True 与超时 error;两者皆不满足则 sleep(float(poll_s)) 后重试(timeout_s=0 时首次 poll 非终态即超时返回)。
268
380
  def wait(job_id: str, *, jobs: str = "", timeout_s: float = DEFAULT_TIMEOUT_S,
269
381
  poll_s: float = DEFAULT_POLL_S) -> dict:
270
382
  """阻塞等终态;超时如实返回(不假装成功、不强杀 job)。"""
@@ -282,6 +394,7 @@ def wait(job_id: str, *, jobs: str = "", timeout_s: float = DEFAULT_TIMEOUT_S,
282
394
  time.sleep(float(poll_s))
283
395
 
284
396
 
397
+ # 生效条件:submit(prompt=prompt, role=role, cg=cg, actor=actor, **kw) 的 ok 为假时返回 stage="submit" 的失败 dict(含 **sub);ok 为真时 wait(sub["job_id"], jobs=kw.get("jobs",""), timeout_s=wait_s),补 stage="wait"/job_id/unit_role/model,并把 ok 改为 res 的 ok 与 content 同时为真。
285
398
  def run(*, prompt: str, role: str = REFLECT, cg=None, actor: str = "",
286
399
  wait_s: float = DEFAULT_TIMEOUT_S, **kw) -> dict:
287
400
  """submit + wait 组合(阻塞式复核,MCP action=review 主路径)。"""
@@ -300,6 +413,7 @@ def run(*, prompt: str, role: str = REFLECT, cg=None, actor: str = "",
300
413
  _FENCE_RE = re.compile(r"```(?:json)?\s*(.+?)\s*```", re.S)
301
414
 
302
415
 
416
+ # 生效条件:raw 是 dict/list 时原样返回;否则取 str(raw or "").strip(),为空串(含 0/False/None 等假值)返回 None;否则依次尝试 _FENCE_RE 第 1 组、整段文本、首个 "{" 到末个 "}" 及首个 "[" 到末个 "]" 的切片,返回首个能 json.loads 成 dict 或 list 的候选;全部失败返回 None。
303
417
  def _json_of(raw):
304
418
  """从单元输出抽第一个 JSON 对象/数组(容忍 ```json 围栏与前后噪声)。"""
305
419
  if isinstance(raw, (dict, list)):
@@ -326,6 +440,7 @@ def _json_of(raw):
326
440
  return None
327
441
 
328
442
 
443
+ # 生效条件:data=_json_of(raw) 为 list 时取 data[0](仅当它是 dict,否则 None);非 dict 时返回 verdict=DEFER、parsed=False;dict 时 word 取 verdict/decision/state/result 中首个真值后 strip().lower(),_VERDICT_MAP.get(word) 为 None 时返回 DEFER、parsed=True 并带 corr 与 reason,命中时返回该裁决及 reason、slot_corrections(取自 slot_corrections/corrections,非 dict 则置 {})、checks、raw 前 500 字符。
329
444
  def verdict_of(raw) -> dict:
330
445
  """单元输出 → 裁决四态;解析不出 → DEFER(不猜测、不当通过)。"""
331
446
  data = _json_of(raw)
@@ -347,6 +462,7 @@ def verdict_of(raw) -> dict:
347
462
  "checks": data.get("checks"), "raw": str(raw)[:500], "parsed": True}
348
463
 
349
464
 
465
+ # 生效条件:state==HIVE 时返回 "hive:<role>:<job_id 或 'unknown'>";state==SUBAGENT 时返回 "subagent:<role>:<channel 或 'harness'>";其余 state 返回空串。
350
466
  def transport_name(state: str, role: str, job_id: str = "", channel: str = "") -> str:
351
467
  """裁决来源标识:**结构上不可能等于编译者**,保证 A 裁定「不得自证」不被误伤。"""
352
468
  if state == HIVE:
@@ -356,6 +472,7 @@ def transport_name(state: str, role: str, job_id: str = "", channel: str = "") -
356
472
  return ""
357
473
 
358
474
 
475
+ # 生效条件:unit 为假值时按 {} 处理;verdict=unit.get("verdict") or DEFER,verifier=transport_name(state, role, job_id, channel),evidence 取 unit 的 reason,为空则退回 str(unit["raw"])[:300],job_id 为真时再追加“(job=<job_id>)”;仅当 unit 的 slot_corrections 为真才带该键,仅当 compiled_by 与 verifier 皆非空且相等才置 self_verify=True。
359
476
  def to_attest_args(unit: dict, *, state: str = "", role: str = REFLECT, job_id: str = "",
360
477
  channel: str = "", compiled_by: str = "") -> dict:
361
478
  """单元裁决 → ccgc.attest(...) 入参(纯函数,离线可测)。
@@ -416,6 +533,7 @@ _VERIFY_TPL = """你是独立**验证单元**(verify)。对下面这份 CCG
416
533
  {{"verdict":"accept|drop|defer","reason":"一句话依据","slot_corrections":{{}}}}"""
417
534
 
418
535
 
536
+ # 生效条件:digest is None 时返回 "{}";digest 是 str 时原样返回;否则先试 to_dict/as_dict 可调用方法并 json.dumps 其返回值(抛异常即跳出改走后续分支);是 dict 时 json.dumps(digest);其余取 vars(digest) 中不以 "_" 开头的属性 json.dumps(default=str)。
419
537
  def _digest_of(digest) -> str:
420
538
  if digest is None:
421
539
  return "{}"
@@ -434,6 +552,7 @@ def _digest_of(digest) -> str:
434
552
  ensure_ascii=False, indent=1, default=str)
435
553
 
436
554
 
555
+ # 生效条件:role==VERIFY 选 _VERIFY_TPL,否则选 _REFLECT_TPL;reflect_rows 是 str 时直接作为 rows,否则 json.dumps(reflect_rows or [], ensure_ascii=False, indent=1);最终返回 tpl.format(digest=_digest_of(digest), dialog=str(dialog or "(未提供)")[:6000], reflect_rows=rows[:3000])。
437
556
  def prompt_for(role: str, digest=None, *, dialog: str = "", reflect_rows="") -> str:
438
557
  """按角色生成复核请求包正文(reflect/verify 共用一处模板真源)。"""
439
558
  tpl = _VERIFY_TPL if role == VERIFY else _REFLECT_TPL
@@ -443,6 +562,7 @@ def prompt_for(role: str, digest=None, *, dialog: str = "", reflect_rows="") ->
443
562
  reflect_rows=rows[:3000])
444
563
 
445
564
 
565
+ # 生效条件:text 取 prompt 真值或 prompt_for(role, digest, dialog=dialog, reflect_rows=reflect_rows);pr=probe(jobs, model, allow_degrade=allow_degrade, channel=channel) 为 SUBAGENT 时原样返回未派发的 SUBAGENT 包、为 CONFIGURE 时返回未派发的 CONFIGURE 包;为 HIVE 时若 autostart 为真且 serve_state(jobs) 非 alive 先 autostart_serve(jobs),再 submit(...),submit 失败回 CONFIGURE 且把 error 拼进 hint,成功则给出 job_id,blocking=False 直接返回;blocking=True 时 wait(sub["job_id"], jobs=jobs, timeout_s=wait_s),res.ok 为假则 unit 记 DEFER 并附未产出有效结果的 hint,为真则 unit=verdict_of(res.get("content")),随后填 transport/attest/result_ok/waited_s 并 _log。
446
566
  def review(*, prompt: str = "", role: str = REFLECT, digest=None, dialog: str = "",
447
567
  reflect_rows="", node_id: str = "", jobs: str = "", model: str = "",
448
568
  timeout_s: int = DEFAULT_TIMEOUT_S, allow_degrade: bool = False,
@@ -501,6 +621,7 @@ def review(*, prompt: str = "", role: str = REFLECT, digest=None, dialog: str =
501
621
 
502
622
  # ---------------------------------------------------------------- serve 拉起(显式)
503
623
 
624
+ # 生效条件:serve_state(jobs_dir(jobs)) 已 alive 时返回 started=False 的“serve 存活”;否则 exe_path() 未被 isfile 命中时返回 started=False 并提示先 cargo build --release;否则以 detached/新会话 Popen 拉起 exe serve --jobs jd 并把 stdout/stderr 写入 jd/_serve.log,Popen 抛 OSError 时返回 started=False 的拉起失败;拉起后在 float(wait_s) 内轮询到 alive 返回 started=True 的“serve 已拉起”,轮询超时仍返回 started=True 但标注心跳未就绪。
504
625
  def autostart_serve(jobs: str = "", wait_s: float = 5.0) -> dict:
505
626
  """**显式**拉起 serve(默认不启用;使用者裁定:不可用即提示配置)。
506
627
 
@@ -532,6 +653,7 @@ def autostart_serve(jobs: str = "", wait_s: float = 5.0) -> dict:
532
653
  return {"started": True, "note": "serve 已拉起(心跳未就绪,稍后自愈)"}
533
654
 
534
655
 
656
+ # 生效条件:返回 dict 的 ok=p["state"]==HIVE,其中 p=plan(jobs)(model/fresh_s/allow_degrade/channel 全走默认),state/serve_alive/serve_age_s/chain/hint/model 取自该 p;jobs_dir 取 jobs_dir(jobs),exe_found 与 exe_path 取 exe_path() 是否被 isfile 命中,model_set=bool(model_name())(无参,读 ENV_MODEL),api_key_set=bool(os.environ.get(ENV_API_KEY)),env 记录 ENV_JOBS_DIR 与 ENV_MODEL 的原值。
535
657
  def doctor(jobs: str = "") -> dict:
536
658
  """能力体检(形态对齐 hive_doctor):判活 + exe + 模型 + 优先级链。"""
537
659
  jd = jobs_dir(jobs)
@@ -543,4 +665,4 @@ def doctor(jobs: str = "") -> dict:
543
665
  "api_key_set": bool(os.environ.get(ENV_API_KEY)),
544
666
  "env": {"HIVE_JOBS_DIR": os.environ.get(ENV_JOBS_DIR, ""),
545
667
  "MDCG_UNIT_MODEL": os.environ.get(ENV_MODEL, "")},
546
- "chain": p["chain"], "hint": p["hint"]}
668
+ "chain": p["chain"], "hint": p["hint"]}
@@ -116,15 +116,18 @@ _RE_DIR_N = re.compile(r"_(\d+)$")
116
116
 
117
117
  # ---- 通用工具 -------------------------------------------------------------
118
118
 
119
+ # 生效条件:x 为 str 时返回 MdCGOS(x) 新实例,否则原样返回 x。
119
120
  def _as_cg(x):
120
121
  """接受 root 路径或已构造 cg 实例——保持密级隔离与密钥上下文。"""
121
122
  return MdCGOS(x) if isinstance(x, str) else x
122
123
 
123
124
 
125
+ # 生效条件:无必需形参,调用即返回 time.strftime("%Y%m%d-%H%M%S") 的当前批次串。
124
126
  def _now_batch() -> str:
125
127
  return time.strftime("%Y%m%d-%H%M%S")
126
128
 
127
129
 
130
+ # 生效条件:base 为字符串批号,先读 _log_path(cg) 的 jsonl 收集 batch 字段中以 base 开头的已有值,base 未被占用则原样返回 base,已占用则返回首个未占用的 f"{base}.{i}"(i 从 2 递增)。
128
131
  def _unique_batch(cg, base: str) -> str:
129
132
  """同秒重复调用时批号去重(后缀 .2/.3…),保证按批次回滚不打偏。"""
130
133
  seen = set()
@@ -140,21 +143,25 @@ def _unique_batch(cg, base: str) -> str:
140
143
  return f"{base}.{i}"
141
144
 
142
145
 
146
+ # 生效条件:cg 具 root 属性时取 cg.root、否则取 str(cg) 作为 root,返回 os.path.join(root, EVIDENCE_LOG)。
143
147
  def _log_path(cg) -> str:
144
148
  root = cg.root if hasattr(cg, "root") else str(cg)
145
149
  return os.path.join(root, EVIDENCE_LOG)
146
150
 
147
151
 
152
+ # 生效条件:batch 与 nid 恒以 "%s|%s" 拼接成条目号,不做空值或类型校验。
148
153
  def _entry_id(batch: str, nid: str) -> str:
149
154
  return "%s|%s" % (batch, nid)
150
155
 
151
156
 
157
+ # 生效条件:v 为 list/tuple 时返回各元素 str(x).strip() 后非空项以「;」连接;否则 v 为 None 返回空串,其余值返回 str(v).strip()。
152
158
  def _as_text(v) -> str:
153
159
  if isinstance(v, (list, tuple)):
154
160
  return ";".join(str(x).strip() for x in v if str(x).strip())
155
161
  return "" if v is None else str(v).strip()
156
162
 
157
163
 
164
+ # 生效条件:path 经 abspath→dirname→basename 取名后匹配 _RE_DIR_N,命中则返回 int(m.group(1)),未命中返回 None。
158
165
  def _gallery_no(path: str):
159
166
  """图集编号:目录名尾部 `_<N>`;缺省 None(脱敏引用用)。"""
160
167
  name = os.path.basename(os.path.dirname(os.path.abspath(path)))
@@ -162,10 +169,12 @@ def _gallery_no(path: str):
162
169
  return int(m.group(1)) if m else None
163
170
 
164
171
 
172
+ # 生效条件:gal 非 None 时返回 "图集_%s" % gal,gal 为 None 时返回 "图集_?"。
165
173
  def _gallery_ref(gal) -> str:
166
174
  return "图集_%s" % (gal if gal is not None else "?")
167
175
 
168
176
 
177
+ # 生效条件:无必需形参,按 os.environ.get(VISION_ROOT_ENV) or os.environ.get(LEGACY_VISION_ROOT_ENV) or DEFAULT_VISION_ROOT 取值——某环境变量为空串时视为假值继续回落下一项。
169
178
  def vision_root() -> str:
170
179
  """视觉证据归档根:env 覆盖 > 遗留 env > 本仓 data/vision 的父目录。"""
171
180
  return (os.environ.get(VISION_ROOT_ENV)
@@ -179,6 +188,7 @@ aeis_root = vision_root
179
188
 
180
189
  # ---- 证据源(只读归档) ---------------------------------------------------
181
190
 
191
+ # 生效条件:root 下 data/vision/*/*.json 逐文件读;文件 OSError/ValueError、JSON 顶层非 dict、parts 非非空 list、或过滤后(type 与 cond_hash 皆真值的 dict)无记录时跳过该文件,否则收入含 path/gallery/image_id/algo/identity_cond_hash/by_type/by_cond 的 src 并最终返回 out 列表。
182
192
  def load_sources(root: str) -> list:
183
193
  """扫描 `AEIS/data/vision/*/*.json`,取逐部件结构化结果(主证据源)。"""
184
194
  base = os.path.join(root, "data", "vision")
@@ -215,6 +225,7 @@ def load_sources(root: str) -> list:
215
225
 
216
226
  # ---- 节点正文解析 ---------------------------------------------------------
217
227
 
228
+ # 生效条件:content 为 None 或空串时按 "" 处理;逐行 strip 后跳过空行与以 # 开头的行,返回首个含 ROOT_MARK 或匹配 _RE_PART 的行,全部无命中返回 ""。
218
229
  def _find_body_line(content: str) -> str:
219
230
  for ln in (content or "").split("\n"):
220
231
  s = ln.strip()
@@ -225,6 +236,7 @@ def _find_body_line(content: str) -> str:
225
236
  return ""
226
237
 
227
238
 
239
+ # 生效条件:rest 为 None/假值时按 "" 处理,分别用 _RE_COND/_RE_VERDICT/_RE_REASON/_RE_FG 捕获;fg 命中则转 float、ValueError 时置 None;reason 命中并 strip 后为空则置 None;返回含 cond_hash/verdict/reason/fg_ratio 四键的 dict(未命中键值为 None)。
228
240
  def _fields(rest: str) -> dict:
229
241
  m = _RE_COND.search(rest or "")
230
242
  v = _RE_VERDICT.search(rest or "")
@@ -242,6 +254,7 @@ def _fields(rest: str) -> dict:
242
254
  "fg_ratio": fg}
243
255
 
244
256
 
257
+ # 生效条件:content 无正文行(空串、全为注释/空行、或无 ROOT_MARK 且不匹配 _RE_PART)返回 None;首行含 ROOT_MARK 返回 kind="root" 记录(n_parts 由 _RE_NPARTS 转 int、未命中为 None,cond_hash 取自 _fields);否则须匹配 _RE_PART,不匹配返回 None,匹配后 bbox 按逗号切分对非空项做 int(float(x))(ValueError 则 bbox=None)并返回 kind="part" 记录。
245
258
  def parse_node(content: str):
246
259
  """视觉节点正文 → 结构化记录;非视觉节点 → None。"""
247
260
  line = _find_body_line(content)
@@ -268,6 +281,7 @@ def parse_node(content: str):
268
281
 
269
282
  # ---- 节点集合 -------------------------------------------------------------
270
283
 
284
+ # 生效条件:layer 透传给 cg._candidates,nid 取 e["id"] 或 path 去 .md 后须以 prefixes 中任一开头且 cg._read 返回非 None 的 fm 才被计数;加密内容记录 locked=True/parsed=None 且不触发 limit 检查,非加密内容 parse_node 后若 limit 非 None 且节点数已达 limit 即 break(因此最多多计该条)。
271
285
  def _vision_nodes(cg, layer=None, prefixes=VISION_PREFIXES, limit=None) -> list:
272
286
  """收集视觉节点(只读 index)。
273
287
 
@@ -295,6 +309,7 @@ def _vision_nodes(cg, layer=None, prefixes=VISION_PREFIXES, limit=None) -> list:
295
309
  return nodes
296
310
 
297
311
 
312
+ # 生效条件:nodes 中 parsed 为 dict、kind=="root" 且 cond_hash 为真值的节点,以其 tags[-1](无 tags 时为空串)为标签 setdefault 记录首个 cond_hash,返回标签→cond_hash 的 out。
298
313
  def _family_root_cond(nodes) -> dict:
299
314
  """家族标签(image_id) → 根节点 cond_hash。"""
300
315
  out = {}
@@ -306,6 +321,7 @@ def _family_root_cond(nodes) -> dict:
306
321
  return out
307
322
 
308
323
 
324
+ # 生效条件:n["id"] 以 "vpipe_" 开头时先以 roots.get(tags[-1] 或 "") 取 cond——cond 为假返回 (None,"no_family_root"),cond 为真则在 sources 中匹配 identity_cond_hash 成功返回 (s,"identity_cond_hash")、未命中再按该标签匹配 image_id 成功返回 (s,"image_id")、仍失败返回 (None,"no_source_archive");非 vpipe_ 时取 tags 中首个匹配 _RE_IMG_TAG 的 img<N>(未取到则不匹配)按 image_id 命中返回 (s,"image_tag"),否则返回 (None,"no_source_archive")。
309
325
  def _pick_source(n, sources, roots) -> tuple:
310
326
  """→ (source, joined_by);无法定位图集 → (None, 原因)。"""
311
327
  nid = n["id"]
@@ -339,6 +355,7 @@ def _pick_source(n, sources, roots) -> tuple:
339
355
 
340
356
  # ---- 三档映射 -------------------------------------------------------------
341
357
 
358
+ # 生效条件:parsed.cond_hash 为真值时按 str(cond_hash) 从 src["by_cond"] 取候选——其中 type 与 parsed["type"] 相同者直接返回 (r,"cond_hash"),否则候选仅 1 条返回 (cands[0],"cond_hash")、多于 1 条返回 (None,None);候选为空或 cond_hash 为假时若 parsed.type 为真则按 src["by_type"].get(type) 命中返回 (r,"type"),否则返回 (None,None)。
342
359
  def _match_record(src, parsed, joined_by):
343
360
  """在归档里定位对应部件记录(imgpart 优先 cond_hash 精确,vpipe 按 type)。"""
344
361
  if parsed.get("cond_hash"):
@@ -355,6 +372,7 @@ def _match_record(src, parsed, joined_by):
355
372
  return None, None
356
373
 
357
374
 
375
+ # 生效条件:n["locked"] 为真→BLINDSPOT(reason="locked");否则 parsed 缺失→"unparsed"、kind 非 "part"→"root_no_verdict";否则 _pick_source(n,sources,roots) 无源→以 why 为 reason;否则 _match_record 无记录→"no_matching_part";否则 parsed 与 rec 的 verdict 均为真且不等→"verdict_mismatch";否则 TIER2_FIELDS 中任一字段为 None→"missing_field:…";全部通过才返回 STATUS_WHITEBOX 与 ev(未用到的形参 aeis_root_used 不参与判定)。
358
376
  def build_evidence(n, sources, roots, aeis_root_used):
359
377
  """单节点 → (status, evidence, meta);严格三档,缺源即 BLINDSPOT。"""
360
378
  parsed = n.get("parsed")
@@ -397,6 +415,7 @@ def build_evidence(n, sources, roots, aeis_root_used):
397
415
 
398
416
  # ---- 预演 / 执行 / 回滚 / 留痕 --------------------------------------------
399
417
 
418
+ # 生效条件:x 经 _as_cg 转换;prefixes 为假值回落 VISION_PREFIXES、aeis_root_ 为假值回落 aeis_root();ids 为真值时才按 set(ids) 过滤 nodes;对 nodes 调 build_evidence,locked 节点只累加 skipped_locked,白箱项入 items、其余入 blindspot_items,全程不写盘并返回含 aeis_root/sources/nodes_scanned/targeted/blindspot/by_reason 的报表。
400
419
  def plan(x, layer=None, prefixes=None, limit=None,
401
420
  aeis_root_=None, ids=None) -> dict:
402
421
  """预演:产出证据回填清单,不写盘。"""
@@ -445,6 +464,7 @@ def plan(x, layer=None, prefixes=None, limit=None,
445
464
  }
446
465
 
447
466
 
467
+ # 生效条件:x 经 _as_cg,batch 为假值回落 BATCH_DEFAULT 并交给 _unique_batch(cg, …) 去重;ids 为真值才按 id 过滤、entry_ids 为真值才按 _entry_id(batch,id) 过滤;循环中节点不在 cg.index["nodes"] 记 skipped_drift,fm 为 None 或内容加密记 skipped_locked,fm 已有同 status(白箱还要求 evidence 相同)记 skipped_already,否则改写 fm 并落盘、追加 jsonl、收集 entry_id;written 非 0 时 cg.rebuild_index() 并尝试 evolution.record(异常被吞)后返回 rep。
448
468
  def apply(x, ids=None, entry_ids=None, layer=None, prefixes=None,
449
469
  limit=None, batch=None, aeis_root_=None, actor=None) -> dict:
450
470
  """执行回填:逐节点改写 frontmatter 证据面,写 `_vision_evidence.jsonl`。"""
@@ -528,6 +548,7 @@ def apply(x, ids=None, entry_ids=None, layer=None, prefixes=None,
528
548
  return rep
529
549
 
530
550
 
551
+ # 生效条件:x 经 _as_cg 后读 _log_path(cg) 日志,只处理 action=="vision_evidence" 记录;batch 为真值时仅取 batch 相同记录、entry_ids 为真值时仅取 entry_id 在集合内记录、该 entry_id 已出现在 rollback 日志则记 skipped_done;节点缺失或 _read 返回 fm 为 None 记 missing,EVIDENCE_KEYS 一个都不在 fm 中记 skipped_done,否则删除命中键、写盘并追加 rollback 留痕,reverted 非 0 时重建索引后返回 rep。
531
552
  def rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
532
553
  """按留痕反向应用:删除本批次写入的证据键(幂等,防覆盖)。"""
533
554
  cg = _as_cg(x)
@@ -580,6 +601,7 @@ def rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
580
601
  return rep
581
602
 
582
603
 
604
+ # 生效条件:x 经 _as_cg 后逐条读 _log_path(cg),batch 为真值时才按 rec.get("batch")==batch 过滤;limit 非 None 且 >=0 时执行 recs = recs[-limit:](limit=0 因 -0 切片退化为全量),limit 为 None 或负数时不截断,返回 {root,total,returned,records}。
583
605
  def history(x, limit=100, batch=None) -> dict:
584
606
  cg = _as_cg(x)
585
607
  recs = []
@@ -596,6 +618,7 @@ def history(x, limit=100, batch=None) -> dict:
596
618
 
597
619
  # ---- CLI(真实库预演/执行用;MCP 侧走 maintain action) -------------------
598
620
 
621
+ # 生效条件:argv 为 None 时 argparse 取 sys.argv;--prefixes 默认由 ",".join(VISION_PREFIXES) 提供并切出非空前缀;a.rollback 为真调 rollback(entry_ids 切分后为空则传 None)、否则 a.apply 为真调 apply、否则调 plan;--json 为真打印整份 JSON,否则按固定关键字打印并恒返回 0。
599
622
  def _main(argv=None) -> int:
600
623
  import argparse
601
624
  ap = argparse.ArgumentParser(description="G5 视觉证据回填(默认只预演)")
@@ -641,4 +664,4 @@ def _main(argv=None) -> int:
641
664
 
642
665
 
643
666
  if __name__ == "__main__": # pragma: no cover
644
- raise SystemExit(_main())
667
+ raise SystemExit(_main())