@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
@@ -0,0 +1,179 @@
1
+ # -*- coding: utf-8 -*-
2
+ """md_cg · 状态摘要协议(MCP 输出面一等公民)
3
+
4
+ 【为什么】验证态若只在 frontmatter / 索引里躺着,读面就得「先知道去哪查」——
5
+ 与纪律只存在于认知图时执行率近零同构。故把状态**推进每一次返回体**:
6
+ 人类与 agent 读面一致,默认开启。
7
+
8
+ 四符号(`HEAD_MARKS`):
9
+ ✓ 已验证 证据在位、依赖未动、时效内
10
+ △ 已修改 复核中(上游动过,复核进行态)
11
+ ! 异常 已过期(时效失效)或依赖目标悬空——**地基动了**
12
+ ? 存疑 依赖变动后被自动标记,待复核
13
+ (未验证补 `·`:不是四态之一,但读面需要一个「尚未验证」的可视锚点。)
14
+
15
+ 【怎么做】在 `_cg_call` **单一出口**统一挂 `status_head` 并升级既有 `hint`——
16
+ 29+ 个 op 分支一处不改,爆炸半径最小。注入仅对返回体中**实际出现的节点 id**
17
+ 计算(O(k),受 `k` 与 MAX_IDS 约束),不做全库扫描。
18
+
19
+ 【关闭】`MDCG_STATUS_HEAD=0` 回退旧 hint 文本(灰度/排障用)。
20
+
21
+ 零第三方依赖(D-005)。
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import os
26
+
27
+ from . import trust
28
+
29
+ #: 四符号(读面契约,勿随意改字形——agent 侧提示词按此对齐)
30
+ MARK_VERIFIED = "✓"
31
+ MARK_MODIFIED = "△"
32
+ MARK_ABNORMAL = "!"
33
+ MARK_DOUBTED = "?"
34
+ MARK_UNVERIFIED = "·"
35
+ HEAD_MARKS = (MARK_VERIFIED, MARK_MODIFIED, MARK_ABNORMAL, MARK_DOUBTED)
36
+
37
+ ENV = "MDCG_STATUS_HEAD"
38
+ #: 注入上限:单次返回体最多为这么多节点计算状态头(防大结果集拖慢出口)
39
+ MAX_IDS = 50
40
+
41
+
42
+ def enabled() -> bool:
43
+ """状态头开关(默认开启;`MDCG_STATUS_HEAD=0` 回退旧 hint 文本)。"""
44
+ v = (os.environ.get(ENV) or "").strip().lower()
45
+ return v not in ("0", "false", "no", "off")
46
+
47
+
48
+ def _mark(state: str, valid_kind: str, dangling: bool) -> str:
49
+ if dangling or valid_kind == "expired" or state == "expired":
50
+ return MARK_ABNORMAL
51
+ if state == "doubted":
52
+ return MARK_DOUBTED
53
+ if state == "rechecking":
54
+ return MARK_MODIFIED
55
+ if state == "verified":
56
+ return MARK_VERIFIED
57
+ return MARK_UNVERIFIED
58
+
59
+
60
+ def _label(mark: str) -> str:
61
+ return {MARK_VERIFIED: "已验证", MARK_MODIFIED: "已修改",
62
+ MARK_ABNORMAL: "异常", MARK_DOUBTED: "存疑",
63
+ MARK_UNVERIFIED: "未验证"}.get(mark, "未验证")
64
+
65
+
66
+ def render(cg, node_id: str, *, node: dict = None, index_entry: dict = None,
67
+ now: float = None) -> str:
68
+ """单条状态头字符串(附方式 / 时间 / 影响范围 / 待确认条件)。只读不抛。"""
69
+ try:
70
+ e = index_entry
71
+ if e is None:
72
+ e = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(node_id)
73
+ fm = (node or {}).get("frontmatter") if node else None
74
+ if fm is None:
75
+ fm = e or {}
76
+ state = trust.state_of(fm)
77
+ kind, _s, _e = trust.validity(fm, now=now)
78
+ deps = trust.as_deps(fm.get(trust.DEPS_FIELD))
79
+ known = set((getattr(cg, "index", None) or {}).get("nodes") or {})
80
+ dangling = [p for p in deps if p not in known]
81
+ mark = _mark(state, kind, bool(dangling))
82
+ bits = []
83
+ if dangling:
84
+ bits.append(f"依赖目标缺失:{','.join(dangling[:3])}")
85
+ if kind == "expired":
86
+ bits.append("时效已过")
87
+ elif kind == "not_yet":
88
+ bits.append("尚未生效")
89
+ method = (fm.get("verification_method") or fm.get("verification_basis")
90
+ or "")
91
+ if method and mark == MARK_VERIFIED:
92
+ bits.append(f"方式={str(method)[:40]}")
93
+ if mark == MARK_DOUBTED:
94
+ bits.append("上游变动,待复核")
95
+ if mark in (MARK_VERIFIED, MARK_MODIFIED) and fm.get("verified_at"):
96
+ try:
97
+ import time as _t
98
+ bits.append("验证于 " + _t.strftime(
99
+ "%Y-%m-%d", _t.localtime(float(fm["verified_at"]))))
100
+ except Exception: # noqa: BLE001
101
+ pass
102
+ return f"{mark} {_label(mark)}" + (f"({';'.join(bits)})" if bits else "")
103
+ except Exception as exc: # noqa: BLE001
104
+ return f"{MARK_UNVERIFIED} 未验证(状态头降级:{type(exc).__name__})"
105
+
106
+
107
+ #: 返回体中可能承载节点 id 的键(显式枚举,避免把任意字符串误当 id)
108
+ _ID_KEYS = ("node_id", "id", "nid")
109
+
110
+
111
+ def _collect_ids(obj, acc: list, depth: int = 0) -> None:
112
+ """递归收集候选节点 id(深度/数量双上限;找不到就不找,绝不猜)。"""
113
+ if len(acc) >= MAX_IDS or depth > 4:
114
+ return
115
+ if isinstance(obj, dict):
116
+ for k, v in obj.items():
117
+ if k in _ID_KEYS and isinstance(v, str) and v:
118
+ acc.append(v)
119
+ elif isinstance(v, (dict, list)):
120
+ _collect_ids(v, acc, depth + 1)
121
+ if len(acc) >= MAX_IDS:
122
+ return
123
+ elif isinstance(obj, list):
124
+ for it in obj:
125
+ _collect_ids(it, acc, depth + 1)
126
+ if len(acc) >= MAX_IDS:
127
+ return
128
+
129
+
130
+ def attach_heads(cg, out: dict, *, now: float = None) -> dict:
131
+ """扫描返回体中的节点 id,批量挂 `status_head`(O(k),幂等)。"""
132
+ if not isinstance(out, dict):
133
+ return out
134
+ ids, seen = [], set()
135
+ _collect_ids(out, ids)
136
+ known = (getattr(cg, "index", None) or {}).get("nodes") or {}
137
+ heads = {}
138
+ for nid in ids:
139
+ if nid in seen or nid not in known:
140
+ continue
141
+ seen.add(nid)
142
+ heads[nid] = render(cg, nid, index_entry=known.get(nid), now=now)
143
+ if not heads:
144
+ return out
145
+ out["status_heads"] = heads
146
+ out["status_head"] = (next(iter(heads.values())) if len(heads) == 1
147
+ else f"{len(heads)} 节点(见 status_heads)")
148
+ return out
149
+
150
+
151
+ def annotate(cg, out: dict, *, now: float = None) -> dict:
152
+ """**单出口**注入口:挂状态头 + 把既有 `hint` 升级为状态头格式。
153
+
154
+ 幂等:已注入过(`status_head` 在返回体中)则不再重复改写 hint。
155
+ """
156
+ if not enabled() or not isinstance(out, dict):
157
+ return out
158
+ if "status_head" in out:
159
+ return out
160
+ before_hint = out.get("hint")
161
+ attach_heads(cg, out, now=now)
162
+ head = out.get("status_head")
163
+ if head and before_hint:
164
+ out["hint"] = f"{head} · {before_hint}"
165
+ elif head:
166
+ out["hint"] = head
167
+ return out
168
+
169
+
170
+ def catalog() -> dict:
171
+ """自描述(供 MCP catalog / 人工核对)。"""
172
+ return {
173
+ "layer": "状态摘要协议",
174
+ "marks": {"verified": MARK_VERIFIED, "modified": MARK_MODIFIED,
175
+ "abnormal": MARK_ABNORMAL, "doubted": MARK_DOUBTED,
176
+ "unverified": MARK_UNVERIFIED},
177
+ "env": ENV, "default_on": True, "max_ids": MAX_IDS,
178
+ "note": "统一在 _cg_call 单出口注入;hint 同步升级(Q3 默认开启)",
179
+ }
package/md_cg/stg.py CHANGED
@@ -14,6 +14,8 @@
14
14
  """
15
15
  from __future__ import annotations
16
16
 
17
+ from . import trust
18
+
17
19
  TIME_RELATIONS = ("before", "after", "equals", "contains", "during", "overlaps")
18
20
  SPACE_RELATIONS = ("left_of", "right_of", "above", "below",
19
21
  "contains", "inside", "overlaps")
@@ -23,12 +25,26 @@ PLACEHOLDER_LOCKED = "[密文·预览已脱敏]"
23
25
  PLACEHOLDER_DENIED = "[无权限·预览已脱敏]"
24
26
 
25
27
 
26
- def _interval(fm):
27
- """节点时间区间:优先 temporal(事件时刻),回退 condition_space.time_window(观测窗)。
28
+ # 生效条件:time_axis 经 trust.time_axis_of 归一(None → observed,非法轴如 believed 抛 ValueError);observed 轴下 fm 的 temporal 非 None 且 float(t) 可成功时返回 (float(t), float(t)),temporal 缺失/为 None/转换抛 TypeError 或 ValueError 时回退到 fm["condition_space"](假值按 {} 处理)的 time_window,仅当其为长度 2 的 list/tuple 且两元素可 float 时返回 (float(tw[0]), float(tw[1])),否则返回 None;effective 轴下取 trust.time_window_of(fm, "effective") 的两端点,两端均可解析才返回 (float(s), float(e)),任一端缺失或不可解析返回 None。
29
+ def _interval(fm, time_axis="observed"):
30
+ """节点时间区间;轴语义与 `trust.time_window_of` **同源**(唯一口径,不新写解析)。
31
+
32
+ · `observed`(默认,与旧行为**逐位一致**):优先 `temporal`(事件时刻),
33
+ 回退 `condition_space.time_window`(观测窗)。
34
+ 注意 add() 在调用方未给 time_window 时会以「写入时刻」自动填充;
35
+ 若把它当事件时间,两条不同时刻的节点会得到假的重叠关系,故 temporal 优先。
36
+ · `effective`(效力轴):`effective_from` / `effective_until` 及其别名
37
+ (`trust.FROM_ALIASES` / `UNTIL_ALIASES`,规范键优先、别名回落)。
38
+ 区间语义要求**两端齐备**——单侧缺失/不可解析 → `None`(不可判定,
39
+ 不猜测边界:给半开区间补 `±inf` 会让 `time_relation` 报出假的 contains/during)。
28
40
 
29
- 注意 add() 在调用方未给 time_window 时会以「写入时刻」自动填充;
30
- 若把它当事件时间,两条不同时刻的节点会得到假的重叠关系,故 temporal 优先。
41
+ `believed_at` **永不参与任何轴**(同 `trust.BELIEVED_FIELD` 的隔离纪律)。
31
42
  """
43
+ if trust.time_axis_of(time_axis) == "effective":
44
+ s, e = trust.time_window_of(fm, "effective")
45
+ if s is None or e is None:
46
+ return None
47
+ return (float(s), float(e))
32
48
  t = fm.get("temporal")
33
49
  if t is not None:
34
50
  try:
@@ -45,6 +61,7 @@ def _interval(fm):
45
61
  return None
46
62
 
47
63
 
64
+ # 生效条件:fm["spatial"](假值按 {} 处理)为 dict 且其 bbox 是长度 4 的 list/tuple 且四元素可 float 时返回浮点四元组,spatial 非 dict、bbox 非长度 4 序列或元素转换抛 TypeError/ValueError 时返回 None。
48
65
  def _bbox(fm):
49
66
  sp = fm.get("spatial") or {}
50
67
  bb = sp.get("bbox") if isinstance(sp, dict) else None
@@ -56,6 +73,7 @@ def _bbox(fm):
56
73
  return None
57
74
 
58
75
 
76
+ # 生效条件:a、b 均非 None 且各可解包为两个元素时,按 a 相对 b 依次返回 equals(两端全等)、before(a2<b1)、after(a1>b2)、contains(a1<=b1 且 a2>=b2)、during(a1>=b1 且 a2<=b2)或 overlaps(其余);a 或 b 为 None 时返回 None;
59
77
  def time_relation(a, b):
60
78
  """Allen 区间代数的 6 个基本态。"""
61
79
  if a is None or b is None:
@@ -74,6 +92,7 @@ def time_relation(a, b):
74
92
  return "overlaps"
75
93
 
76
94
 
95
+ # 生效条件:a 或 b 为 None 时返回 None,否则按 a=(ax1,ay1,ax2,ay2)、b=(bx1,by1,bx2,by2) 依序判定 ax2<=bx1→"left_of"、ax1>=bx2→"right_of"、ay2<=by1→"above"、ay1>=by2→"below"、四边全含→"contains"、四边全被含→"inside",全部不满足时返回 "overlaps"。
77
96
  def space_relation(a, b):
78
97
  """RCC-8 简化的 7 个空间态(图像坐标:y 向下为正)。"""
79
98
  if a is None or b is None:
@@ -97,6 +116,7 @@ def space_relation(a, b):
97
116
 
98
117
  # ---------- 查询实现 ----------
99
118
 
119
+ # 生效条件:cg.get(node_id) 为真值(非 None、非空映射)时返回 {"id": node_id, "frontmatter": n.get("frontmatter") or {}(假值回落 {}), "content": n.get("content") or ""(假值回落 "")},cg.get(node_id) 为假值时返回 None。
100
120
  def _node(cg, node_id):
101
121
  n = cg.get(node_id)
102
122
  if not n:
@@ -105,6 +125,7 @@ def _node(cg, node_id):
105
125
  "content": n.get("content") or ""}
106
126
 
107
127
 
128
+ # 生效条件:cg.index["nodes"] 存在时按 list(...items())[:max_scan] 遍历,layer 为真值时仅保留 e.get("layer")==layer 的条目(layer 为假值不筛层),e 含 "temporal" 或 "spatial" 键时直接以快照字段构造 frontmatter、否则调用 cg._read(e) 且在 fm 为 None 时跳过;返回 out 列表(max_scan=None 切片取全部,0 时为空);
108
129
  def _scan(cg, layer=None, max_scan=5000):
109
130
  """遍历节点:时空字段直接读索引快照(不读文件,O(1)/节点)。
110
131
 
@@ -116,7 +137,14 @@ def _scan(cg, layer=None, max_scan=5000):
116
137
  if layer and e.get("layer") != layer:
117
138
  continue
118
139
  if "temporal" in e or "spatial" in e:
140
+ # 效力轴四键必须一并从快照带出:否则 `time_axis="effective"` 在快照
141
+ # 路径上永远「不可判定」(静默全空,比报错更难查)。旧索引快照无这些
142
+ # 键时 `.get` 得 None → 不可判定,是本轴**如实降级**而非误判。
119
143
  fm = {"temporal": e.get("temporal"), "spatial": e.get("spatial"),
144
+ trust.EFFECTIVE_FROM_FIELD: e.get(trust.EFFECTIVE_FROM_FIELD),
145
+ trust.EFFECTIVE_UNTIL_FIELD: e.get(trust.EFFECTIVE_UNTIL_FIELD),
146
+ trust.FROM_FIELD: e.get(trust.FROM_FIELD),
147
+ trust.UNTIL_FIELD: e.get(trust.UNTIL_FIELD),
120
148
  "condition_space": {"time_window": e.get("time_window")}}
121
149
  else:
122
150
  fm, _content = cg._read(e)
@@ -127,6 +155,7 @@ def _scan(cg, layer=None, max_scan=5000):
127
155
  return out
128
156
 
129
157
 
158
+ # 生效条件:cg.index["nodes"].get(node_id) 缺失或为假值时返回 "";否则 cg._readable 可调用且对其返回假值或抛异常时返回 PLACEHOLDER_DENIED;cg._read(e) 的 frontmatter 为 None 时返回 "";content 非密文时返回 content[:n](n 默认 200);content 为密文时,cg._open_content 可调用且取到非 None 且非密文的 opened 才返回 opened[:n],opened 为 None、抛异常或仍为密文时返回 PLACEHOLDER_LOCKED。
130
159
  def _preview(cg, node_id, n=200):
131
160
  """按需读单个节点正文做预览(只发生在最终返回的条目上)。
132
161
 
@@ -165,13 +194,15 @@ def _preview(cg, node_id, n=200):
165
194
  return opened[:n]
166
195
 
167
196
 
168
- def relation(cg, a_id, b_id):
169
- """两节点间的时空关系(a 相对 b)。"""
197
+ # 生效条件:cg 上 _node(cg, a_id) 与 _node(cg, b_id) 均返回真值时返回含 a_id/b_id、时间关系、空间关系和 time_known/space_known 的 dict(两侧时间区间均按 time_axis 轴取,见 _interval;time_axis 非法经 trust.time_axis_of 抛 ValueError);任一 _node 结果为假值时返回 {"error":"node_not_found","missing":[...]};
198
+ def relation(cg, a_id, b_id, time_axis="observed"):
199
+ """两节点间的时空关系(a 相对 b)。`time_axis` 决定时间区间取哪条轴(见 `_interval`)。"""
170
200
  na, nb = _node(cg, a_id), _node(cg, b_id)
171
201
  if not na or not nb:
172
202
  return {"error": "node_not_found",
173
203
  "missing": [x for x, n in ((a_id, na), (b_id, nb)) if not n]}
174
- ia, ib = _interval(na["frontmatter"]), _interval(nb["frontmatter"])
204
+ ia, ib = (_interval(na["frontmatter"], time_axis),
205
+ _interval(nb["frontmatter"], time_axis))
175
206
  ba, bb = _bbox(na["frontmatter"]), _bbox(nb["frontmatter"])
176
207
  return {"a": a_id, "b": b_id,
177
208
  "time": {"relation": time_relation(ia, ib), "a": ia, "b": ib},
@@ -180,23 +211,27 @@ def relation(cg, a_id, b_id):
180
211
  "space_known": ba is not None and bb is not None}}
181
212
 
182
213
 
183
- def timeline(cg, layer=None, limit=50, desc=True, max_scan=5000):
184
- """按时间排序的节点列表。"""
214
+ # 生效条件:以 _scan(cg,layer=layer,max_scan=max_scan) 为范围,_interval(n["frontmatter"], time_axis) 为 None 的节点被跳过,其余按 (start,end) 以 reverse=bool(desc) 排序,返回 count=全部命中数、limit=传入 limit、items 为排序后前 limit 项(limit=0 时为空列表)且每项附 _preview(cg,id)(time_axis 缺省 observed,与旧行为逐位一致;非法轴抛 ValueError)。
215
+ def timeline(cg, layer=None, limit=50, desc=True, max_scan=5000,
216
+ time_axis="observed"):
217
+ """按时间排序的节点列表。`time_axis` 决定排序依据的时间区间(见 `_interval`)。"""
185
218
  items = []
186
219
  for n in _scan(cg, layer=layer, max_scan=max_scan):
187
- iv = _interval(n["frontmatter"])
220
+ iv = _interval(n["frontmatter"], time_axis)
188
221
  if iv is None:
189
222
  continue
190
223
  items.append((iv[0], iv[1], n["id"], n["layer"]))
191
- items.sort(key=lambda x: (x[0], x[1]), reverse=bool(desc))
224
+ items.sort(key=lambda x: (x[0], x[1], x[2]), reverse=bool(desc))
192
225
  return {"count": len(items), "limit": limit,
193
226
  "items": [{"id": i, "layer": l, "start": s, "end": e,
194
227
  "preview": _preview(cg, i)}
195
228
  for s, e, i, l in items[:limit]]}
196
229
 
197
230
 
198
- def anchors(cg, time_window=None, bbox=None, layer=None, limit=50, max_scan=5000):
199
- """落在给定时间窗 / 空间范围内的节点。"""
231
+ # 生效条件:time_window 为长度 2 的 list/tuple 时 q_t=(float(time_window[0]),float(time_window[1]))(元素不可转 float 会直接抛异常,源码未捕获),bbox 为长度 4 的 list/tuple 时同理构造 q_b;q_t 与 q_b 均为 None 时返回 {"error":"need_time_window_or_bbox"};否则扫描节点、每节点时间区间按 _interval(fm, time_axis) 取(time_axis 缺省 observed 与旧行为逐位一致,非法轴抛 ValueError),并要求时间关系在 during/contains/overlaps/equals、空间关系在 inside/contains/overlaps/equals(提供查询侧才检查),返回 hits[:limit](limit=None 取全部,0/False 取空);
232
+ def anchors(cg, time_window=None, bbox=None, layer=None, limit=50, max_scan=5000,
233
+ time_axis="observed"):
234
+ """落在给定时间窗 / 空间范围内的节点。`time_axis` 决定候选时间区间(见 `_interval`)。"""
200
235
  q_t = None
201
236
  if isinstance(time_window, (list, tuple)) and len(time_window) == 2:
202
237
  q_t = (float(time_window[0]), float(time_window[1]))
@@ -209,7 +244,7 @@ def anchors(cg, time_window=None, bbox=None, layer=None, limit=50, max_scan=5000
209
244
  hits = []
210
245
  for n in _scan(cg, layer=layer, max_scan=max_scan):
211
246
  fm = n["frontmatter"]
212
- iv, bb = _interval(fm), _bbox(fm)
247
+ iv, bb = _interval(fm, time_axis), _bbox(fm)
213
248
  t_rel = time_relation(iv, q_t) if (q_t and iv) else None
214
249
  s_rel = space_relation(bb, q_b) if (q_b and bb) else None
215
250
  if q_t and t_rel not in ("during", "contains", "overlaps", "equals"):
@@ -224,14 +259,20 @@ def anchors(cg, time_window=None, bbox=None, layer=None, limit=50, max_scan=5000
224
259
  "items": hits[:limit]}
225
260
 
226
261
 
227
- def consistency(cg, layer=None, limit=50, max_scan=5000):
228
- """时空字段自洽性检查:非法 bbox / 时间倒置 / 窗口与时刻冲突。"""
262
+ # 生效条件:遍历 _scan(cg,layer=layer,max_scan=max_scan) 每条 frontmatter,bb 非 None 且不满足 bb[0]<=bb[2] and bb[1]<=bb[3] 记 invalid_bbox、iv(由 _interval(fm, time_axis) 取,time_axis 缺省 observed 与旧行为逐位一致、非法轴抛 ValueError)非 None 且 iv[0]>iv[1] 记 inverted_time_window、temporal 非 None 且 time_window 为长度 2 的 list/tuple 且 float 比较成功却不满足 tw[0]<=t<=tw[1] 记 temporal_outside_window(该检查恒按观察轴内部口径、不随 time_axis 漂移;转换抛 TypeError/ValueError 则忽略),返回 scanned 计数、issues 总数与 issues[:limit](limit 默认 50)。
263
+ def consistency(cg, layer=None, limit=50, max_scan=5000, time_axis="observed"):
264
+ """时空字段自洽性检查:非法 bbox / 时间倒置 / 窗口与时刻冲突。
265
+
266
+ `time_axis` 只决定「时间倒置」按哪条轴判;`temporal_outside_window`
267
+ 恒按**观察轴内部**口径(temporal 与 time_window 的关系)——那是该 issue 的
268
+ 定义本身,换轴会让它变成另一件事(不随参数漂移)。
269
+ """
229
270
  issues = []
230
271
  scanned = 0
231
272
  for n in _scan(cg, layer=layer, max_scan=max_scan):
232
273
  scanned += 1
233
274
  fm = n["frontmatter"]
234
- bb, iv = _bbox(fm), _interval(fm)
275
+ bb, iv = _bbox(fm), _interval(fm, time_axis)
235
276
  if bb and not (bb[0] <= bb[2] and bb[1] <= bb[3]):
236
277
  issues.append({"id": n["id"], "issue": "invalid_bbox", "bbox": bb})
237
278
  if iv and iv[0] > iv[1]:
@@ -247,4 +288,4 @@ def consistency(cg, layer=None, limit=50, max_scan=5000):
247
288
  except (TypeError, ValueError):
248
289
  pass
249
290
  return {"scanned": scanned, "issues": len(issues), "limit": limit,
250
- "items": issues[:limit]}
291
+ "items": issues[:limit]}
package/md_cg/subgraph.py CHANGED
@@ -40,6 +40,7 @@ SEP_MAX_NODES = 400 # 单次扫描节点上限(防 O(N²) 读盘爆
40
40
  SEP_MAX_PAIRS = 50 # 单次返回/落库的候选对上限
41
41
 
42
42
 
43
+ # 生效条件:fm 为假值或 fm.get("subgraph") 取值为假(None/[]/{}/空串)时直接返回 {'nodes': [], 'edges': []};subgraph 为 list/tuple 时其元素作节点、边记空列表,为 dict 时节点取 sg.get("nodes")(缺键或假值回落 [])、边取 sg.get("edges")(同样回落),为其他类型仍返回空;节点项为 dict 时取 n.get("id")、否则取元素本身,nid 为 None 跳过,仅当 str(nid).strip() 非空且未出现过才追加;边只保留 isinstance(e, dict) 的项;
43
44
  def declared(fm):
44
45
  """规范化 `frontmatter.subgraph` → `{"nodes": [id...], "edges": [edge...]}`。
45
46
 
@@ -68,6 +69,7 @@ def declared(fm):
68
69
  return {"nodes": nodes, "edges": [e for e in raw_edges if isinstance(e, dict)]}
69
70
 
70
71
 
72
+ # 生效条件:当 getattr(cg,"index",None) 的 nodes 字典中存在 nid(entry 非 None)且 "subgraph" in entry 时,返回 {'id': nid, 'subgraph': entry.get("subgraph"), 'edges': entry.get("edges") or []}(subgraph 值为 None 也照样返回);否则改走 cg.get(nid),返回 (node or {}).get("frontmatter") or {},节点不存在或 frontmatter 为假值时得空 dict;
71
73
  def _fm(cg, nid):
72
74
  """取节点 frontmatter:优先索引快照(免 IO),快照缺字段时回退读文件。"""
73
75
  entry = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(nid)
@@ -89,6 +91,7 @@ def _fm(cg, nid):
89
91
  SRC_REL_MAP = {"hierarchical": "contains"}
90
92
 
91
93
 
94
+ # 生效条件:rel 先经 str(rel or "").strip().lower() 归一(None/空串得 ""),返回 {'target': str(tgt), 'relation_type': SRC_REL_MAP.get(rel, rel), 'confidence': confidence, 'verified': verified}——归一后的 rel 命中模块常量 SRC_REL_MAP 时用映射值、未命中(含 rel 为空串)时原样保留,confidence/verified 直接透传默认 1.0/0 的实参不做校验;
92
95
  def normalize_edge(tgt, rel, confidence=1.0, verified=0):
93
96
  """源库边 → md_cg 本地边(迁移器专用)。做两件事:
94
97
 
@@ -102,6 +105,7 @@ def normalize_edge(tgt, rel, confidence=1.0, verified=0):
102
105
  "confidence": confidence, "verified": verified}
103
106
 
104
107
 
108
+ # 生效条件:cg 的 `_subgraph_children` 不为 None 时原样返回该缓存字典,否则遍历 cg.index["nodes"] 的键,用 declared(fm)["nodes"] 收子节点、并按边 rel 为 part_of/hierarchical(target 记为父、当前 pid 记为子)或 parent_of/contains(当前 pid 记为父、target 记为子,target 为 None 则该边跳过)补全 parent_id→[child_id] 索引,回写 cg._subgraph_children 后返回 idx;
105
109
  def children_index(cg):
106
110
  """全局正查:parent_id → [child_id...](声明式 ∪ 边式,一次 O(N) 后缓存)。"""
107
111
  idx = getattr(cg, "_subgraph_children", None)
@@ -140,11 +144,13 @@ def children_index(cg):
140
144
  return idx
141
145
 
142
146
 
147
+ # 生效条件:返回 list(children_index(cg).get(nid) or []),即 nid 在索引中无键或对应值为空(None/空列表)时得空列表,否则返回其子 id 的浅拷贝列表;
143
148
  def children(cg, nid):
144
149
  """直接子节点 id 列表:`subgraph.nodes` 声明 ∪ 边式父子关系。"""
145
150
  return list(children_index(cg).get(nid) or [])
146
151
 
147
152
 
153
+ # 生效条件:cg 的 `_subgraph_parents` 不为 None 时原样返回该缓存字典,否则遍历 cg.index["nodes"] 的键,用 declared(fm)["nodes"] 把 pid 记为每个子节点的父、并按边 rel 为 part_of/hierarchical(target 记为 pid 的父)或 parent_of/contains(pid 记为 target 的父,target 为 None 则该边跳过)补全 child_id→[parent_id] 索引,回写 cg._subgraph_parents 后返回 idx;
148
154
  def parents_index(cg):
149
155
  """全局反查:child_id → [parent_id...](供 children/validate 复用,一次 O(N))。"""
150
156
  idx = getattr(cg, "_subgraph_parents", None)
@@ -183,6 +189,7 @@ def parents_index(cg):
183
189
  return idx
184
190
 
185
191
 
192
+ # 生效条件:无条件执行——把 cg 的 _subgraph_parents 与 _subgraph_children 依次 setattr 为 None(setattr 抛异常被 except 吞掉后继续下一个),无返回值,与 cg 是否已建有缓存无关;
186
193
  def invalidate_cache(cg):
187
194
  """写入/删除节点后调用,丢弃父子正查/反查缓存。"""
188
195
  for attr in ("_subgraph_parents", "_subgraph_children"):
@@ -192,12 +199,14 @@ def invalidate_cache(cg):
192
199
  pass
193
200
 
194
201
 
202
+ # 生效条件:取 parents_index(cg).get(nid) or [],列表非空时返回其首元素,键缺失或值为空/假(None、[])时返回 None;多父时只返回第一个,不在此处报歧义;
195
203
  def parent_of(cg, nid):
196
204
  """唯一父(多父时返回第一个并置 `ambiguous` 标记由 validate 报出)。"""
197
205
  ps = parents_index(cg).get(nid) or []
198
206
  return ps[0] if ps else None
199
207
 
200
208
 
209
+ # 生效条件:max_depth 非 None 时先 max(0, min(int(max_depth), MAX_DEPTH_HARD)) 收敛;以 nid 为根迭代 DFS,已入 seen 的节点跳过,len(seen) >= max_nodes(max_nodes 传 0 时首次循环即成立)置 truncated=True 并 break,depth >= max_depth 且 children 非空时置 truncated=True 并 continue,否则对 children(cg,cur) 逆序、对未出现过的 (ch,cur) 追加 part_of 边并压栈;返回 {'root','nodes','paths','edges','n_nodes','n_edges','truncated'};
201
210
  def expand(cg, nid, max_depth=None, max_nodes=MAX_NODES_DEFAULT):
202
211
  """递归展开子树(迭代 DFS,防递归深度爆栈)。
203
212
 
@@ -237,6 +246,7 @@ def expand(cg, nid, max_depth=None, max_nodes=MAX_NODES_DEFAULT):
237
246
  "truncated": truncated}
238
247
 
239
248
 
249
+ # 生效条件:先调用 expand(cg, nid, max_depth=max_depth, max_nodes=max_nodes),对其中每条边复制一份并追加 source/target 互换、relation_type="parent_of"、confidence/verified 沿用原边 .get(…,1.0/0) 的反向边;返回 root=nid、nodes=ex["nodes"]、paths=ex["paths"]、n_nodes=ex["n_nodes"]、n_edges=双边后条数、truncated=ex["truncated"];
240
250
  def flatten(cg, nid, max_depth=None, max_nodes=MAX_NODES_DEFAULT):
241
251
  """把嵌套子图摊平成「节点 + 边」,父子生成**对称双边**。
242
252
 
@@ -257,6 +267,7 @@ def flatten(cg, nid, max_depth=None, max_nodes=MAX_NODES_DEFAULT):
257
267
  "truncated": ex["truncated"]}
258
268
 
259
269
 
270
+ # 生效条件:known 取 cg.index 的 nodes 键集合,仅当 parents_index(cg).get(nid) or [] 为空(键缺失或空列表)的 nid 才视为树根,返回其排序后的 id 列表;
260
271
  def roots(cg):
261
272
  """无父节点(树根)的 id 列表。"""
262
273
  known = set(((getattr(cg, "index", None) or {}).get("nodes") or {}).keys())
@@ -264,6 +275,7 @@ def roots(cg):
264
275
  return sorted(nid for nid in known if not (pmap.get(nid) or []))
265
276
 
266
277
 
278
+ # 生效条件:known 取 cg.index 的 nodes 键列表,max_scan 为真值时截断为 known[:int(max_scan)];逐 pid 遍历 declared(_fm(cg,pid))["nodes"]:ch==pid 记 self_loop 并 continue(不再进入悬空/多父判断),ch 不在 known_set 记 dangling_child,随后 parent 中已有 ch 且 prev != pid 记 multi_parent、否则写 parent[ch]=pid;再沿 parent 指针上溯,遇本路径已访问节点记 cycle;_issue 仅在 len(issues) < limit*4 时追加(limit 为 0 时 0<0 不成立,不追加任何 issue);返回 scanned、issues=len(issues)、items=issues[:limit]、truncated=len(issues)>limit;
267
279
  def validate(cg, limit=50, max_scan=None):
268
280
  """树一致性校验:自环 / 悬空子节点 / 多父 / 环。
269
281
 
@@ -276,6 +288,7 @@ def validate(cg, limit=50, max_scan=None):
276
288
  known_set = set(known)
277
289
  parent, issues = {}, []
278
290
 
291
+ # 生效条件:仅当 len(issues) < limit*4 时把 kw 追加进 issues(限流防报告膨胀);无返回值,只改外层 issues;
279
292
  def _issue(**kw):
280
293
  if len(issues) < limit * 4:
281
294
  issues.append(kw)
@@ -320,6 +333,7 @@ def validate(cg, limit=50, max_scan=None):
320
333
  # · 边携带 reason + 判定分量,供后续重构/审计追溯;
321
334
  # · 条件**相同**的相似对**不是**分离候选(那是重复,该走 MERGE)。
322
335
 
336
+ # 生效条件:cg.get(nid) 为假值(None/{})时返回 None;否则取 fm=node.get("frontmatter") or {}、content=node.get("content") or "",返回 {'grams': bigrams(forgetting.payload(content)), 'pos','neg': consistency.condition_terms(fm, content) 的结果, 'layer': fm.get("layer")}——grams 为空也照常返回该字典,不返回 None;
323
337
  def _node_terms_and_grams(cg, nid):
324
338
  """读取节点并返回 (payload 二元组, 正条件词面, 负条件词面);不可读返回 None。"""
325
339
  from . import consistency, forgetting
@@ -334,6 +348,7 @@ def _node_terms_and_grams(cg, nid):
334
348
  return {"grams": grams, "pos": pos, "neg": neg, "layer": fm.get("layer")}
335
349
 
336
350
 
351
+ # 生效条件:pool 初取 cg.index 的 nodes 键,layer 为假值(None/"")时不按层过滤、否则只留 e.get("layer")==layer 的 nid,ids 为真值时只留 str(x) 属于该集合的 nid;pool 排序后 truncated=len(pool)>int(max_nodes)、并截为前 int(max_nodes) 个;只有 _node_terms_and_grams 返回非 None 且 grams 非空的节点进 cache;对 cache 键两两比较——无公共 bigram 跳过、jaccard 小于 float(min_jaccard) 跳过、condition_distinct 的 distinct 为假或 overlap > float(max_cond_overlap) 跳过,其余成候选并按 (-jaccard,a,b) 排序;返回 scanned、compared、candidates=out[:int(limit)]、total_candidates、truncated、note;
337
352
  def separation_pairs(cg, layer=None, ids=None, max_nodes=SEP_MAX_NODES,
338
353
  min_jaccard=SEP_MIN_JACCARD,
339
354
  max_cond_overlap=SEP_MAX_COND_OVERLAP,
@@ -393,6 +408,7 @@ def separation_pairs(cg, layer=None, ids=None, max_nodes=SEP_MAX_NODES,
393
408
  "note": "只读候选;apply=True 才写分离边"}
394
409
 
395
410
 
411
+ # 生效条件:batch 为假值(None 或空串)时回落到 time.strftime("%Y%m%d-%H%M%S");按 (a,b)、(b,a) 两向处理:cg.get(src) 为假值跳过,dst 已出现在 consistency.separation_targets(fm) 中跳过,否则往 fm["edges"] 追加 distinct_from 边、向 fm["pattern_separated_from"] 追加 dst,按 os.path.join(cg.root, e.get("path") or f"{src}.md") 调 cg._write_node 写盘并追加 MAINTAIN_LOG 行,src 记入 written;返回 {'a','b','written','batch'};
396
412
  def mark_separated(cg, a, b, reason="", actor="maintain", batch=None):
397
413
  """写入 (a↔b) 对称 `distinct_from` 边(幂等:已存在则不重复写)。"""
398
414
  from . import consistency
@@ -427,6 +443,7 @@ def mark_separated(cg, a, b, reason="", actor="maintain", batch=None):
427
443
  return {"a": a, "b": b, "written": written, "batch": batch}
428
444
 
429
445
 
446
+ # 生效条件:pairs 为 None 时调 separation_pairs 取 candidates 与 meta(scanned/compared/truncated/total_candidates),否则 cands=list(pairs) 且 meta 中 scanned/compared 记 0、truncated 记 False、total_candidates=len(cands);apply 为真值时先生成统一 batch=time.strftime(...),逐个候选调 mark_separated(..., batch=batch),written 非空且 cg 有 rebuild_index 时调 cg.rebuild_index()、否则调 invalidate_cache(cg),apply 为假值时不写盘;返回 ok、action='separate'、dry_run=not apply、layer、candidates、written、written_count、elapsed_ms、log 与 meta、note;
430
447
  def separate_run(cg, layer=None, pairs=None, apply=False, ids=None,
431
448
  min_jaccard=SEP_MIN_JACCARD, limit=SEP_MAX_PAIRS,
432
449
  actor="maintain"):
@@ -487,6 +504,7 @@ RECON_COMMON_SHARE = 0.5 # 共同条件判定:≥ 半数锚点共享
487
504
  RECON_SCENE_PREFIX = "scene_"
488
505
 
489
506
 
507
+ # 生效条件:clues 为 None 时返回 [];clues 为 str 时包装成单元素列表;逐项取 str(c or "").strip().lower(),空串跳过,整串去重入 out,再按 re.split(r"[\s,,、;;/|]+") 切词、去重后追加;返回 out;
490
508
  def _clue_terms(clues):
491
509
  """线索归一化:字符串 / 列表 → 去重词面(保留整串 + 切分后的词)。"""
492
510
  if clues is None:
@@ -507,6 +525,7 @@ def _clue_terms(clues):
507
525
  return out
508
526
 
509
527
 
528
+ # 生效条件:term_sets 为假值(None/[])时返回 [];否则对每个集合的 set(s or ()) 统计词面出现次数,need = max(2, int(len(term_sets)*RECON_COMMON_SHARE) 向上取整),返回出现次数 >= need 的词面排序列表(阈值取自模块常量 RECON_COMMON_SHARE);
510
529
  def _shared_terms(term_sets):
511
530
  """出现在 ≥ RECON_COMMON_SHARE 比例集合中的词面(且至少 2 个集合共享)。"""
512
531
  if not term_sets:
@@ -519,6 +538,7 @@ def _shared_terms(term_sets):
519
538
  return sorted(t for t, c in cnt.items() if c >= need)
520
539
 
521
540
 
541
+ # 生效条件:按 term_sets 原顺序遍历每个集合的 sorted(s or ()),首次出现的词面追加到 seen,返回 seen[:limit](limit 默认 99,传 0 时切片为空列表,无 or 回落);
522
542
  def _uniq_terms(term_sets, limit=99):
523
543
  seen = []
524
544
  for s in term_sets:
@@ -528,6 +548,7 @@ def _uniq_terms(term_sets, limit=99):
528
548
  return seen[:limit]
529
549
 
530
550
 
551
+ # 生效条件:bigrams(" ".join(clue_terms)) 为空时返回 [];否则遍历 pool,cg.get(nid) 为假值、或 forgetting.payload 后 bigrams 为空、或与 clue_grams 无交集时跳过,否则 jac=inter/len(并集 or 1)、exact 为 clue_terms 中非空且作为子串出现在 body 的个数,score=jac+0.05*exact,仅 score >= RECON_MIN_SCORE 才收录,最终按 (-score, nid) 排序返回;
531
552
  def _anchor_scores(cg, pool, clue_terms):
532
553
  """按词面重合给候选锚点打分(返回 [(nid, score, shared, exact)],降序)。"""
533
554
  from . import forgetting
@@ -556,6 +577,7 @@ def _anchor_scores(cg, pool, clue_terms):
556
577
  return scored
557
578
 
558
579
 
580
+ # 生效条件:对 anchor_ids 中每个 nid,cg.get(nid) 为假值跳过,否则取 fm 与 consistency.condition_terms 得 pos/neg,per[nid] 记为 {"effective": sorted(pos)[:8], "non_applicable": sorted(neg)[:8]};返回 common=_shared_terms(pos_sets)、individual=_uniq_terms(pos_sets) 中不在 common 的项、non_applicable=_uniq_terms(neg_sets)、declared=list(declared_terms or [])、per_anchor=per;
559
581
  def _condition_space(cg, anchor_ids, declared_terms=None):
560
582
  """合成锚点群的条件空间:共同条件 / 个别条件 / 不适用条件 / 逐锚点明细。"""
561
583
  from . import consistency
@@ -576,6 +598,7 @@ def _condition_space(cg, anchor_ids, declared_terms=None):
576
598
  "declared": list(declared_terms or []), "per_anchor": per}
577
599
 
578
600
 
601
+ # 生效条件:以 cg.index["nodes"] 为节点池(layer 为真时只保留 layer 等于该值的节点,池按 max_nodes 截断),由 clues 词项与 ids 收集锚点并截取 limit 个,锚点为空则返回 status="blindspot" 且 condition_space=None,否则返回 status="reconstructed"(neighbors 为真时按链邻接补边并在 max_nodes 内补节点,apply 为真且算出的 scene_id 尚不在 nodes 中时再写入该情境节点并记日志);
579
602
  def reconstruct_scene(cg, clues=None, ids=None, conditions=None, layer=None,
580
603
  max_nodes=RECON_MAX_NODES, limit=RECON_MAX_ANCHORS,
581
604
  neighbors=True, apply=False, actor="insight"):