@furongjun1999/dsh-memory 0.4.7 → 0.4.8

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 (172) hide show
  1. package/README.md +92 -37
  2. package/codebuddy/CODEBUDDY.md +195 -185
  3. package/docs/Pi/345/217/257/345/255/246/344/271/240/344/274/230/347/202/271_/347/201/265/346/236/242/345/244/247/350/204/221/346/224/271/350/277/233/344/272/244/346/216/245_20260915.md +144 -0
  4. package/docs/README.md +111 -0
  5. package/docs/discipline/harnesses.yaml +215 -152
  6. package/docs/discipline/templates/full.md.tmpl +60 -58
  7. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.md +127 -0
  8. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.png +0 -0
  9. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_/347/201/265/346/236/242_vs_dejavu_/347/273/237/344/270/200/350/257/204/345/210/206_v7.md +202 -0
  10. package/docs/experiments/m4-role-probe/census.py +86 -0
  11. package/docs/experiments/m4-role-probe/probe.py +89 -0
  12. package/docs/experiments/mapped_confidence/bench_mapped_conf.py +216 -0
  13. package/docs/experiments/mapped_confidence/post_check.py +75 -0
  14. package/docs/experiments/mapped_confidence/repro_thirdparty_tol.py +83 -0
  15. package/docs/experiments/mapped_confidence/result_mapped_conf.json +215 -0
  16. package/docs/{ → hive/}/350/234/202/345/267/242/345/267/245/344/275/234/350/256/260/345/277/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +14 -2
  17. package/docs/{README → mdcg/README}/350/257/246/347/273/206/347/211/210_v0.4.5.md +9 -9
  18. package/docs/{ → mdcg/}/344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md +1 -1
  19. 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 +261 -0
  20. package/docs/{ → mdcg/}/345/215/225/345/205/203/350/207/252/346/210/221/351/224/232/347/202/271_/347/263/273/347/273/237/346/217/220/347/244/272/350/257/215/346/240/207/345/207/206_v0.3.md +1 -1
  21. 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 +148 -0
  22. package/docs/{ → mdcg/}/346/272/220/347/240/201/347/272/247/346/236/266/346/236/204/345/256/241/350/256/241_GPT/346/211/271/350/257/204/345/257/271/347/205/247_v1.0.md +2 -2
  23. package/docs/{ → mdcg/}/347/201/265/346/236/242/344/270/211/345/261/202/346/213/206/345/210/206/350/247/204/345/210/222_v0.1.md +181 -181
  24. package/docs/{ → mdcg/}/347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +2 -2
  25. package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_G4-G8/347/274/272/345/217/243/350/243/201/345/256/232/345/215/225_v0.1.md +1 -1
  26. package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +4 -4
  27. package/docs/{ → mdcg/}/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_MdCGOS/344/270/216MCP/346/216/245/345/205/245_v0.1.md +2 -2
  28. package/docs/plans/GridWorld/346/234/200/345/260/217/351/227/255/347/216/257/350/247/204/346/240/274_v0.1.md +176 -0
  29. package/docs/{ → swarm/}/350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +3 -3
  30. package/docs/swarm//350/234/202/347/276/244/345/220/214/351/224/231/346/243/200/346/265/213/345/256/236/351/252/214/345/215/217/350/256/256_v0.1.md +242 -0
  31. package/docs/theory//345/215/225/347/272/277/347/250/213/344/270/216/346/263/250/346/204/217/345/212/233/351/233/206/344/270/255_/346/227/240/344/272/211/350/256/256/347/220/206/350/256/272/346/226/207/346/241/243_v1.0.md +129 -0
  32. 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 +134 -0
  33. package/docs/theory//346/246/202/345/277/265/345/210/206/345/261/202/345/257/271/351/275/220/350/241/250_v0.1.md +145 -0
  34. 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
  35. package/docs/{ → theory/}/347/220/206/350/256/272/344/273/223/346/213/206/345/210/206/344/270/216/347/231/275/347/256/261/347/237/245/350/257/206/345/272/223/345/206/205/350/277/201_v0.1.md +1 -1
  36. 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 +221 -0
  37. package/docs/world_model//344/270/226/347/225/214/346/250/241/345/236/213_/347/245/236/347/273/217/347/275/221/347/273/234/345/272/225/345/261/202/346/236/266/346/236/204_v1.0.md +237 -0
  38. package/docs//345/255/230/347/256/227/344/270/200/344/275/223/346/236/266/346/236/204/345/255/246/344/271/240_/345/244/226/351/203/250/347/220/206/350/256/272/345/257/271/347/205/247/344/270/216/350/267/257/347/272/277/344/272/244/346/216/245_20260916.md +98 -0
  39. package/docs//345/255/230/347/256/227/344/270/200/344/275/223/347/245/236/347/273/217/347/275/221/347/273/234/346/236/266/346/236/204_/346/200/273/347/272/262/344/270/216/344/272/244/346/216/245_20260916.md +109 -0
  40. 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 +34 -6
  41. package/docs//347/231/275/347/256/261/345/214/226/347/272/262/351/242/206_/344/270/211/351/241/271/347/233/256/345/255/246/344/271/240/346/200/273/346/236/266/346/236/204/344/270/216Ghidra/344/272/244/346/216/245_20260916.md +104 -0
  42. package/docs//350/256/260/345/277/206/350/257/204/345/256/241/347/263/273/347/273/237_/347/253/213/351/241/271/350/256/276/350/256/241/344/270/216/346/226/275/345/267/245/344/272/244/346/216/245_20260915.md +183 -0
  43. package/dsh/README.md +1 -1
  44. package/lib/hooks.d.ts +5 -0
  45. package/lib/hooks.js +10 -1
  46. package/lib/lib/prompt_safety.d.ts +48 -0
  47. package/lib/lib/prompt_safety.js +62 -0
  48. package/md_cg/audit.py +76 -2
  49. package/md_cg/bench_membench.py +10 -5
  50. package/md_cg/bench_progressive.py +48 -9
  51. package/md_cg/branches.py +275 -0
  52. package/md_cg/ccgc.py +884 -0
  53. package/md_cg/conformance.py +662 -0
  54. package/md_cg/consistency.py +1 -1
  55. package/md_cg/corpus.py +1 -1
  56. package/md_cg/eval_common.py +6 -5
  57. package/md_cg/forgetting.py +11 -3
  58. package/md_cg/fsutil.py +63 -1
  59. package/md_cg/identity.py +2 -2
  60. package/md_cg/insight.py +2 -2
  61. package/md_cg/lifecycle.py +262 -0
  62. package/md_cg/mcp_server.py +547 -140
  63. package/md_cg/mdcg.py +206 -18
  64. package/md_cg/mdcos.py +512 -50
  65. package/md_cg/metacognition.py +2 -2
  66. package/md_cg/mreview/__init__.py +25 -0
  67. package/md_cg/mreview/__main__.py +107 -0
  68. package/md_cg/mreview/bundle.py +170 -0
  69. package/md_cg/mreview/candidates.py +253 -0
  70. package/md_cg/mreview/govern.py +674 -0
  71. package/md_cg/mreview/locate.py +905 -0
  72. package/md_cg/mreview/pipeline.py +701 -0
  73. package/md_cg/mreview/rules/duplication.json +21 -0
  74. package/md_cg/mreview/rules/field_coverage.json +54 -0
  75. package/md_cg/mreview/rules/source_license.json +21 -0
  76. package/md_cg/mreview/rules/template_flow.json +21 -0
  77. package/md_cg/mreview/ruleset.py +238 -0
  78. package/md_cg/nodefile.py +38 -0
  79. package/md_cg/predict.py +24 -6
  80. package/md_cg/protect.py +4 -4
  81. package/md_cg/refindex.py +41 -2
  82. package/md_cg/scrub.py +5 -1
  83. package/md_cg/self_state.py +1 -1
  84. package/md_cg/semantic/en_normalizer.py +85 -25
  85. package/md_cg/sustain.py +18 -0
  86. package/md_cg/tasks.py +447 -0
  87. package/md_cg/test_action_derive.py +203 -0
  88. package/md_cg/test_audit_rotate.py +270 -0
  89. package/md_cg/test_branches.py +249 -0
  90. package/md_cg/test_ccg_perturb.py +1 -1
  91. package/md_cg/test_ccgc.py +423 -0
  92. package/md_cg/test_conformance.py +343 -0
  93. package/md_cg/test_en_pipeline.py +24 -0
  94. package/md_cg/test_health_scale.py +173 -0
  95. package/md_cg/test_identity_attribution.py +6 -0
  96. package/md_cg/test_index_durability.py +224 -0
  97. package/md_cg/test_lifecycle.py +309 -0
  98. package/md_cg/test_mr_m1.py +679 -0
  99. package/md_cg/test_mr_m2.py +587 -0
  100. package/md_cg/test_mr_m3.py +703 -0
  101. package/md_cg/test_mr_m4.py +485 -0
  102. package/md_cg/test_p1.py +1 -1
  103. package/md_cg/test_p11_consistency.py +1 -1
  104. package/md_cg/test_p12_metacognition.py +2 -2
  105. package/md_cg/test_p16_self_state.py +1 -1
  106. package/md_cg/test_p26_refindex.py +1 -1
  107. package/md_cg/test_p27_docindex.py +22 -2
  108. package/md_cg/test_p28_refcheck.py +1 -1
  109. package/md_cg/test_p29_session_ingest_export.py +1 -1
  110. package/md_cg/test_p2_mcp.py +7 -1
  111. package/md_cg/test_p38_contextualize.py +1 -1
  112. package/md_cg/test_p39_vision_evidence.py +1 -1
  113. package/md_cg/test_p40_refine_worklist.py +1 -1
  114. package/md_cg/test_p41_evolve_patrol.py +1 -1
  115. package/md_cg/test_p42_provenance.py +1 -1
  116. package/md_cg/test_p43_pooling.py +41 -13
  117. package/md_cg/test_read_clip.py +137 -0
  118. package/md_cg/test_review_conformance.py +310 -0
  119. package/md_cg/test_tasks.py +409 -0
  120. package/md_cg/test_tool_face.py +189 -0
  121. package/md_cg/test_twophase.py +286 -0
  122. package/md_cg/test_writepipe.py +210 -0
  123. package/md_cg/theory.py +1 -1
  124. package/md_cg/tokens.py +95 -5
  125. package/md_cg/tool_face.py +250 -0
  126. package/md_cg/twophase.py +221 -0
  127. package/md_cg/units.py +546 -0
  128. package/md_cg/vision_evidence.py +1 -1
  129. package/md_cg/weights.py +1 -1
  130. package/md_cg/whitebox.py +1 -1
  131. package/md_cg/whitebox_kb/data/verify_cache.json +28 -0
  132. package/md_cg/whitebox_kb/data/verify_savings.jsonl +46 -0
  133. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  134. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
  135. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
  136. package/md_cg/writelimit.py +19 -3
  137. package/md_cg/writepipe.py +372 -0
  138. package/package.json +1 -1
  139. package/src/hooks.ts +10 -1
  140. package/src/lib/prompt_safety.ts +62 -0
  141. package/zcode/AGENTS.md +195 -185
  142. package/docs//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +0 -221
  143. /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v1.0.md" +0 -0
  144. /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v2.0.md" +0 -0
  145. /package/docs/{ → eval/}/345/256/236/351/252/214/346/226/271/346/241/210_/345/255/246/344/271/240/351/227/255/347/216/257AB/344/270/216/346/250/252/350/257/204_v1.0.md" +0 -0
  146. /package/docs/{ → eval/}/346/250/252/350/257/204_/345/205/255/345/256/266100/351/242/230/344/270/255/350/213/261/345/217/214/346/237/245_v1.0.md" +0 -0
  147. /package/docs/{guardrail-charter.md → mdcg/guardrail-charter.md} +0 -0
  148. /package/docs/{lingshu_tutorial.html → mdcg/lingshu_tutorial.html} +0 -0
  149. /package/docs/{memory-assessment.html → mdcg/memory-assessment.html} +0 -0
  150. /package/docs/{memory_score.html → mdcg/memory_score.html} +0 -0
  151. /package/docs/{memory_score.png → mdcg/memory_score.png} +0 -0
  152. /package/docs/{release_v0.3.0.md → mdcg/release_v0.3.0.md} +0 -0
  153. /package/docs/{release_v0.4.5.md → mdcg/release_v0.4.5.md} +0 -0
  154. /package/docs/{tool_table_v0.3.0.md → mdcg/tool_table_v0.3.0.md} +0 -0
  155. /package/docs/{ → mdcg/}/344/273/244/347/211/214/344/270/216/350/247/222/350/211/262/346/235/203/350/201/214/345/210/206/347/246/273_v0.1.md" +0 -0
  156. /package/docs/{ → mdcg/}/347/201/265/346/236/24282/345/267/245/345/205/267_/345/212/237/350/203/275/346/225/264/347/220/206/344/270/216/350/277/201/347/247/273/346/230/240/345/260/204_v0.1.md" +0 -0
  157. /package/docs/{ → mdcg/}/347/201/265/346/236/242MCP/345/267/245/345/205/267/346/200/273/350/241/250_v3.4.md" +0 -0
  158. /package/docs/{ → mdcg/}/347/201/265/346/236/242_/350/207/252/346/210/221/345/261/202/345/256/232/344/271/211.md" +0 -0
  159. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_MD/347/233/256/345/275/225/346/226/271/346/241/210_v0.1.md" +0 -0
  160. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/346/235/241/344/273/266/347/251/272/351/227/264/345/220/210/346/210/220/344/270/216/347/224/237/346/225/210/346/235/241/344/273/266/345/217/243/345/276/204_v0.1.md" +0 -0
  161. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276/344/275/234/344/270/272/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_/350/257/204/344/274/260/344/270/216/350/267/257/347/272/277_v0.1.md" +0 -0
  162. /package/docs/{ → plans/}/345/221/275/345/220/215/346/262/273/347/220/206_/351/241/271/347/233/256/350/256/241/345/210/222.md" +0 -0
  163. /package/docs/{ → plans/}/347/237/245/350/257/206/345/233/276/350/260/261/351/241/271/347/233/256_/344/272/244/346/216/245/346/226/207/346/241/243_20260914.md" +0 -0
  164. /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/344/275/277/347/224/250/346/212/245/345/221/212_20260913.md" +0 -0
  165. /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/345/212/237/350/203/275/350/257/264/346/230/216_v0.6.md" +0 -0
  166. /package/docs/{ → swarm/}/350/234/202/347/276/244/347/233/262/345/214/272/346/240/207/350/256/260_v1.0.md" +0 -0
  167. /package/docs/{ → theory/}/344/277/241/346/201/257/345/267/256/344/270/272/344/273/200/344/271/210/345/277/205/347/204/266/345/255/230/345/234/250/344/270/224/350/207/252/347/204/266/346/211/251/345/244/247.md" +0 -0
  168. /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/345/205/254/347/220/206/345/214/226/345/237/272/347/237/263.md" +0 -0
  169. /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/350/256/244/347/237/245/350/277/207/347/250/213.md" +0 -0
  170. /package/docs/{ → theory/}/346/231/272/350/203/275/350/256/2723.4.md" +0 -0
  171. /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/346/230/257/344/273/200/344/271/210/357/274/237.md" +0 -0
  172. /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/347/263/273/345/210/227/302/267/347/254/254/344/272/224/347/257/207/357/274/232/350/256/251AI/347/234/237/346/255/243/350/243/205/344/270/212/350/256/260/345/277/206.md" +0 -0
@@ -121,6 +121,12 @@ EN_ZH = {
121
121
  "pet": "宠物", "mom": "妈妈", "tournament": "锦标赛",
122
122
  "festival": "节日", "advice": "建议", "often": "经常",
123
123
  "pottery": "陶艺",
124
+ # 第三方 LoCoMo 报告错译暴露面语境化修正(2026-09-15,修复面=暴露面对齐):
125
+ # scared 错切链路 = strip_tense(scared)→scar × CEDICT scar→创痕,复合错译
126
+ # (应为 害怕);shelter/tough/times 为 CEDICT 第一义项与口语语料语境失配
127
+ # (CEDICT 庇护/厉害/时间 → 语境 收容所/坚强/次)。低置信标记
128
+ # (source=cedict)对此全部捕获后,人工校对层逐一核对的落地
129
+ "scared": "害怕", "shelter": "收容所", "tough": "坚强", "times": "次",
124
130
  }
125
131
 
126
132
  # ---- 复合词映射(英文复合 → 中文标准概念)----
@@ -143,9 +149,18 @@ COMPOUND_ZH = {
143
149
  "fire vehicle": "火车", "cow meat": "牛肉", "gas vehicle": "汽车",
144
150
  "work discipline": "工作纪律", "knowledge graph": "知识图谱",
145
151
  "cognition graph": "认知图", "memory store": "记忆库",
152
+ # 第三方 LoCoMo 报告错译暴露面(2026-09-15):scavenger hunt 逐词直译
153
+ # → 食腐动物 猎取(应为 寻宝游戏);three times a week → 三 时间 周
154
+ # (应为 每周三次)。配合内嵌短语扫描机制在长文本/问句内部生效
155
+ "scavenger hunt": "寻宝游戏", "three times a week": "每周三次",
146
156
  "word": "字", "soul": "灵", "hub": "枢",
147
157
  }
148
158
 
159
+ # 含空格多词短语预计算(内嵌短语扫描用,长键优先防前缀吞并)
160
+ COMPOUND_ZH_PHRASES = sorted(
161
+ ((k, v) for k, v in COMPOUND_ZH.items() if " " in k),
162
+ key=lambda kv: -len(kv[0]))
163
+
149
164
 
150
165
  def _de_double(w):
151
166
  """双写辅音还原(CVC 动词屈折):regrett→regret / runn→run / stopp→stop。
@@ -181,7 +196,6 @@ def is_proper(w):
181
196
 
182
197
 
183
198
  _CEDICT_CACHE = None
184
- _COMBINED_CACHE = None
185
199
 
186
200
 
187
201
  def cedict_map():
@@ -204,14 +218,22 @@ def cedict_map():
204
218
  return _CEDICT_CACHE
205
219
 
206
220
 
207
- def _combined_map():
208
- """组合映射表:词级机械层 ← 手工校对层覆盖(手工优先)。缓存一次组合。"""
209
- global _COMBINED_CACHE
210
- if _COMBINED_CACHE is None:
211
- m = dict(cedict_map())
212
- m.update(EN_ZH)
213
- _COMBINED_CACHE = m
214
- return _COMBINED_CACHE
221
+ def _lookup_chain(extra_map=None):
222
+ """置信分层查表链(第三方验证 2026-09-15):extra(实验注入)→
223
+ EN_ZH(人工逐词校对)→ CEDICT(机械反查,第一义项直译)。
224
+
225
+ 分层依据 = 词条来源的人工校对程度,即映射质量的确定性代理信号:
226
+ CEDICT 层命中即 low_confidence(语境失配风险,实证 scavenger→食腐动物,
227
+ 见 docs/eval/第三方验证报告_LoCoMo_灵枢_.md 发现 5)。
228
+ """
229
+ chain = []
230
+ if extra_map:
231
+ chain.append((extra_map, "extra"))
232
+ chain.append((EN_ZH, "manual"))
233
+ cedict = cedict_map()
234
+ if cedict:
235
+ chain.append((cedict, "cedict"))
236
+ return chain
215
237
 
216
238
 
217
239
  def normalize_en_query(query, extra_map=None):
@@ -219,16 +241,27 @@ def normalize_en_query(query, extra_map=None):
219
241
 
220
242
  返回 (normalized_terms, detail):
221
243
  normalized_terms: 用于检索的中文/保留词序列
222
- detail: 逐词归一化记录
244
+ detail: 逐词归一化记录(mapped 词带 source/low_confidence 置信信号;
245
+ low_confidence=True = 机械反查未人工校对,语境失配风险)
223
246
  """
224
- en_zh = dict(_combined_map())
225
- if extra_map:
226
- en_zh.update(extra_map)
247
+ chain = _lookup_chain(extra_map)
227
248
  # 短语优先匹配:先尝试多词短语整体映射(复合概念名)
228
249
  phrase_key = query.lower().strip()
229
250
  if phrase_key in COMPOUND_ZH:
230
251
  comp_zh = COMPOUND_ZH[phrase_key]
231
- return [comp_zh], [{"orig": query, "phrase_zh": comp_zh, "action": "phrase_mapped"}]
252
+ return [comp_zh], [{"orig": query, "phrase_zh": comp_zh,
253
+ "action": "phrase_mapped"}]
254
+ # 内嵌短语扫描:多词短语在长文本/问句内部出现时整体替换为中文
255
+ # (整体 query 匹配只救短语独占 query 的形态;doc 侧英文原文与含修饰语
256
+ # 的问句靠此层,否则逐词直译拆散复合语义——第三方 LoCoMo 报告错译样本
257
+ # scavenger hunt → 食腐动物 猎取 即此缺口。短语表键 ≤11 个,逐键扫描
258
+ # 成本可忽略)。中文替换段经 findall 整体成原子,后续逐词链路不受影响
259
+ inline_hits = []
260
+ for phrase, pzh in COMPOUND_ZH_PHRASES:
261
+ if phrase in query.lower():
262
+ query = re.sub(re.escape(phrase), pzh, query, flags=re.IGNORECASE)
263
+ inline_hits.append({"orig": phrase, "phrase_zh": pzh,
264
+ "action": "phrase_inline"})
232
265
  # 屈折还原 + 查表。所有格剥离:'s 是正字法黏着成分非独立词
233
266
  # (Melanie's → Melanie)——不剥离则分词残留 "s" 成为伪 OOV
234
267
  # (locomo-500 实测 85 词次)。
@@ -245,14 +278,21 @@ def normalize_en_query(query, extra_map=None):
245
278
  detail.append({"orig": w, "action": "stopword_drop"})
246
279
  continue
247
280
  base = base0
248
- zh = en_zh.get(base) or en_zh.get(wl)
249
- if not zh and not base.endswith("e"):
250
- # e-脱落动词词表感知还原:loved→lov(错形)→love。词表小时收益≈0
251
- # (2026-09-14 早前取证 102 词次判不修);词级表 17700 键后
252
- # motivated→motivate / visited 类命中真实存在,条件已变
253
- zh = en_zh.get(base + "e")
254
- if zh:
255
- base = base + "e"
281
+ zh = src = None
282
+ for m, s in chain:
283
+ z = m.get(base) or m.get(wl)
284
+ if z:
285
+ zh, src = z, s
286
+ break
287
+ if not base.endswith("e"):
288
+ # e-脱落动词词表感知还原:loved→lov(错形)→love。词表小时收益≈0
289
+ # (2026-09-14 早前取证 102 词次判不修);词级表 17700 键后
290
+ # motivated→motivate / visited 类命中真实存在,条件已变
291
+ z = m.get(base + "e")
292
+ if z:
293
+ base = base + "e"
294
+ zh, src = z, s
295
+ break
256
296
  if zh and is_proper(w) and wl not in EN_ZH:
257
297
  # 专名词表命中双原子:doc 侧音译/原文两形态并存(corpus567 实测
258
298
  # 地名 巴黎13/Paris4 音译主导,人名 Caroline127/卡罗琳0 原文主导)
@@ -261,10 +301,14 @@ def normalize_en_query(query, extra_map=None):
261
301
  # 基础词)形态唯一,不具双形态不确定性,排除
262
302
  terms.append(w)
263
303
  terms.append(zh)
264
- detail.append({"orig": w, "zh": zh, "action": "proper_mapped_both"})
304
+ detail.append({"orig": w, "base": base, "zh": zh, "source": src,
305
+ "low_confidence": src == "cedict",
306
+ "action": "proper_mapped_both"})
265
307
  elif zh:
266
308
  terms.append(zh)
267
- detail.append({"orig": w, "base": base, "zh": zh, "action": "mapped"})
309
+ detail.append({"orig": w, "base": base, "zh": zh, "source": src,
310
+ "low_confidence": src == "cedict",
311
+ "action": "mapped"})
268
312
  elif is_proper(w):
269
313
  terms.append(w) # 专有词保留原名
270
314
  detail.append({"orig": w, "action": "proper_noun_keep"})
@@ -277,7 +321,23 @@ def normalize_en_query(query, extra_map=None):
277
321
  else:
278
322
  terms.append(w)
279
323
  detail.append({"orig": w, "action": "unknown_keep"})
280
- return terms, detail
324
+ return terms, inline_hits + detail
325
+
326
+
327
+ def low_confidence_terms(detail):
328
+ """从归一化 detail 提取低置信映射词清单(去重保序)。
329
+
330
+ 低置信 = mapped 词来自 CEDICT 机械反查(第一义项直译,未经人工
331
+ 语境校对)。用途:错译审计靶子 / 词表人工校对优先级(警告不拒绝,
332
+ 与 semantic_oov 同哲学——标记是观测依据,不改检索行为)。
333
+ """
334
+ out = []
335
+ for d in detail:
336
+ if d.get("low_confidence"):
337
+ w = (d.get("orig") or "").lower()
338
+ if w and w not in out:
339
+ out.append(w)
340
+ return out
281
341
 
282
342
 
283
343
  def main():
package/md_cg/sustain.py CHANGED
@@ -797,6 +797,8 @@ class SustainLoop:
797
797
  self.evolves = []
798
798
  self.last_tidy = None
799
799
  self.tidys = []
800
+ # 数据健康不变量断言集结论(Pi⑤):与 tidy 同节奏,只取结论不落盘
801
+ self.last_conformance = None
800
802
  self._started_at = None
801
803
  self._th = None
802
804
  self._stop = threading.Event()
@@ -894,6 +896,21 @@ class SustainLoop:
894
896
  with self._lock:
895
897
  self.tidys.append(rec)
896
898
  self.tidys = self.tidys[-20:]
899
+ self._tick_conformance()
900
+
901
+ def _tick_conformance(self):
902
+ """数据健康不变量断言集(Pi⑤)——与 tidy 同节奏,**只取结论**。
903
+
904
+ 复用 tidy 的 6h 节奏而不新开周期:两者都是「存量数据体检」,且都是
905
+ 只读巡检。纪律:本断言集**只告警不改数据**,故 `auto_*` 在此无意义;
906
+ `check_paths=False` 跳过 1.1 万次 stat,`_audit.jsonl` 只读尾窗。
907
+ """
908
+ from . import conformance
909
+ try:
910
+ self.last_conformance = conformance.report_summary(self.cg)
911
+ except Exception as e: # noqa: BLE001
912
+ self.last_conformance = {"ok": False, "verdict": "BLINDSPOT",
913
+ "error": f"{type(e).__name__}: {e}"}
897
914
 
898
915
  def _tick_evolve(self):
899
916
  """演化巡检(G7):盘点固化/重要性候选 —— 让「有能力」变成「有驱动」。
@@ -980,6 +997,7 @@ class SustainLoop:
980
997
  "evolves": self.evolves[-5:],
981
998
  "last_tidy": self.last_tidy,
982
999
  "tidys": self.tidys[-5:],
1000
+ "last_conformance": self.last_conformance,
983
1001
  "peers": peers(self.d),
984
1002
  "sessions": self.ledger.summary()}
985
1003
 
package/md_cg/tasks.py ADDED
@@ -0,0 +1,447 @@
1
+ """结构层任务实体 —— structural 层正式业务写入口(2026-09-16)。
2
+
3
+ 使用者裁定(按既有记忆分层,三层分工):
4
+
5
+ 任务 → structural 层(与「协议/自我/信任」同级;
6
+ 跨会话稳定、不可遗忘——`DEMOTE_CONFIDENCE`
7
+ 的降级面只覆盖 knowledge 层)
8
+ 执行任务的中间信息 → contextual 层(不落库,由 AI 上下文自理)
9
+ 任务知识 / 外部参考 / 交接文档 → knowledge 层(被验证的重要信息,按需调用;
10
+ 节点 tags 记 `task:<slug>` 实现反向关联)
11
+
12
+ 消除 AI 失忆四症状:
13
+
14
+ ① 忘记已实现的工程 → `session_tasks()` 供 `session_recall` 装配
15
+ 「进行中 + 近期完成」,新会话开机即见
16
+ ② 计划与实际不符 → `plan_add()` 累积「计划变更」节,偏差可追溯
17
+ ③ 缺核验 → 状态迁 `done` 时「结果」节必填(缺一不收,
18
+ 对齐 `branches.BRANCH_MARKS` 的同款闸)
19
+ ④ 换表述即新任务 → 身份判据 = 语义命名 slug(**刻意不用内容哈希**);
20
+ `find_similar()` 只提示疑似同族,不自动合并
21
+
22
+ 为什么身份不用内容哈希:`add_goal` 的 `goal_<sha1(text)>` 正是第 ④ 点的病根——
23
+ 同一目标换个说法就变成新 goal、重复开工。任务名是使用者给的稳定标识,
24
+ 重复登记必须更新原卡而不是再开一张。
25
+
26
+ 与 goals 的关系:**不双写**。goals 是「检索定向槽」(谁该被检索到),任务是
27
+ 「工程台账」(做到哪一步、结果是什么)——语义不同,双写必漂移(既有实证:
28
+ 覆盖写会让库正文回退首版)。会话装配面各自成段。
29
+
30
+ 与 branches 同哲学:库层函数收 `cg`;写盘走 `cg.add`(`require_layer_write`
31
+ 权限闸自动生效,库层不绕闸);生命周期事件走 `cg._audit`(不新造日志格式,
32
+ 审计失败绝不阻断主流程)。
33
+
34
+ 正文格式 = CCG 六要素 + 三节(计划 / 计划变更 / 结果):
35
+
36
+ # 功能名:<任务名>
37
+ # 生效条件:<任务适用范围;填「无条件」则豁免资格判定中的正条件确认>
38
+ # 子功能:<任务目标>
39
+ # 执行:<状态>|<进度说明>
40
+ # 验证方式:<验收判据>
41
+ # 不适用条件:<边界>
42
+
43
+ ## 计划
44
+ ## 计划变更
45
+ ## 结果
46
+ """
47
+ from __future__ import annotations
48
+
49
+ import re
50
+ import time
51
+
52
+ TASK_STATUSES = ("active", "blocked", "done", "dropped")
53
+
54
+ #: 状态中文投影(只用于正文可读性,机械判据始终用英文值)
55
+ STATUS_ZH = {"active": "进行中", "blocked": "受阻", "done": "完成", "dropped": "放弃"}
56
+
57
+ #: 任务节点统一标签(第一个是身份标记,第二个是任务族标记)
58
+ TASK_TAG = "task"
59
+ TASK_PREFIX = "task_"
60
+
61
+ #: ≥0.7 会被 protect 自动打「不可遗忘」标记——即「任务结果必须得到维护」的
62
+ #: 架构层保障:普通 forget 搬不走它,只有显式解保护才动得了。
63
+ DEFAULT_IMPORTANCE = 0.8
64
+
65
+ #: 正文节名
66
+ SEC_PLAN = "计划"
67
+ SEC_CHANGE = "计划变更"
68
+ SEC_RESULT = "结果"
69
+
70
+ #: 语义 slug 守卫——首字符须为中文/字母/数字,其后允许 `_ . -`,总长 ≤64。
71
+ #: 与 `branches._BRANCH_RE` 同风格(防路径穿越与非法文件名),但放宽到 Unicode:
72
+ #: 任务名多为中文,缩到 ASCII 会逼使用者起英文别名,反而制造第二个身份。
73
+ _SLUG_RE = re.compile(r"^[0-9A-Za-z\u4e00-\u9fff][0-9A-Za-z\u4e00-\u9fff_.-]{0,63}$")
74
+ _ILLEGAL_RE = re.compile(r"[^0-9A-Za-z\u4e00-\u9fff_.-]+")
75
+
76
+ # ---------------------------------------------------------------- 命名与解析
77
+
78
+ def slugify(name: str) -> str:
79
+ """任务名 → 语义 slug(稳定标识;同 slug 即同任务)。
80
+
81
+ 只做「可安全落文件名」的归一:路径分隔符与非法字符折叠为 `-`,连续 `-`
82
+ 压成一个。**不做语义改写**(不翻译、不去停用词)——slug 是对外可见的
83
+ 身份,擅自改写会让使用者按原名检索时对不上号。
84
+ """
85
+ s = (name or "").strip()
86
+ if not s:
87
+ return ""
88
+ s = s.replace("/", "-").replace("\\", "-").replace("..", "-")
89
+ s = _ILLEGAL_RE.sub("-", s)
90
+ s = re.sub(r"-{2,}", "-", s).strip("-.")
91
+ return s[:64].strip("-.")
92
+
93
+
94
+ def task_node_id(name: str) -> str:
95
+ """任务名或节点 id → 规范节点 id(`task_<slug>`)。非法名返回空串。"""
96
+ s = (name or "").strip()
97
+ if s.startswith(TASK_PREFIX):
98
+ s = s[len(TASK_PREFIX):]
99
+ slug = slugify(s)
100
+ if not slug or not _SLUG_RE.match(slug):
101
+ return ""
102
+ return TASK_PREFIX + slug
103
+
104
+
105
+ def _has(v) -> bool:
106
+ """「本次调用是否提供了该字段」——空串/None 一律视为未提供(不静默清空)。"""
107
+ return v is not None and str(v).strip() != ""
108
+
109
+
110
+ def _today() -> str:
111
+ return time.strftime("%Y-%m-%d %H:%M")
112
+
113
+
114
+ def _field_line(content: str, field: str) -> str:
115
+ """从正文抽 `# <字段>:` 行的值(与 `mdcos._ccg_field` 同源口径,
116
+ 本模块不反向 import mdcos,避免包内循环依赖)。"""
117
+ pre = "# " + field + ":"
118
+ for line in (content or "").splitlines():
119
+ s = line.strip()
120
+ if s.startswith(pre):
121
+ return s[len(pre):].strip()
122
+ return ""
123
+
124
+
125
+ def sections(content: str) -> dict:
126
+ """正文 → `{节名: 节内容}`;无标题部分归入 `__body__`。"""
127
+ out: dict = {"__body__": []}
128
+ cur = "__body__"
129
+ for line in (content or "").splitlines():
130
+ s = line.strip()
131
+ if s.startswith("## "):
132
+ cur = s[3:].strip()
133
+ out.setdefault(cur, [])
134
+ continue
135
+ out.setdefault(cur, []).append(line)
136
+ return {k: "\n".join(v).strip() for k, v in out.items()}
137
+
138
+
139
+ def _brief(text: str, n: int = 200) -> str:
140
+ t = (text or "").strip()
141
+ if t in ("", "(无)", "(未填)"):
142
+ return ""
143
+ return t if len(t) <= n else t[:n].rstrip() + "…"
144
+
145
+
146
+ def _append_change(old_text: str, change: str) -> str:
147
+ """「计划变更」节追加一行(累积式,不覆盖历史)。"""
148
+ base = (old_text or "").strip()
149
+ if base in ("(无)", "(未填)"):
150
+ base = ""
151
+ if not _has(change):
152
+ return base or "(无)"
153
+ line = "- [%s] %s" % (_today(), str(change).strip())
154
+ return (base + "\n" + line).strip() if base else line
155
+
156
+
157
+ def render(name: str, *, condition: str = "", goal: str = "", status: str = "active",
158
+ note: str = "", acceptance: str = "", boundary: str = "",
159
+ plan: str = "", changes: str = "", result: str = "") -> str:
160
+ """按 CCG 六要素 + 三节渲染任务正文。"""
161
+ exec_line = STATUS_ZH.get(status, status)
162
+ if _has(note):
163
+ exec_line = exec_line + "|" + str(note).strip()
164
+ return "\n".join([
165
+ "# 功能名:%s" % (name or "").strip(),
166
+ "# 生效条件:%s" % (condition.strip() if _has(condition) else "无条件"),
167
+ "# 子功能:%s" % (goal.strip() if _has(goal) else "(未填:任务目标待补)"),
168
+ "# 执行:%s" % exec_line,
169
+ "# 验证方式:%s" % (acceptance.strip() if _has(acceptance) else "other"),
170
+ "# 不适用条件:%s" % (boundary.strip() if _has(boundary)
171
+ else "任务转 done/dropped 终态后不再作为进行中任务参与装配"),
172
+ "",
173
+ "## %s" % SEC_PLAN,
174
+ (plan.strip() if _has(plan) else "(未填)"),
175
+ "",
176
+ "## %s" % SEC_CHANGE,
177
+ (changes.strip() if _has(changes) else "(无)"),
178
+ "",
179
+ "## %s" % SEC_RESULT,
180
+ (result.strip() if _has(result) else ""),
181
+ "",
182
+ ])
183
+
184
+
185
+ # ---------------------------------------------------------------- 读写
186
+
187
+ def _read_task(cg, nid: str):
188
+ """读回任务卡;层或标签不符一律视为不存在(防串号:别的节点占用了同 id)。"""
189
+ if not nid:
190
+ return None
191
+ rec = cg.get(nid)
192
+ if not rec:
193
+ return None
194
+ fm = rec.get("frontmatter") or {}
195
+ if fm.get("layer") != "structural" or TASK_TAG not in (fm.get("tags") or []):
196
+ return None
197
+ content = rec.get("content") or ""
198
+ return {"id": nid, "fm": fm, "content": content,
199
+ "path": rec.get("path"), "sec": sections(content)}
200
+
201
+
202
+ def _write(cg, nid: str, name: str, content: str, status: str, tags,
203
+ importance: float, actor, extra: dict = None):
204
+ """唯一写盘点——走 `cg.add`(权限闸 / 归属注入全部复用既有链路)。
205
+
206
+ `override=True` 的含义:任务更新是**系统自身的幂等更新**(同 slug 即同任务),
207
+ 显式声明该意图后,若节点已带 `protected`/`self_state` 标记,protect 会走
208
+ 「旧版本快照 + 审计留痕」的放行分支而不是静默改写——任务台账天然获得版本快照。
209
+ """
210
+ kw = dict(layer="structural", tags=list(tags), importance=float(importance),
211
+ confidence=1.0, verification_basis="other",
212
+ override=True, consistency=False,
213
+ task_name=name, task_status=status,
214
+ task_updated_at=time.time(), actor=actor or "task")
215
+ if extra:
216
+ kw.update(extra)
217
+ return cg.add(nid, content, **kw)
218
+
219
+
220
+ def _audit(cg, op: str, nid: str, **meta) -> None:
221
+ """生命周期留痕;审计失败绝不阻断主流程(与 branches._audit 同哲学)。"""
222
+ a = getattr(cg, "_audit", None)
223
+ if a is None:
224
+ return
225
+ try:
226
+ a(op, nid, **meta)
227
+ except Exception: # noqa: BLE001
228
+ pass
229
+
230
+
231
+ def _entry(cg, nid: str):
232
+ """节点 id → 对外任务条目(摘要形态;全字段查 `get_task`)。"""
233
+ rec = _read_task(cg, nid)
234
+ if not rec:
235
+ return None
236
+ fm, sec = rec["fm"], rec["sec"]
237
+ return {"id": rec["id"],
238
+ "name": fm.get("task_name") or rec["id"],
239
+ "status": fm.get("task_status") or "active",
240
+ "plan": _brief(sec.get(SEC_PLAN, "")),
241
+ "changes": _brief(sec.get(SEC_CHANGE, "")),
242
+ "result": _brief(sec.get(SEC_RESULT, "")),
243
+ "created_at": fm.get("task_created_at"),
244
+ "updated_at": fm.get("task_updated_at"),
245
+ "path": rec["path"]}
246
+
247
+
248
+ # ---------------------------------------------------------------- 写操作
249
+
250
+ def upsert(cg, name: str, *, plan: str = None, status: str = None,
251
+ result: str = None, condition: str = None, goal: str = None,
252
+ acceptance: str = None, boundary: str = None, change: str = None,
253
+ note: str = None, tags=None, importance: float = None,
254
+ actor: str = None) -> dict:
255
+ """登记或更新一张任务卡(同 slug 即同任务;重复登记不新建卡)。
256
+
257
+ 未提供的字段**沿用旧值**(不静默清空);`status="done"` 且合并后「结果」节
258
+ 仍为空 → 拒收,盘上内容不变。
259
+
260
+ :param note: 进度说明,落 `# 执行:` 行(`|` 之后)。
261
+ :param change: 本次发现的新问题/计划偏差,追加到「计划变更」节。
262
+ """
263
+ slug = slugify(name)
264
+ if not slug or not _SLUG_RE.match(slug):
265
+ return {"ok": False,
266
+ "error": "任务名非法(slug=%r);要求:中文/字母/数字起头,"
267
+ "仅含中文/字母/数字/_ . -,长度 ≤64" % (slug,)}
268
+
269
+ nid = TASK_PREFIX + slug
270
+ old = _read_task(cg, nid)
271
+ prev_fm = old["fm"] if old else {}
272
+ prev = old["sec"] if old else {}
273
+
274
+ display = (name or "").strip() or prev_fm.get("task_name") or slug
275
+ st = str(status if _has(status) else (prev_fm.get("task_status") or "active")).strip().lower()
276
+ if st not in TASK_STATUSES:
277
+ return {"ok": False, "node_id": nid,
278
+ "error": "未知任务状态:%r(可选 %s)" % (st, list(TASK_STATUSES))}
279
+
280
+ # 未提供 → 沿用旧值(结果与计划绝不被静默清空)
281
+ new_plan = plan if _has(plan) else prev.get(SEC_PLAN, "")
282
+ new_res = result if _has(result) else prev.get(SEC_RESULT, "")
283
+ new_cond = condition if _has(condition) else _field_line(old["content"], "生效条件") if old else ""
284
+ new_goal = goal if _has(goal) else _field_line(old["content"], "子功能") if old else ""
285
+ new_acc = acceptance if _has(acceptance) else _field_line(old["content"], "验证方式") if old else ""
286
+ new_bnd = boundary if _has(boundary) else _field_line(old["content"], "不适用条件") if old else ""
287
+ new_note = note if _has(note) else _exec_note(old["content"]) if old else ""
288
+ new_changes = _append_change(prev.get(SEC_CHANGE, ""), change)
289
+
290
+ if st == "done" and not _has(new_res):
291
+ return {"ok": False, "node_id": nid,
292
+ "error": "任务转 done 必须填写「结果」——结果不可缺失(缺一不收)",
293
+ "hint": "补 result 后重试;盘上内容未变更"}
294
+
295
+ content = render(display, condition=new_cond, goal=new_goal, status=st,
296
+ note=new_note, acceptance=new_acc, boundary=new_bnd,
297
+ plan=new_plan, changes=new_changes, result=new_res)
298
+ tg = list(dict.fromkeys([TASK_TAG, "%s:%s" % (TASK_TAG, slug)]
299
+ + [str(t) for t in (tags or []) if str(t).strip()]))
300
+ imp = DEFAULT_IMPORTANCE if importance is None else float(importance)
301
+ created = prev_fm.get("task_created_at") or time.time()
302
+ _write(cg, nid, display, content, st, tg, imp, actor,
303
+ extra={"task_created_at": created, "task_slug": slug})
304
+ _audit(cg, "task_open" if old is None else "task_update", nid,
305
+ status=st, slug=slug, name=display)
306
+ out = {"ok": True, "node_id": nid, "name": display, "status": st,
307
+ "created": old is None,
308
+ "result_present": _has(new_res),
309
+ "hint": "新建任务卡" if old is None else "已更新原卡(同 slug 即同任务)"}
310
+ if slug != (name or "").strip():
311
+ # 身份被归一化了就必须说出来——否则调用方按原名去找会找不到卡
312
+ out["slug_normalized"] = slug
313
+ return out
314
+
315
+
316
+ def _exec_note(content: str) -> str:
317
+ """从 `# 执行:` 行取 `|` 之后的进度说明。"""
318
+ val = _field_line(content, "执行")
319
+ return val.split("|", 1)[1].strip() if "|" in val else ""
320
+
321
+
322
+ def set_status(cg, node_id: str, status: str, *, result: str = None,
323
+ note: str = None, actor: str = None) -> dict:
324
+ """任务状态迁移(进行中/受阻/完成/放弃)。迁 done 且无结果 → 拒收。"""
325
+ nid = task_node_id(node_id)
326
+ if not nid:
327
+ return {"ok": False, "error": "非法任务标识:%r" % (node_id,)}
328
+ old = _read_task(cg, nid)
329
+ if not old:
330
+ return {"ok": False, "node_id": nid, "error": "任务不存在:%s" % nid}
331
+ return upsert(cg, old["fm"].get("task_name") or nid, status=status,
332
+ result=result, note=note, actor=actor)
333
+
334
+
335
+ def plan_add(cg, node_id: str, change: str, *, actor: str = None) -> dict:
336
+ """计划变更追加——执行中发现的错误/新问题/偏差,累积进「计划变更」节。"""
337
+ if not _has(change):
338
+ return {"ok": False, "error": "计划变更内容为空"}
339
+ nid = task_node_id(node_id)
340
+ if not nid:
341
+ return {"ok": False, "error": "非法任务标识:%r" % (node_id,)}
342
+ old = _read_task(cg, nid)
343
+ if not old:
344
+ return {"ok": False, "node_id": nid, "error": "任务不存在:%s" % nid}
345
+ return upsert(cg, old["fm"].get("task_name") or nid, change=change, actor=actor)
346
+
347
+
348
+ # ---------------------------------------------------------------- 读操作
349
+
350
+ def get_task(cg, node_id: str) -> dict:
351
+ """单卡全字段读回(计划/变更/结果不截断)。"""
352
+ nid = task_node_id(node_id)
353
+ rec = _read_task(cg, nid)
354
+ if not rec:
355
+ return {"ok": False, "error": "任务不存在:%s" % (nid or node_id)}
356
+ fm, sec = rec["fm"], rec["sec"]
357
+ return {"ok": True, "id": rec["id"], "name": fm.get("task_name") or rec["id"],
358
+ "status": fm.get("task_status") or "active",
359
+ "condition": _field_line(rec["content"], "生效条件"),
360
+ "goal": _field_line(rec["content"], "子功能"),
361
+ "note": _exec_note(rec["content"]),
362
+ "acceptance": _field_line(rec["content"], "验证方式"),
363
+ "boundary": _field_line(rec["content"], "不适用条件"),
364
+ "plan": sec.get(SEC_PLAN, ""),
365
+ "changes": sec.get(SEC_CHANGE, ""),
366
+ "result": sec.get(SEC_RESULT, ""),
367
+ "tags": list(fm.get("tags") or []),
368
+ "created_at": fm.get("task_created_at"),
369
+ "updated_at": fm.get("task_updated_at"),
370
+ "path": rec["path"]}
371
+
372
+
373
+ def list_tasks(cg, status: str = None, limit: int = None) -> dict:
374
+ """任务清单(按 updated_at 倒序);status 可选 active/blocked/done/dropped。"""
375
+ st = str(status or "").strip().lower() or None
376
+ if st and st not in TASK_STATUSES:
377
+ return {"ok": False, "error": "未知任务状态:%r(可选 %s)" % (st, list(TASK_STATUSES))}
378
+ items = []
379
+ for nid in list((cg.index.get("nodes") or {}).keys()):
380
+ t = _entry(cg, nid)
381
+ if t:
382
+ items.append(t)
383
+ items.sort(key=lambda x: -(x.get("updated_at") or 0))
384
+ if st:
385
+ items = [t for t in items if t["status"] == st]
386
+ total = len(items)
387
+ if limit:
388
+ items = items[:max(int(limit), 1)]
389
+ return {"ok": True, "count": len(items), "total": total, "tasks": items}
390
+
391
+
392
+ def session_tasks(cg, active_limit: int = 5, done_limit: int = 5) -> dict:
393
+ """会话装配用:进行中(active/blocked)+ 近期完成(done,按 updated_at 倒序)。
394
+
395
+ 这是第 ① 点(忘记已实现的工程)的机制解:上下文装配面在会话开始时把
396
+ 「还没做完的」与「刚做完的」一起带出来,agent 不必先想到去查。
397
+ """
398
+ active, done = [], []
399
+ for nid in list((cg.index.get("nodes") or {}).keys()):
400
+ t = _entry(cg, nid)
401
+ if not t:
402
+ continue
403
+ if t["status"] in ("active", "blocked"):
404
+ active.append(t)
405
+ elif t["status"] == "done":
406
+ done.append(t)
407
+ key = lambda x: -(x.get("updated_at") or 0) # noqa: E731
408
+ active.sort(key=key)
409
+ done.sort(key=key)
410
+ return {"ok": True,
411
+ "active": active[:max(int(active_limit), 0)],
412
+ "done": done[:max(int(done_limit), 0)],
413
+ "active_total": len(active), "done_total": len(done)}
414
+
415
+
416
+ def find_similar(cg, name: str, k: int = 5) -> dict:
417
+ """同族任务提示(**只提示,不自动合并**——是否同一任务由调用方裁决)。
418
+
419
+ 身份判据是命名而非相似度:这里只回答「已经有一张同名的卡吗」与
420
+ 「还有哪些卡看起来像」。相似度只能产生候选,不能授予「同一任务」的资格。
421
+ """
422
+ nid = task_node_id(name)
423
+ exact = _read_task(cg, nid) is not None
424
+ kk = max(int(k or 5), 1)
425
+ res = []
426
+ try:
427
+ res, _meta = cg.search(name, layer="structural", k=kk + (1 if exact else 0),
428
+ record=False)
429
+ except Exception: # noqa: BLE001
430
+ res = []
431
+ out = []
432
+ for item in res or []:
433
+ if not (isinstance(item, (tuple, list)) and len(item) >= 3):
434
+ continue
435
+ node, score, qual = item[0], item[1], item[2]
436
+ fm = (node.get("frontmatter") or {}) if isinstance(node, dict) else {}
437
+ if TASK_TAG not in (fm.get("tags") or []):
438
+ continue
439
+ if node.get("id") == nid:
440
+ continue
441
+ out.append({"id": node.get("id"),
442
+ "name": fm.get("task_name") or node.get("id"),
443
+ "status": fm.get("task_status"),
444
+ "score": round(float(score), 4),
445
+ "qualification": (qual or {}).get("state") if isinstance(qual, dict) else None})
446
+ return {"ok": True, "node_id": nid, "exists": exact, "similar": out[:kk],
447
+ "note": "仅提示疑似同族任务,不自动合并——终裁权在调用方"}