@furongjun1999/dsh-memory 0.4.8 → 0.4.9

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 (231) 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 +6 -3
  47. package/lib/lib/mdcg_client.js +6 -5
  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_trust.py +361 -0
  195. package/md_cg/test_units_poll.py +71 -0
  196. package/md_cg/test_v14_fixes.py +397 -0
  197. package/md_cg/test_validity_filter.py +280 -0
  198. package/md_cg/test_wisdom_md_store.py +7 -3
  199. package/md_cg/test_writepipe.py +5 -1
  200. package/md_cg/theory.py +16 -1
  201. package/md_cg/tokens.py +40 -8
  202. package/md_cg/tool_face.py +13 -2
  203. package/md_cg/trust.py +943 -0
  204. package/md_cg/twophase.py +12 -1
  205. package/md_cg/units.py +129 -10
  206. package/md_cg/vision_evidence.py +24 -1
  207. package/md_cg/weights.py +24 -1
  208. package/md_cg/whitebox.py +32 -1
  209. package/md_cg/whitebox_kb/data/verify_cache.json +21210 -365
  210. package/md_cg/whitebox_kb/data/verify_savings.jsonl +5078 -0
  211. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  212. package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
  213. package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
  214. package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
  215. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
  216. package/md_cg/writelimit.py +18 -4
  217. package/md_cg/writepipe.py +178 -7
  218. package/package.json +2 -2
  219. package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
  220. package/skills/skills/designer-perspective/scripts/designer.py +17 -1
  221. package/skills/skills/designer-perspective/tests/selftest.py +3 -1
  222. package/src/hooks.ts +17 -21
  223. package/src/index.ts +33 -6
  224. package/src/lib/datapath.ts +211 -13
  225. package/src/lib/mdcg_client.ts +12 -8
  226. package/src/lib/mutual.ts +411 -411
  227. package/src/lib/token_store.ts +4 -5
  228. package/zcode/AGENTS.md +196 -195
  229. package/zcode/README.md +4 -0
  230. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
  231. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
@@ -32,6 +32,7 @@ GROUP_TOPIC = "topic"
32
32
  GROUP_BATCH = "batch"
33
33
 
34
34
 
35
+ # 生效条件:content 为假值(如空串)时返回 (None, None);content 为真且 WL.template_signature(content) 返回真值时返回 (s, 'strong');否则遍历 content.splitlines() 中 strip 后非空的行,遇首个非空行时其 WL._skeleton 长度 ≥ WL.MIN_SKELETON 则返回 ('h:' + sk, 'head'),否则立即返回 (None, None)(不再看后续行),无任何非空行时也返回 (None, None);
35
36
  def _sig_of(content: str):
36
37
  """模板签名(强/弱)。返回 (sig, kind) 或 (None, None)。"""
37
38
  if not content:
@@ -50,6 +51,7 @@ def _sig_of(content: str):
50
51
  return None, None
51
52
 
52
53
 
54
+ # 生效条件:meta(dict)中按 ("branched_from", "branch_id") 顺序取到的首个 v 为真且 str(v) != 'None' 时返回 str(v);否则 meta.get('derived_from') 为 list/tuple 且非空时返回 str(d[0]);否则其为非空 str 且不等于 '[]' 与 'None' 时返回该 str 本身;以上皆不成立时返回 None;
53
55
  def _lineage_key(meta: dict):
54
56
  for f in ("branched_from", "branch_id"):
55
57
  v = meta.get(f)
@@ -63,6 +65,7 @@ def _lineage_key(meta: dict):
63
65
  return None
64
66
 
65
67
 
68
+ # 生效条件:meta 为真时先取 sorted(CF._tag_prefixes(meta)) 作为 pref,pref 为空且 tags 有真值元素时改用各 tag 的 str(t).split(':', 1)[0](跳过假值 tag)集合排序;pref 非空返回 '+'.join(pref);pref 为空时取 str((meta or {}).get('layer') or ''),该层名非空返回 'layer:' + layer,层名为假值(None/空串/0)则返回 None;
66
69
  def _topic_key(meta: dict, tags: list):
67
70
  pref = sorted(CF._tag_prefixes(meta)) if meta else []
68
71
  if not pref and tags:
@@ -73,6 +76,7 @@ def _topic_key(meta: dict, tags: list):
73
76
  return ("layer:" + layer) if layer else None
74
77
 
75
78
 
79
+ # 生效条件:content 经 _sig_of 得到的 sig 为真时返回 (GROUP_TEMPLATE, sig, kind);否则 meta 经 _lineage_key 得到的值非空时返回 (GROUP_LINEAGE, lin, None);否则 (meta, tags) 经 _topic_key 得到的值非空时返回 (GROUP_TOPIC, top, None);三者皆不成立时返回 (GROUP_BATCH, 'misc', None);
76
80
  def _group_of(meta: dict, tags: list, content: str):
77
81
  sig, kind = _sig_of(content)
78
82
  if sig:
@@ -86,6 +90,7 @@ def _group_of(meta: dict, tags: list, content: str):
86
90
  return GROUP_BATCH, "misc", None
87
91
 
88
92
 
93
+ # 生效条件:root 与 meta 均为真、且 meta.get('path') 经 or '' 再 str 后为非空路径时,把 rel 中的 '\\' 与 '/' 替换为 os.sep 并与 root 拼接,以 encoding='utf-8'、errors='replace' 打开并返回 f.read(int(limit))(limit 为 0 时读 0 字节);root 或 meta 为假值、rel 为空、或该打开/读取抛 OSError 时返回 None;
89
94
  def _load_content(root: str, meta: dict, limit: int):
90
95
  if not root or not meta:
91
96
  return None
@@ -100,6 +105,7 @@ def _load_content(root: str, meta: dict, limit: int):
100
105
  return None
101
106
 
102
107
 
108
+ # 生效条件:恒返回构造 dict——ref/node_id/proposal_id/origin/layer/role/importance/evidence_count/verification_basis/lifecycle_state 均以 .get 取值(缺键得 None),content_hash 先取 cand.get('content_hash') 为空再取 (meta or {}).get('content_hash'),tags/issue_kinds/evidence 经 or [] 后 list 化,excerpt 取 (content or '')[:int(limit)] 为空时(含 content 为空串或 limit 为 0)为 None;
103
109
  def _entry(cand: dict, meta: dict, content: str, limit: int) -> dict:
104
110
  return {"ref": cand.get("key"), "node_id": cand.get("node_id"),
105
111
  "proposal_id": cand.get("proposal_id"), "origin": cand.get("origin"),
@@ -115,11 +121,13 @@ def _entry(cand: dict, meta: dict, content: str, limit: int) -> dict:
115
121
  "excerpt": (content or "")[:int(limit)] or None}
116
122
 
117
123
 
124
+ # 生效条件:ids 与 size 为必需形参,size 先经 max(1, int(size))(0、负值或 1 均按 1)作为步长,返回 [ids[i:i + size] for i in range(0, len(ids), size)];ids 为空(如 [] 或空串)时返回 [];
118
125
  def _chunks(ids: list, size: int) -> list:
119
126
  size = max(1, int(size))
120
127
  return [ids[i:i + size] for i in range(0, len(ids), size)]
121
128
 
122
129
 
130
+ # 生效条件:candidates(list)为必需输入,nodes 为假值时按 {} 取节点表;逐候选以 c.get('node_id') 查 meta(无 node_id 则 meta 为 {})、以 c.get('content') or '' 取内容,内容为空且 read_content 为真且 nid 为真时改用 _load_content(root, meta, content_limit) 的结果(再 or ''),并按 int(content_limit) 截断;随后按 _group_of(meta, c.get('tags') or [], content) 分组,非 GROUP_BATCH 且组内 id 数 < int(min_group)(min_group 为 0 时该比较恒不成立,即无降级)的组与既有 GROUP_BATCH 组一并降级,降级集合非空时并入 (GROUP_BATCH, 'misc')、为空则不建该组;再对每组按 int(max_per_bundle) 经 _chunks 切块生成带 entries 的 bundles 与 stats,返回 content_missing 为 contents 中该候选键对应值为假值的候选数;
123
131
  def bundle(candidates: list, nodes=None, root=None, *, max_per_bundle=50,
124
132
  min_group=2, read_content=True, content_limit=3000) -> dict:
125
133
  """候选清单 → 待评包(含条目上下文与机械字段,供级 3 装配与级 4 spec)。"""
@@ -33,6 +33,7 @@ PATROL_EPOCH = 3600.0 # 轮巡窗口推进周期(秒):默认每
33
33
  COLD_IMPORTANCE_MIN = 0.6 # 冷节点抽样门槛(importance 解析失败按 0 计)
34
34
 
35
35
 
36
+ # 生效条件:path 不满足 os.path.exists(path) 时返回 [];否则逐行 strip 后跳过空行与 json.JSONDecodeError 行,返回成功 json.loads 的每行结果列表。
36
37
  def _read_jsonl(path: str) -> list:
37
38
  if not os.path.exists(path):
38
39
  return []
@@ -49,6 +50,7 @@ def _read_jsonl(path: str) -> list:
49
50
  return out
50
51
 
51
52
 
53
+ # 生效条件:os.path.join(root, CF.ACCESS_LOG) 不存在时返回空集;否则逐行解析,rec.get("ids") 为 list/tuple 时仅加入其中真值元素的 str 形式,nid 取 rec.get("id") or rec.get("node_id") or rec.get("nid") 的真值并加入 str(nid),最终返回 seen & set(nodes)(仅保留 nodes 键内的 id)。
52
54
  def _touched(root: str, nodes: dict) -> set:
53
55
  """access log 触达集(与 conformance._reach_metrics 同一解析口径:ids / id / node_id / nid)。"""
54
56
  p = os.path.join(root, CF.ACCESS_LOG)
@@ -73,6 +75,7 @@ def _touched(root: str, nodes: dict) -> set:
73
75
  return seen & set(nodes)
74
76
 
75
77
 
78
+ # 生效条件:str(layer)=="knowledge" 的节点在 role 为假值、CF._as_int(evidence_count)==0、或 {"doc","code"} 命中 CF._tag_prefixes(r) 时分别产出 missing_field/mixed_layer;任意节点在 CF._as_int(evidence_count)==0 且无 CF._basis_of(r) 且 lifecycle_state 为假值时产出 unanalyzed;str(content_hash) 既非 "None" 也非 "" 且同指纹计数 >1 时该组内每个节点产出 dup_content;edges 为 list/tuple 时逐条处理其中的 dict 边,当该边所有真值键(CF.CANONICAL_EDGE_KEYS 与 "type")中不含任一规范键时产出 edge_non_canonical 并停止该节点后续边。
76
79
  def _assertion_items(nodes: dict) -> list:
77
80
  """断言集超限项 → 条目级(判据函数与 conformance 同源,不另立口径)。"""
78
81
  out = []
@@ -116,6 +119,7 @@ def _assertion_items(nodes: dict) -> list:
116
119
  return out
117
120
 
118
121
 
122
+ # 生效条件:当 root、nodes 给定,对 _read_jsonl(root/CF.INBOX_LOG) 解析出的 inbox 记录中 pid(str(r.get("pid") or ""))非空且不在 decisions 中 x.get("pid") 真值 str 集合的记录,生成 origin=proposal、issue_kind=gate_defer 条目,其中 layer 直接取 r.get("layer") 不回落、content 取 r.get("content") or ""、tags 取 r.get("tags") or []、content_hash 取 r.get("payload_hash") or ""、node_id 取 str(r.get("id") or "") or None;再对 _read_jsonl(root/FORGET_LOG) 中 str(r.get("verdict"))=="DEFER" 的记录,若 str(r.get("node_id") or "") 非空且该 nid 是 nodes 的键则加入 out,否则加入 skipped(source=defer_queue、reason=node_not_landed、detail 为 nid 或 str(r.get("reason")));返回 (out, skipped);
119
123
  def _defer_items(root: str, nodes: dict):
120
124
  """闸门待裁决提案 + 遗忘日志 DEFER 留痕。返回 (items, skipped)。"""
121
125
  ib = _read_jsonl(os.path.join(root, CF.INBOX_LOG))
@@ -144,9 +148,11 @@ def _defer_items(root: str, nodes: dict):
144
148
  return out, skipped
145
149
 
146
150
 
151
+ # 生效条件:取 nodes 中不在 _touched(root, nodes) 触达集、且 importance 经 _imp 归一后 >= COLD_IMPORTANCE_MIN 的节点,按归一 importance 降序(同值按 node_id 升序)后取 [:int(limit)];limit=0 得空列表,limit 为负则排除末尾 |limit| 条而非空返回(源码无 int(limit)<=0 早退)。
147
152
  def _cold_items(root: str, nodes: dict, limit: int) -> list:
148
153
  seen = _touched(root, nodes)
149
154
 
155
+ # 生效条件:r 的 importance 为真值且 float() 可转换时返回其 float 值;importance 为假值(None/0/"")时回落 0.0;转换抛 TypeError 或 ValueError 时返回 0.0。
150
156
  def _imp(r):
151
157
  try:
152
158
  return float(r.get("importance") or 0.0)
@@ -161,6 +167,7 @@ def _cold_items(root: str, nodes: dict, limit: int) -> list:
161
167
  for i, (nid, r) in enumerate(picked)]
162
168
 
163
169
 
170
+ # 生效条件:int(window) <= 0 或 nodes 为假值时返回 [];否则按 sha1(nid.encode("utf-8")).hexdigest() 排序,从 (int(offset)*int(window)) % len(nodes) 起取 int(window) 条,不足时用头部元素环绕补足。
164
171
  def _patrol_items(nodes: dict, window: int, offset: int) -> list:
165
172
  if int(window) <= 0 or not nodes:
166
173
  return []
@@ -175,6 +182,7 @@ def _patrol_items(nodes: dict, window: int, offset: int) -> list:
175
182
  for nid in picked]
176
183
 
177
184
 
185
+ # 生效条件:items 逐条以 node_id 为真时 key=str(node_id)、否则 key="pid:"+str(proposal_id) 聚合;首次建房时 node_id 为真则取 nodes.get(node_id) or {} 作 meta,tags 取 it.get("tags") or meta.get("tags") or [],evidence 每聚合记录最多 8 条;结果 sources 去重后为空则置 ["-"],priority 取 _PRIORITY.get(s, 9) 的最小值,按 (priority, str(key)) 排序返回。
178
186
  def _merge(items: list, nodes: dict) -> list:
179
187
  """按条目(节点/提案)聚合:一条候选 = 一个待评条目,多个问题挂在 issue_kinds 上。"""
180
188
  acc = {}
@@ -211,6 +219,7 @@ def _merge(items: list, nodes: dict) -> list:
211
219
  return out
212
220
 
213
221
 
222
+ # 生效条件:root 为假值(None/"")时回落 mdcg_root();nodes 为 None 时用 CF.load_index(root) or {};wanted 为 (sources or SOURCES) 与 SOURCES 的交集,按其中是否含 assertion/defer_queue/cold/patrol 分派取候选(cold 传 cold_limit,patrol 传 patrol_window 与偏移,patrol_offset 为 None 时偏移取 int((now if now is not None else time.time())//PATROL_EPOCH));limit 为真值时才把 merged 截到 int(limit);with_report 为真值时补 CF.check(root, check_paths=False) 的指标与失败断言 id。
214
223
  def generate(root=None, *, sources=SOURCES, nodes=None, limit=None, cold_limit=500,
215
224
  patrol_window=200, patrol_offset=None, with_report=False, now=None) -> dict:
216
225
  """四源候选生成。nodes 可注入(测试用合成库,免读真源)。"""
@@ -112,24 +112,29 @@ ACTIONS = ("role", "role_rollback", "role_history", "role_stats")
112
112
 
113
113
  # ---- 通用工具 ------------------------------------------------------------
114
114
 
115
+ # 生效条件:传入 cg 即返回 os.path.join(cg.root, GOVERN_LOG),无分支。
115
116
  def _log_path(cg) -> str:
116
117
  return os.path.join(cg.root, GOVERN_LOG)
117
118
 
118
119
 
120
+ # 生效条件:fm 为假值(None/{})时按 {} 处理,其 .get("tags") 为假值(None/空串/空列表)时回落 [],否则对该值逐项过滤,只保留 isinstance(t, str) 的元素。
119
121
  def _tags(fm: dict) -> list:
120
122
  return [t for t in ((fm or {}).get("tags") or []) if isinstance(t, str)]
121
123
 
122
124
 
125
+ # 生效条件:detail 为假值(None/空串)时返回 kind,否则返回 "%s:%s" % (kind, detail)。
123
126
  def _reason_key(kind: str, detail: str = None) -> str:
124
127
  return kind if not detail else "%s:%s" % (kind, detail)
125
128
 
126
129
 
130
+ # 生效条件:box 缺 key 时以 box.get(key, 0) 取 0 再加 1 写回 box[key];key 已存在(含值为非数值)时直接对现值 +1。
127
131
  def _bump(box: dict, key: str) -> None:
128
132
  box[key] = box.get(key, 0) + 1
129
133
 
130
134
 
131
135
  # ---- 靶子定位(rule 驱动)------------------------------------------------
132
136
 
137
+ # 生效条件:rules/rules_dir 经 _as_rules 得到的列表中存在 id == rule_id 的项、且该项 matcher.layer 非空、mechanical 中存在 check == CHECK_FIELD_ABSENT 且 field 为真的项时,返回 {'rule_id','title','field','layers','severity','remedy','llm'};该项不存在或 layers/field 为空则抛 ValueError。
133
138
  def role_rule(*, rules=None, rules_dir=None, rule_id: str = RULE_ROLE) -> dict:
134
139
  """从 M1 规则库取「role 回填」靶规则 → `{field, layers, remedy, …}`。
135
140
 
@@ -158,6 +163,7 @@ def role_rule(*, rules=None, rules_dir=None, rule_id: str = RULE_ROLE) -> dict:
158
163
 
159
164
  # ---- 取值推导(唯一入口:只搬运已声明的证据)------------------------------
160
165
 
166
+ # 生效条件:按 sources 逐档判定——含 SOURCE_TAGS 时取 fm 的 tags 内首个 ROLE_TAG_PREFIX 前缀标签,其值属 ALL_ROLES 则返回 (值, BASIS_TAG, SOURCE_TAGS, None),值域外则返回 (None,None,None, REASON_TAG_OOD 原因);tags 未定出时含 SOURCE_MAP 则以小写 writer 取 role_map 值,属 WORK_ROLES 返回 REASON_MAP_WORK 原因、属 ALL_ROLES 返回 (值,"WRITER_ROLE_MAP[writer]",SOURCE_MAP,None)、否则返回 REASON_MAP_OOD 原因(命中域外值即返回、不降级);再否且含 SOURCE_LAYER 且 LAYER_DEFAULT_ROLE 命中 e["layer"] 时返回 (值, BASIS_LAYER, SOURCE_LAYER, None);全部未命中则 writer 非空返回 REASON_WRITER_NO_MAP:writer,writer 为空返回 REASON_NO_SOURCE。
161
167
  def _candidate_role(e, fm, *, sources, role_map) -> tuple:
162
168
  """→ `(role, basis, source, reason)`:有来源则 reason=None;无来源则 role=None。
163
169
 
@@ -192,6 +198,7 @@ def _candidate_role(e, fm, *, sources, role_map) -> tuple:
192
198
  else REASON_NO_SOURCE)
193
199
 
194
200
 
201
+ # 生效条件:按序判定——_readable_guard(cg, e) 为假返回 ("denied", None);cg._read(e) 的 fm 为 None 返回 ("unreadable", None);crypto.is_encrypted(content) 为真返回 ("locked", None);fm.get("role") 非空白返回 ("present", None);否则 _candidate_role(e, fm, sources=sources, role_map=role_map) 的 role 为假返回 ("unfillable", {id,layer,reason,writer}),role 为真返回 ("", {id,layer,role,source,basis,before,had_key,writer})。
195
202
  def _classify(cg, e, nid, *, sources, role_map) -> tuple:
196
203
  """单条裁决 → `(skip_reason, item|gap)`;skip_reason 为空串表示可回填。"""
197
204
  if not _readable_guard(cg, e):
@@ -213,6 +220,7 @@ def _classify(cg, e, nid, *, sources, role_map) -> tuple:
213
220
  "had_key": "role" in fm, "writer": fm.get("writer")}
214
221
 
215
222
 
223
+ # 生效条件:want 由必填 layers 决定;layer 为真值时须属 layers 否则抛 ValueError,且命中后 want 收窄为 {layer};ids 为真值时只保留白名单内 nid、为假值(None/[])时不过滤;prefix 为真值时只保留 nid 以之开头者;仅 str(e.get("layer")) 属 want 的条目按 nid 升序进入返回列表。
216
224
  def _iter_scope(cg, *, layers, layer=None, prefix=None, ids=None) -> list:
217
225
  """按规则作用域遍历索引条目 → `[(nid, entry)]`(只读、nid 稳定序)。
218
226
 
@@ -241,6 +249,7 @@ def _iter_scope(cg, *, layers, layer=None, prefix=None, ids=None) -> list:
241
249
 
242
250
  # ---- 预演 -----------------------------------------------------------------
243
251
 
252
+ # 生效条件:以 x 经 _as_cg 得 cg 并先经 role_rule(rules,rules_dir,rule_id) 取规则(缺规则/缺 layer/缺 field 时该步抛 ValueError),role_map 非空时的条目以小写去空白键合并进 WRITER_ROLE_MAP 副本,再遍历 _iter_scope(cg, layers=rule["layers"], layer=layer, prefix=prefix, ids=ids) 逐条 _classify 后返回不写盘的 rep;每条 item 仅在 limit is None 或 len(rep["items"]) < limit 时追加(limit=0 时 items 为空但 targeted 仍累加),sample 为真且 planned_ids 非空时以 max(1, len(planned_ids)//int(sample)) 为步长取前 int(sample) 项。
244
253
  def role_plan(x, layer=None, limit=None, ids=None, prefix=None, *,
245
254
  sources=DEFAULT_SOURCES, role_map=None, rule_id=RULE_ROLE,
246
255
  rules=None, rules_dir=None, sample=0) -> dict:
@@ -298,6 +307,7 @@ def role_plan(x, layer=None, limit=None, ids=None, prefix=None, *,
298
307
 
299
308
  # ---- 执行 / 回滚 / 留痕 / 对照 -------------------------------------------
300
309
 
310
+ # 生效条件:x 经 _as_cg,batch 为假值时回落 BATCH_DEFAULT;先调 role_plan 取 items,entry_ids 为真值时按 entry_id 收窄;逐项处理:索引无该节点或 _read 得 fm 为 None → skipped_missing,_readable_guard 为假 → skipped_denied,crypto.is_encrypted(content) 为真 → skipped_locked,fm["role"] 非空 → skipped_drift,否则写回 role 并 append_jsonl 留痕、written 递增;written 非零时 cg.rebuild_index(),返回含 plan_remaining 的 rep。
301
311
  def role_apply(x, ids=None, entry_ids=None, layer=None, limit=None,
302
312
  batch=BATCH_DEFAULT, sources=DEFAULT_SOURCES, role_map=None,
303
313
  rule_id=RULE_ROLE, rules=None, rules_dir=None, actor=None,
@@ -366,6 +376,7 @@ def role_apply(x, ids=None, entry_ids=None, layer=None, limit=None,
366
376
  return rep
367
377
 
368
378
 
379
+ # 生效条件:只处理 action == ACTION_ROLE 的留痕,且 batch 为真时要求 rec.get("batch") == batch(batch 为 None/假值时不按批次过滤)、entry_ids 为真时要求 rec.get("entry_id") ∈ set(entry_ids);rec 的 write_id 已在既有 ACTION_ROLE_ROLLBACK 记录中 → skipped_done,节点不在索引或 read 不到或加密 → missing,当前 fm.get("role") 为空白或与 rec.get("value") 不等 → conflict 不覆盖,相等时按 fm_before.get("had_key") 恢复 fm_before["role"] 或删除 role 键;reverted 非 0 时 cg.rebuild_index()。
369
380
  def role_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
370
381
  """按留痕反向应用:撤销 role 回填。
371
382
 
@@ -423,6 +434,7 @@ def role_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
423
434
  return rep
424
435
 
425
436
 
437
+ # 生效条件:action 为真时只保留 r.get("action") == action 的记录、batch 为真时只保留 r.get("batch") == batch 的记录,total/by_action 统计的是过滤后的 recs 全量;records 在 limit 为假值(0/None)时返回全部 recs,否则返回 recs[-int(limit):]。
426
438
  def history(x, limit=100, action=None, batch=None) -> dict:
427
439
  """读 `_govern.jsonl` 留痕(治理动作的可审计面)。"""
428
440
  cg = _as_cg(x)
@@ -437,6 +449,7 @@ def history(x, limit=100, action=None, batch=None) -> dict:
437
449
  "by_action": by_action, "records": tail}
438
450
 
439
451
 
452
+ # 生效条件:x 经 _as_cg 且 role_rule(rules=rules, rules_dir=rules_dir, rule_id=rule_id) 命中并声明了 matcher.layer 与 field_absent.field 时,返回以 conformance.load_index(cg.root) 的节点为口径的统计(by_layer 计全部节点、by_value 只计 role 非空白者、target_met 由 role_ratio_kn >= float(THRESHOLDS["role_coverage_min"]) 决定、gap_to_target 取 max(0.0, thr-kn));规则缺失或声明不全时在 role_rule 处抛 ValueError。
440
453
  def role_stats(x, *, rule_id=RULE_ROLE, rules=None, rules_dir=None) -> dict:
441
454
  """只读对照:role 覆盖率 + 取值分布(治理前后量化用)。
442
455
 
@@ -466,6 +479,7 @@ def role_stats(x, *, rule_id=RULE_ROLE, rules=None, rules_dir=None) -> dict:
466
479
 
467
480
  # ---- 统一入口 ------------------------------------------------------------
468
481
 
482
+ # 生效条件:action 不在 ACTIONS 内时抛 ValueError;action == ACTION_ROLE 时以 kw.pop("apply", False) 为真调用 role_apply(x, **kw)、为假(含缺该键)调用 role_plan(x, **kw);其余情况以 {ACTION_ROLE_ROLLBACK: role_rollback, "role_history": history, "role_stats": role_stats}[action] 取 fn 并返回 fn(x, **kw)。
469
483
  def run(x, action, **kw) -> dict:
470
484
  """`role`(`apply=True` 则执行)/ `role_rollback` / `role_history` / `role_stats`。"""
471
485
  if action not in ACTIONS:
@@ -485,6 +499,7 @@ def run(x, action, **kw) -> dict:
485
499
  # 须写在子命令**之后**:`govern plan --root <root>`。写在子命令之前会被顶层解析器
486
500
  # 当作位置参数吃掉(`scripts/review_cli.py` 的同型坑,2026-09-15 实测更正)。
487
501
 
502
+ # 生效条件:s 为假值(None/空串)时直接返回 DEFAULT_SOURCES;否则按 "," 切分并 strip 丢弃空段,任一段不在 ALL_SOURCES 内即抛 ValueError,切分后 vals 为空(如 s=",")也抛 ValueError,其余返回该非空元组。
488
503
  def _parse_sources(s) -> tuple:
489
504
  """CLI 侧来源档解析:逗号分隔,值域封闭(非法即抛,不静默降级)。"""
490
505
  if not s:
@@ -498,6 +513,7 @@ def _parse_sources(s) -> tuple:
498
513
  return vals
499
514
 
500
515
 
516
+ # 生效条件:无参数,调用即返回 argparse.ArgumentParser(add_help=False),其中已含 --root(default=None)、--rules-dir(default=None)、--json(store_true) 三项,无分支。
501
517
  def _common_parser() -> argparse.ArgumentParser:
502
518
  ap = argparse.ArgumentParser(add_help=False)
503
519
  ap.add_argument("--root", default=None, help="真源根(缺省读环境变量 MDCG_ROOT)")
@@ -506,6 +522,7 @@ def _common_parser() -> argparse.ArgumentParser:
506
522
  return ap
507
523
 
508
524
 
525
+ # 生效条件:传入 p 即向它注册 --layer/--prefix/--ids/--limit(type=int)/--sources 五个参数,无分支且无返回值。
509
526
  def _add_scope_args(p) -> None:
510
527
  p.add_argument("--layer", help="收窄到该层(须在规则 matcher 层之内)")
511
528
  p.add_argument("--prefix", help="只处理该 id 前缀")
@@ -516,17 +533,20 @@ def _add_scope_args(p) -> None:
516
533
  % ",".join(DEFAULT_SOURCES))
517
534
 
518
535
 
536
+ # 生效条件:传入 a 后返回 {'layer': a.layer, 'prefix': a.prefix, 'limit': a.limit, 'ids': [...], 'sources': _parse_sources(a.sources), 'rules_dir': a.rules_dir},其中 ids 由 a.ids(为假值即 None/空串)按 "," 切分去空段、结果为空则回落 None,sources 由 a.sources 经 _parse_sources 解析(假值 → DEFAULT_SOURCES,非法值抛 ValueError)。
519
537
  def _scope_kw(a) -> dict:
520
538
  return {"layer": a.layer, "prefix": a.prefix, "limit": a.limit,
521
539
  "ids": [i.strip() for i in (a.ids or "").split(",") if i.strip()] or None,
522
540
  "sources": _parse_sources(a.sources), "rules_dir": a.rules_dir}
523
541
 
524
542
 
543
+ # 生效条件:SOURCE_LAYER 在 sources 内时向 sys.stderr 打印 "警告:" + LAYER_DEFAULT_WARNING,否则不输出。
525
544
  def _warn_layer_default(sources) -> None:
526
545
  if SOURCE_LAYER in sources:
527
546
  print("警告:" + LAYER_DEFAULT_WARNING, file=sys.stderr)
528
547
 
529
548
 
549
+ # 生效条件:传入 msg 即向 sys.stderr 打印该 msg 并返回 2,无分支。
530
550
  def _die(msg: str) -> int:
531
551
  print(msg, file=sys.stderr)
532
552
  return 2
@@ -671,4 +691,4 @@ def main(argv=None) -> int:
671
691
 
672
692
 
673
693
  if __name__ == "__main__":
674
- sys.exit(main())
694
+ sys.exit(main())
@@ -113,12 +113,14 @@ _REF_DEAD_STATUSES = ("dangling", "stale")
113
113
  _FIELD_LAYER_CACHE = None
114
114
 
115
115
 
116
+ # 生效条件:kind 经 str(kind or "").strip() 得 k(None/空串等假值 → 空串 ""),k 命中 KIND_ALIASES 时返回其规范名,否则原样返回 k。
116
117
  def canonical_kind(kind) -> str:
117
118
  """M1/D1 用词 → D1 规范名(未知原样返回,不假装认路)。"""
118
119
  k = str(kind or "").strip()
119
120
  return KIND_ALIASES.get(k, k)
120
121
 
121
122
 
123
+ # 生效条件:仅当 rules 与 rules_dir 均为 None 且模块级 _FIELD_LAYER_CACHE 非 None 时直接返回该缓存;否则遍历 rules(dict 取其 "rules" 键的列表、非 dict 直接 list(rules))或 rules_dir 经 RS.load_rules 取得的 rules 列表,从限了 matcher.layer 的 mechanical 规则中收集 field_absent 的 spec["field"] 与 evidence_zero 的 evidence_count,返回 {字段: 排序列},且只在 rules 与 rules_dir 均为 None 时写回 _FIELD_LAYER_CACHE。
122
124
  def field_layer_scope(rules=None, rules_dir=None) -> dict:
123
125
  """字段 → 适用层清单(**派生**自 M1 规则库,不在此另立一份)。
124
126
 
@@ -155,6 +157,7 @@ def field_layer_scope(rules=None, rules_dir=None) -> dict:
155
157
  return out
156
158
 
157
159
 
160
+ # 生效条件:(scope or {}).get(field) 为假值(scope 为 None、空 dict 或 field 不在其中)→ 返回 True;want 为真值时仅当 str((meta or {}).get("layer")) 在 set(want) 内返回 True,否则 False。
158
161
  def _layer_ok(field, scope, meta) -> bool:
159
162
  """字段层门限:**限层的字段只在该层的节点上检查**。
160
163
 
@@ -168,6 +171,7 @@ def _layer_ok(field, scope, meta) -> bool:
168
171
  return str((meta or {}).get("layer")) in set(want)
169
172
 
170
173
 
174
+ # 生效条件:os.path.getmtime(path) 成功则返回该 mtime;抛 OSError 或 TypeError → 返回 None。
171
175
  def _mtime(path):
172
176
  """文件 mtime(不可读 → `None`,不假装知道)。"""
173
177
  try:
@@ -176,6 +180,7 @@ def _mtime(path):
176
180
  return None
177
181
 
178
182
 
183
+ # 生效条件:root 或 rel(path 为真值时取 path,否则取 meta 的 "path")为空 → 返回 ("unknown", "无 root/path 可用,取不到盘上 mtime,成因未判定");两者都有时取节点文件与 CF.INDEX_FILE 的 mtime,任一为 None → 返回 ("unknown", "节点文件或索引快照不可读,成因未判定"),节点 mtime 减索引 mtime 之差 > MTIME_TOLERANCE → 返回 ("index_lag", …),否则返回 ("true_mismatch", …)。
179
184
  def hash_mismatch_cause(meta, *, root=None, path=None) -> tuple:
180
185
  """`content_hash` 声明值 ≠ 正文实算值 → **成因**判定(靠 mtime 证据,不猜)。
181
186
 
@@ -204,12 +209,14 @@ def hash_mismatch_cause(meta, *, root=None, path=None) -> tuple:
204
209
 
205
210
  # ---------------------------- 基础工具(纯函数) ----------------------------
206
211
 
212
+ # 生效条件:v 为 list/tuple/dict 时返回 not v(空容器 → True);其余类型返回 v is None 或 str(v).strip() 为 "" 或 "None"。
207
213
  def _blank(v) -> bool:
208
214
  if isinstance(v, (list, tuple, dict)):
209
215
  return not v
210
216
  return v is None or str(v).strip() in ("", "None")
211
217
 
212
218
 
219
+ # 生效条件:int(float(v)) 可算(含数字字符串)则返回该整数;抛 TypeError 或 ValueError(含 v 为 None、非数字串)→ 返回 None。
213
220
  def _as_int(v):
214
221
  try:
215
222
  return int(float(v))
@@ -217,6 +224,7 @@ def _as_int(v):
217
224
  return None
218
225
 
219
226
 
227
+ # 生效条件:float(v) 可算则返回该浮点数;抛 TypeError 或 ValueError(含 v 为 None、非数字串)→ 返回 None。
220
228
  def _as_float(v):
221
229
  try:
222
230
  return float(v)
@@ -227,6 +235,7 @@ def _as_float(v):
227
235
  _SENT_END = "。!?;!?;\n"
228
236
 
229
237
 
238
+ # 生效条件:content 为假值(None/空串)按 "" 处理,逐字符命中 _SENT_END 切出且 seg.strip() 非空的段以 len(out) 为序号追加 (索引, start, i+1, 原文),末尾 tail.strip() 非空亦追加;无合格段 → 返回空列表。
230
239
  def sentence_spans(content: str) -> list:
231
240
  """正文 → `[(句索引, start, end, 原文)]`;空句不编号(索引连续,确定性)。"""
232
241
  text, out, start = content or "", [], 0
@@ -246,6 +255,7 @@ def sentence_spans(content: str) -> list:
246
255
  _RE_MARK = re.compile(r"^#[ \t]*(?P<mark>[^::\n]{1,16})[::][^\n]*", re.M)
247
256
 
248
257
 
258
+ # 生效条件:在 content(假值 → "")上用模块级 _RE_MARK 迭代匹配,以 m.group("mark").strip() 为键 setdefault 记下首次出现的 [m.start(), m.end());无匹配(含 content 为假值)→ 返回空 dict。
249
259
  def mark_spans(content: str) -> dict:
250
260
  """正文 CCG 要素行 → `{要素名: [start, end)}`;同要素取首次出现(确定性)。"""
251
261
  out = {}
@@ -254,6 +264,7 @@ def mark_spans(content: str) -> dict:
254
264
  return out
255
265
 
256
266
 
267
+ # 生效条件:(text or "").split("\n") 的每行 strip 后非空、不以 "#" 开头且含 ":" 时,取首个冒号前的键 k,k 非空且尚未入表则记 (1-based 行号, [该行起始偏移, 起始偏移+len(line)]);text 为假值或无合格行 → 返回空 dict。
257
268
  def key_line_spans(text: str) -> dict:
258
269
  """节点文件原文 → `{键: (1-based 行号, [start, end))}`。
259
270
 
@@ -271,6 +282,7 @@ def key_line_spans(text: str) -> dict:
271
282
  return out
272
283
 
273
284
 
285
+ # 生效条件:index 为 dict 时其 "nodes" 为 dict 则返回 index["nodes"],否则返回 index 本身;index 为 None/非 dict 时调 CF.load_index(root),抛 OSError 或 ValueError → 返回 {},返回值为 dict 且其 "nodes" 为 dict → 返回该 "nodes",是 dict → 原样返回,否则返回 {}。
274
286
  def _index(root, index=None) -> dict:
275
287
  """索引节点表 `{node_id: meta}`(兼容 load_index 的 `{"nodes": …}` 形态)。"""
276
288
  if isinstance(index, dict):
@@ -284,6 +296,7 @@ def _index(root, index=None) -> dict:
284
296
  return idx if isinstance(idx, dict) else {}
285
297
 
286
298
 
299
+ # 生效条件:_index(root, index) 中 node_id 对应值非 dict → 返回 None;否则用 meta.get("path")(绝对路径直接用,否则 join(root, str(rel or "")))读文件——OSError 时返回 content/text 为 None、fm 为 {} 的 dict,成功则把 NF.loads(text) 得到的 content 与 fm(假值 → {})连同 meta/path/text 一并返回。
287
300
  def load_node(node_id, root, *, index=None) -> dict:
288
301
  """读一个节点(索引 meta + 文件 frontmatter + 正文原文)。
289
302
 
@@ -307,6 +320,7 @@ def load_node(node_id, root, *, index=None) -> dict:
307
320
  "text": text, "path": fp}
308
321
 
309
322
 
323
+ # 生效条件:span 为 None → 返回 "";否则取 (text or "")[span[0]:span[1]] 去空白并把换行替换为 "⏎",长度超 SNIPPET_MAX 时截断并追加 "…"。
310
324
  def _snippet(text, span) -> str:
311
325
  """命中片段(供人工核对肉眼确认「指的是不是这一句」)。"""
312
326
  if span is None:
@@ -315,6 +329,7 @@ def _snippet(text, span) -> str:
315
329
  return seg[:SNIPPET_MAX] + ("…" if len(seg) > SNIPPET_MAX else "")
316
330
 
317
331
 
332
+ # 生效条件:任意 node_id/kind/field/span/evidence 均原样写入返回 dict 的 node_id/issue_kind/field/span/evidence 键,可选 rule/line/sentence/snippet/peer/cause/severity 未传时为 None、status 未传时为 "located",不做任何校验。
318
333
  def _hit(node_id, kind, field, span, evidence, *, rule=None, line=None,
319
334
  sentence=None, snippet=None, peer=None, cause=None, status="located",
320
335
  severity=None) -> dict:
@@ -330,6 +345,7 @@ def _hit(node_id, kind, field, span, evidence, *, rule=None, line=None,
330
345
  "severity": severity}
331
346
 
332
347
 
348
+ # 生效条件:text 为真值、span 与 content 均非 None 且 text.find(content)>=0 时,返回 text.count("\n",0,min(off+span[0],len(text)))+1 的 1-based 行号;text 假值或 span/content 为 None 或 content 未找到时返回 None。
333
349
  def _line_of(text, content, span):
334
350
  """正文区间 → 节点文件原文的 1-based 行号(供人工核对直接跳文件)。"""
335
351
  if not text or span is None or content is None:
@@ -346,6 +362,7 @@ def _line_of(text, content, span):
346
362
  # 新增判据 = 加函数 + 注册,不改 locate 主流程(与 M1「引擎冻结、规则可增删」同构)。
347
363
  # ctx = {"fm", "text", "root", "index", "peers", "now"}
348
364
 
365
+ # 生效条件:在 node_id/meta/content/ctx 下,对 FM_SCAN_FIELDS 中除 verification_basis、condition_space 外且 _layer_ok(f,scope,meta) 为真的字段,若 meta.get(f) 与 ctx.get("fm") 或 {} 中的同名字段皆 _blank 则记 field_absent;对过 _layer_ok 且 _as_int(meta.get("evidence_count"))==0 记 evidence_zero;对 meta.get("importance") 非 None 且 _as_float 为 None 或不在 [0.0,1.0] 记 field_invalid;对 condition_space 经 meta 或回落 ctx.get("fm") 后 NF.condition_space_missing 非空记 condition_slots;对正文缺 NF.CCG_MARKS 行记 ccg_incomplete,返回这些命中列表。
349
366
  def _loc_missing_field(node_id, meta, content, ctx):
350
367
  """frontmatter/正文结构字段为空(判据与 M1 `field_absent`/`evidence_zero` 同源)。
351
368
 
@@ -407,6 +424,7 @@ def _loc_missing_field(node_id, meta, content, ctx):
407
424
  return hits
408
425
 
409
426
 
427
+ # 生效条件:meta.get("verification_basis") 为空时回落 ctx.get("fm") 或 {} 的 verification_basis,若两者皆 _blank 返回 basis_absent 命中;非空但 str(basis) 不在 NF.VERIFICATION_BASIS 返回 basis_enum 命中;在枚举内时按 CC.classify_track 依 meta.get("layer")、meta.get("tags") 与 content 判赛道,若 CC.basis_licensed 为假返回 basis_licensed 命中;否则返回 []。
410
428
  def _loc_weak_source(node_id, meta, content, ctx):
411
429
  """验证基底缺失/越枚举/与赛道不相容(与 M1 `basis_licensed` 判据逐字同源)。"""
412
430
  fm = ctx.get("fm") or {}
@@ -436,6 +454,7 @@ def _loc_weak_source(node_id, meta, content, ctx):
436
454
  return []
437
455
 
438
456
 
457
+ # 生效条件:ctx.get("fm") 或 {} 中 code_ref 或 doc_ref 为非空 dict 且 RI.probe_ref(ref) 返回的 status 属于 _REF_DEAD_STATUSES(dangling 或 stale)时,返回对应 stale 命中(dangling 归因载体消失、stale 归因区间哈希不符);否则返回 []。
439
458
  def _loc_stale(node_id, meta, content, ctx):
440
459
  """**依赖存在性**——声明的载体(源文件)已不存在 / 已漂移(真正的适用边界越出)。
441
460
 
@@ -486,6 +505,7 @@ def _loc_stale(node_id, meta, content, ctx):
486
505
  return hits
487
506
 
488
507
 
508
+ # 生效条件:从 meta.get("condition_space") 或回落 ctx.get("fm") 的 condition_space(须为 dict)取 time_window,缺则取 meta.get("time_window"),若 NF.is_full_time_window(tw) 或 float(tw[1]) 抛 TypeError/ValueError/IndexError/KeyError 或 ctx.get("now") 为 None 或 hi>=float(now) 则返回 [];否则返回一条 severity="info" 的 observation_aged 命中。
489
509
  def _loc_observation_aged(node_id, meta, content, ctx):
490
510
  """时间窗已过——**这是观测时刻,不是失效声明**(观测面,不进告警面)。
491
511
 
@@ -526,6 +546,7 @@ def _loc_observation_aged(node_id, meta, content, ctx):
526
546
  snippet=_snippet(content, span))]
527
547
 
528
548
 
549
+ # 生效条件:h 取 str(meta.get("content_hash") or "").strip(),若 _blank(h) 则 h=NF.content_hash(content 或 "");对 ctx.get("peers") 或 [] 中 node_id 不等于本节点且 ph=str(p.get("hash") or "") 或回落 NF.content_hash(p.get("content") or "") 后非空且等于 h 的 peer 计入 matched;matched 非空时返回一条 dup 命中(span=[0,len(body)]),否则返回 []。
529
550
  def _loc_dup(node_id, meta, content, ctx):
530
551
  """同组同内容指纹(与 M1 `dup_hash_group` 同判据;D1 名 `dup`)。"""
531
552
  body = content if content is not None else ""
@@ -549,6 +570,7 @@ def _loc_dup(node_id, meta, content, ctx):
549
570
  snippet=_snippet(body, span))]
550
571
 
551
572
 
573
+ # 生效条件:对 ctx.get("peers") 或 [] 中每个非自身 peer,按 sentence_spans 对齐 content 与 peer content 的句子,当同一句位上去数字骨架相同(WL._skeleton(seg) 非空、长度 ≥ WL.MIN_SKELETON 且等于 peer 骨架)且 seg.strip() != ptext.strip() 的句子数达到 MIN_FLOW_SENTENCES 时,为这些句各返回一条 template_flow 命中;否则返回 []。
552
574
  def _loc_template_flow(node_id, meta, content, ctx):
553
575
  """同模板流水:与同组节点逐句「去数字骨架相同、字面不同」→ 指向那些句。
554
576
 
@@ -586,6 +608,7 @@ def _loc_template_flow(node_id, meta, content, ctx):
586
608
  return hits
587
609
 
588
610
 
611
+ # 生效条件:meta.get("content_hash") 非 _blank 且 content 非 None 且 h != NF.content_hash(content) 时,返回一条 hash_declared_vs_actual 的 contradiction 命中(cause 由 hash_mismatch_cause 依 meta/ctx.get("root")/ctx.get("path") 判);ctx.get("fm") 或 {} 的 id 非 _blank 且 str(fm_id) != str(node_id) 时额外返回一条 id_declared_vs_index 命中;两者皆不成立返回 []。
589
612
  def _loc_contradiction(node_id, meta, content, ctx):
590
613
  """**确定性**矛盾:声明与事实不符(指纹 / 标识)。
591
614
 
@@ -632,6 +655,7 @@ SEMANTIC_ONLY = {
632
655
  }
633
656
 
634
657
 
658
+ # 生效条件:始终返回 SEMANTIC_ONLY 中以 str(kind or "").strip() 为键查得的值,缺键时返回空串(空串表示可定位)。
635
659
  def blindspot_reason(kind) -> str:
636
660
  """D1 拒绝定位的类别 → 理由(空串表示可定位)。"""
637
661
  return SEMANTIC_ONLY.get(str(kind or "").strip(), "")
@@ -639,6 +663,7 @@ def blindspot_reason(kind) -> str:
639
663
 
640
664
  # ---------------------------- 主入口 ----------------------------
641
665
 
666
+ # 生效条件:items 中每项按 content=it.get("content") or ""、hash=str(it.get("hash") or "").strip() 或回落 NF.content_hash(content)、sk=WL.template_signature(content) or "" 预处理后,返回 {node_id: [同 hash 或同非空 sk 的其他 peer]};items 为空/None 返回空 dict。
642
667
  def build_peers(items) -> dict:
643
668
  """`[{node_id, content, hash?, …}]` → `{node_id: [peer, …]}`。
644
669
 
@@ -668,6 +693,7 @@ def build_peers(items) -> dict:
668
693
  return out
669
694
 
670
695
 
696
+ # 生效条件:对 issue_hint(None/str/dict 或其 list/tuple)逐 hint 分类,返回 (kinds, fields, blind):能 canonical_kind 到 ISSUE_KINDS 或 ADVISORY_KINDS 的入 kinds,blindspot_reason 非空的入 blind,其余非空 kind 与 field 入 fields;issue_hint 为 None 时 kinds/fields 为空集、blind 为空列表。
671
697
  def _norm_hint(issue_hint):
672
698
  """issue_hint → `(kinds, fields, blindspot)`。
673
699
 
@@ -699,6 +725,7 @@ def _norm_hint(issue_hint):
699
725
  return kinds, fields, blind
700
726
 
701
727
 
728
+ # 生效条件:当 meta 与 content 均非 None 时不读盘;否则 root 为 None 抛 ValueError,经 load_node 读不到节点返回 {"hits": [], "blindspot": blind+["节点 %s 不在索引"%node_id], "load": None};读到时补 meta/content/text/fm/path,按 kinds 非空时只跑 sorted(kinds)、否则 fields 非空或 issue_hint 为 None 时跑 sorted(ISSUE_KINDS)、否则 run=[] 执行 LOCATORS,对命中按 fields 过滤、补 line 并排序后返回 {"hits": hits, "blindspot": blind, "load": {...}}。
702
729
  def locate_ex(node_id, issue_hint=None, *, meta=None, content=None, fm=None, text=None,
703
730
  root=None, index=None, peers=None, now=None, path=None,
704
731
  field_layers=None) -> dict:
@@ -761,11 +788,13 @@ def locate_ex(node_id, issue_hint=None, *, meta=None, content=None, fm=None, tex
761
788
  "content_len": len(content or "")}}
762
789
 
763
790
 
791
+ # 生效条件:给定 node_id(必填)与可选 issue_hint 及 kw 后,直接返回 locate_ex(node_id, issue_hint, **kw) 结果的 "hits" 列表。
764
792
  def locate(node_id, issue_hint=None, **kw) -> list:
765
793
  """**D1 契约入口**:`locate(node_id, issue_hint) -> [{field, span, issue_kind, evidence}]`。"""
766
794
  return locate_ex(node_id, issue_hint, **kw)["hits"]
767
795
 
768
796
 
797
+ # 生效条件:给定 hits 与 key 后,返回 hits 中每个 h.get(key) 字符串化取值到出现次数的字典(键升序);hits 为 None/空时返回空 dict。
769
798
  def _counts(hits, key) -> dict:
770
799
  out = {}
771
800
  for h in hits or []:
@@ -774,6 +803,7 @@ def _counts(hits, key) -> dict:
774
803
  return dict(sorted(out.items()))
775
804
 
776
805
 
806
+ # 生效条件:items 显式给出时不读盘;items 为 None 时 root 为 None 抛 ValueError,否则按 node_ids 逐节点 load_node 组装 items(读不到的记入 missing);随后对每个 item 调 locate_ex 汇总 hits 或 clean,并返回含 nodes/hits/by_kind/by_field/by_rule/clean/missing 的 dict。
777
807
  def locate_many(node_ids=None, *, root=None, index=None, items=None,
778
808
  issue_hint=None, now=None) -> dict:
779
809
  """批量定位:**同一批内**互为对照(`dup` / `template_flow` 的组 = 本批)。
@@ -817,6 +847,7 @@ def locate_many(node_ids=None, *, root=None, index=None, items=None,
817
847
  "clean": clean, "missing": missing}
818
848
 
819
849
 
850
+ # 生效条件:从 pkg.get("entries") or [] 取条目并跳过 e.get("node_id") 为空者,root 非 None 时逐个 load_node 取正文,取不到时回落 e.get("excerpt") or "",组装 items 调 locate_many 后返回其结果并附加 bundle_id 与 entries 数量;pkg 为 None 时 entries 为空列表。
820
851
  def locate_package(pkg, *, root=None, index=None, issue_hint=None, now=None) -> dict:
821
852
  """M1 包 → 定位汇总(组的作用域 = 本包,与 M1 机械层同口径)。
822
853
 
@@ -844,6 +875,7 @@ def locate_package(pkg, *, root=None, index=None, issue_hint=None, now=None) ->
844
875
  return out
845
876
 
846
877
 
878
+ # 生效条件:给定 hits 后返回 total=len(hits or [])、去重 node_id 数、排序后的 node_ids、以及 by_kind/by_field/by_rule 计数;hits 为 None/空时 total=0、nodes=0、node_ids=[]。
847
879
  def summary(hits) -> dict:
848
880
  """命中汇总(审计留痕用)。"""
849
881
  nodes = sorted({str(h.get("node_id")) for h in hits or []})
@@ -852,6 +884,7 @@ def summary(hits) -> dict:
852
884
  "by_rule": _counts(hits, "rule")}
853
885
 
854
886
 
887
+ # 生效条件:给定 hits 后逐条生成 Markdown 行并返回表头加各行:field/line/snippet 取 h.get(...) or "—"(假值回落 "—"),span 为 None 时显示 "—" 否则 "起-止",evidence 取 (h.get("evidence") or "").replace("|","\\|")(假值回落空串)。
855
888
  def markdown_table(hits) -> str:
856
889
  """人工核对清单:每行一条命中,末列留空供核对者填判定(D1 验收抽样用)。"""
857
890
  head = ("| # | node_id | issue_kind | field | span | line | 片段 | evidence | 人工判定 |\n"
@@ -867,6 +900,7 @@ def markdown_table(hits) -> str:
867
900
  return head + "\n".join(rows)
868
901
 
869
902
 
903
+ # 生效条件:解析 argv(缺省 sys.argv)后,--root 为假值(含默认 os.environ.get("MDCG_ROOT") 为 None 或空串)时打印提示并返回 2;--node 追加列表为空时打印提示并返回 2;否则以 --root 与 --node 调 locate_many,并按 --json 或 --markdown 输出后返回 0,两者皆无则逐行打印命中与汇总后返回 0。
870
904
  def main(argv=None) -> int:
871
905
  ap = argparse.ArgumentParser(prog="python -m md_cg.mreview.locate",
872
906
  description="记忆评审 M3 · D1 字段级定位")