@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
@@ -3,7 +3,7 @@
3
3
 
4
4
  理论出处(本仓原文,非外部知识)
5
5
  ------------------------------
6
- · **情绪 = 信息差的二阶变化 d²D/dt²**(`docs/智能的公理化基石.md` §十一,:412-510):
6
+ · **情绪 = 信息差的二阶变化 d²D/dt²**(`docs/theory/智能的公理化基石.md` §十一,:412-510):
7
7
  「情绪不是状态本身,而是状态变化的变化」。元认知正是对认知过程做
8
8
  二阶观测的单元:一阶 = 检索结果好坏,二阶 = 认知状态在如何变化。
9
9
  · **情感 = 信任的二阶变化 d²T/dt²**(同文档 :469-508)。
@@ -12,7 +12,7 @@
12
12
  · **推论三 局部不可知**(同文档 :277-288):
13
13
  「知道什么不知道,同样属于知识」——盲区地图是法定输出,不是失败记录。
14
14
  · **信任 P_trust / P_gap**(同文档 :393-408)。
15
- · **导航税**(`docs/智能的认知过程.md`:84-86):维持认知过程一致性需要显式
15
+ · **导航税**(`docs/theory/智能的认知过程.md`:84-86):维持认知过程一致性需要显式
16
16
  状态记录——元认知就是付这笔税的单元。
17
17
 
18
18
  独立性的三重含义(本模块的硬约束)
@@ -0,0 +1,25 @@
1
+ # -*- coding: utf-8 -*-
2
+ """记忆评审流水线(M1 机械层 / M2 蜂巢并发 / M3 定位)。
3
+
4
+ 真源:docs/记忆评审系统_立项设计与施工交接_20260915.md §3 / §5 / §6。
5
+
6
+ [1 候选生成] → [2 捆绑] → [3 规则装配] → [4 蜂巢评审] → [5 落库治理]
7
+ 确定性 确定性 确定性 LLM(M2) 确定性(M4)
8
+
9
+ * candidates.py 级 1:四源候选(断言超限 / 闸门 DEFER / 冷节点 / 轮巡)
10
+ * bundle.py 级 2:按模板签名/血缘/主题捆绑(同模板组一包一评)
11
+ * ruleset.py 级 3:D3 规则装配(规则全落 rules/*.json,引擎零改动)
12
+ * pipeline.py M2 级 4+5:投递收卷(蜂巢)+ 意见落库(verify 令牌 + writepipe)
13
+ * locate.py M3 D1:字段级定位——意见说「有问题」,定位说「在第几个字段/哪一段」
14
+ * govern.py M4 落库治理(确定性部分):规则驱动 · 只回填 · 永不删除
15
+ plan(预演)/ apply(执行 + 留痕)/ rollback(按留痕回滚)/ history / stats
16
+
17
+ 边界:M1/M3 只读不写;机械可算的在蜂巢 job 前跑完(LLM 只看语义问题);
18
+ 本条流水线不产出「删除」动作(治理只有合并/降权/回填/移层,M4 终裁)。
19
+ M4 只做**确定性**部分;依赖语义判断的合并/降权建议(LLM 面)不在本模块。
20
+ """
21
+
22
+ __all__ = ["candidates", "bundle", "ruleset", "pipeline", "locate", "govern",
23
+ "SPEC_VERSION"]
24
+
25
+ SPEC_VERSION = 1
@@ -0,0 +1,107 @@
1
+ # -*- coding: utf-8 -*-
2
+ """M1 机械层入口(零写入):
3
+
4
+ python -m md_cg.mreview [--root R] [--out dump.json] [--full] [--limit N]
5
+
6
+ 默认只打印/落盘**摘要 + 包清单 + 机械聚合**;``--full`` 追加条目明细与逐条机械 issue
7
+ (真源全量 dump 会很大,默认裁剪是为可审查性服务,不是隐藏内容)。
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import collections
13
+ import json
14
+ import os
15
+
16
+ from .. import conformance as CF
17
+ from ..datapath import mdcg_root
18
+ from . import bundle as BD
19
+ from . import candidates as CD
20
+ from . import ruleset as RS
21
+
22
+
23
+ def run(root=None, *, sources=CD.SOURCES, cold_limit=500, patrol_window=200,
24
+ max_per_bundle=50, limit=None, patrol_offset=None, rules_dir=None,
25
+ full=False) -> dict:
26
+ root = root or mdcg_root()
27
+ nodes = CF.load_index(root) or {} # load_index 直接返回 nodes 字典
28
+ gen = CD.generate(root, sources=sources, nodes=nodes, limit=limit,
29
+ cold_limit=cold_limit, patrol_window=patrol_window,
30
+ patrol_offset=patrol_offset, with_report=True)
31
+ bd = BD.bundle(gen["candidates"], nodes=nodes, root=root,
32
+ max_per_bundle=max_per_bundle)
33
+ asm = RS.assemble_all(bd, rules_dir=rules_dir)
34
+ by_rule = collections.Counter(i["rule_id"] for p in asm["packages"] for i in p["mechanical"])
35
+ out = {
36
+ "root": root,
37
+ "candidates": {"by_source": gen["by_source"], "total_raw": gen["total_raw"],
38
+ "total": gen["total"], "skipped": gen["skipped"]},
39
+ "report": gen.get("report"),
40
+ "assertions": gen.get("assertions"),
41
+ "bundles": [{"bundle_id": b["bundle_id"], "group_kind": b["group_kind"],
42
+ "group_key": b["group_key"], "size": b["size"], "refs": b["refs"]}
43
+ for b in bd["bundles"]],
44
+ "bundle_stats": bd["stats"],
45
+ "content_missing": bd["content_missing"],
46
+ "assemble_stats": asm["stats"],
47
+ "mechanical_by_rule": dict(by_rule),
48
+ "llm_checks": sorted({(c["rule_id"], c["check"]) for p in asm["packages"]
49
+ for c in p["llm"]}),
50
+ }
51
+ if full:
52
+ out["candidates_full"] = gen["candidates"]
53
+ out["bundles_full"] = bd["bundles"]
54
+ out["packages_full"] = asm["packages"]
55
+ return out
56
+
57
+
58
+ def _fmt_summary(d: dict) -> str:
59
+ c, b, a = d["candidates"], d["bundle_stats"], d["assemble_stats"]
60
+ lines = ["", "== M1 机械层(候选 → 捆绑 → 规则装配) ==",
61
+ "真源:%s" % d["root"],
62
+ "候选:原始 %d 条 → 去重后 %d 条(按源:%s)"
63
+ % (c["total_raw"], c["total"],
64
+ ", ".join("%s=%d" % kv for kv in sorted(c["by_source"].items()))),
65
+ "跳过(未落盘/无节点):%d 条" % len(c["skipped"]),
66
+ "捆绑:%d 包(最大 %d 条/包;分组类型 %s)"
67
+ % (b["bundles"], b["largest"], b["by_group_kind"]),
68
+ "正文缺失:%d 条(读盘失败计入,不冒充内容)" % d["content_missing"],
69
+ "规则:%d 条(%s)" % (len(a["rules"]), ", ".join(a["rules"])),
70
+ "机械检查:%d 条 issue(按类 %s)" % (a["mechanical_total"], a["mechanical_by_kind"]),
71
+ "待 LLM 检查项:%d 条(%s)"
72
+ % (a["llm_checks"], ", ".join("%s:%s" % t for t in d["llm_checks"]))]
73
+ asr = d.get("assertions") or {}
74
+ if asr:
75
+ lines.append("断言集交叉核对:verdict=%s,未达标项 %s"
76
+ % (asr.get("verdict"), ", ".join(asr.get("failed") or []) or "无"))
77
+ return "\n".join(lines)
78
+
79
+
80
+ def main(argv=None) -> int:
81
+ ap = argparse.ArgumentParser(prog="python -m md_cg.mreview",
82
+ description="M1 机械层:候选生成→捆绑→规则装配(只读零写入)")
83
+ ap.add_argument("--root", default=None, help="认知图根(缺省 MDCG_ROOT/默认路径)")
84
+ ap.add_argument("--sources", default=",".join(CD.SOURCES))
85
+ ap.add_argument("--limit", type=int, default=None)
86
+ ap.add_argument("--cold-limit", type=int, default=500)
87
+ ap.add_argument("--patrol-window", type=int, default=200)
88
+ ap.add_argument("--patrol-offset", type=int, default=None)
89
+ ap.add_argument("--max-per-bundle", type=int, default=50)
90
+ ap.add_argument("--rules-dir", default=None)
91
+ ap.add_argument("--out", default=None, help="落盘 dump(JSON)")
92
+ ap.add_argument("--full", action="store_true", help="含条目明细与逐条 issue")
93
+ a = ap.parse_args(argv)
94
+ d = run(a.root, sources=tuple(s for s in a.sources.split(",") if s),
95
+ cold_limit=a.cold_limit, patrol_window=a.patrol_window, limit=a.limit,
96
+ patrol_offset=a.patrol_offset, max_per_bundle=a.max_per_bundle,
97
+ rules_dir=a.rules_dir, full=a.full)
98
+ print(_fmt_summary(d))
99
+ if a.out:
100
+ with open(os.path.abspath(a.out), "w", encoding="utf-8") as f:
101
+ json.dump(d, f, ensure_ascii=False, indent=1)
102
+ print("已落盘:%s" % os.path.abspath(a.out))
103
+ return 0
104
+
105
+
106
+ if __name__ == "__main__":
107
+ raise SystemExit(main())
@@ -0,0 +1,170 @@
1
+ # -*- coding: utf-8 -*-
2
+ """记忆评审流水线 · 级 2:捆绑(确定性,零写入)。
3
+
4
+ 真源:docs/记忆评审系统_立项设计与施工交接_20260915.md §3 第 2 级。
5
+
6
+ **同模板组一包一评**:批次报告类节点(「批次N收官记忆」)正文各不相同但同模板,
7
+ 逐条评审=重复劳动且立场漂移;成组评审=一次裁决整组。分组键优先级:
8
+
9
+ 1. ``template`` 模板签名——复用 :func:`writelimit.template_signature`(强信号 ``t:``);
10
+ 长文(≥ MAX_CONTENT)无标题签名时回退**首行骨架**(弱信号 ``h:``)——真源知识节点
11
+ 正文多为长文四要素,标题/首行才是同模板流水的载体(该回退只在评审分组内生效,
12
+ 不改写侧聚合口径)。
13
+ 2. ``lineage`` 血缘——branch_id / branched_from / derived_from[0](同源迁移一起看)。
14
+ 3. ``topic`` 标签前缀集合(无标签回退层)。
15
+ 4. ``batch`` 无任何分组特征的孤立候选,按确定性顺序成批(不误聚、不丢弃)。
16
+
17
+ 组内条数 < ``min_group`` 的组降级并入 batch;组内超 ``max_per_bundle`` 切分多包。
18
+ 包号确定性:``b1_<kind>_<sha1(key)[:8]>_c<idx>``;同一输入两次运行逐字节一致。
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import collections
23
+ import hashlib
24
+ import os
25
+
26
+ from .. import conformance as CF
27
+ from .. import writelimit as WL
28
+
29
+ GROUP_TEMPLATE = "template"
30
+ GROUP_LINEAGE = "lineage"
31
+ GROUP_TOPIC = "topic"
32
+ GROUP_BATCH = "batch"
33
+
34
+
35
+ def _sig_of(content: str):
36
+ """模板签名(强/弱)。返回 (sig, kind) 或 (None, None)。"""
37
+ if not content:
38
+ return None, None
39
+ s = WL.template_signature(content)
40
+ if s:
41
+ return s, "strong"
42
+ for line in content.splitlines():
43
+ t = line.strip()
44
+ if not t:
45
+ continue
46
+ sk = WL._skeleton(t)
47
+ if len(sk) >= WL.MIN_SKELETON:
48
+ return "h:" + sk, "head"
49
+ return None, None
50
+ return None, None
51
+
52
+
53
+ def _lineage_key(meta: dict):
54
+ for f in ("branched_from", "branch_id"):
55
+ v = meta.get(f)
56
+ if v and str(v) != "None":
57
+ return str(v)
58
+ d = meta.get("derived_from")
59
+ if isinstance(d, (list, tuple)) and d:
60
+ return str(d[0])
61
+ if isinstance(d, str) and d and d not in ("[]", "None"):
62
+ return d
63
+ return None
64
+
65
+
66
+ def _topic_key(meta: dict, tags: list):
67
+ pref = sorted(CF._tag_prefixes(meta)) if meta else []
68
+ if not pref and tags:
69
+ pref = sorted({str(t).split(":", 1)[0] for t in tags if t})
70
+ if pref:
71
+ return "+".join(pref)
72
+ layer = str((meta or {}).get("layer") or "")
73
+ return ("layer:" + layer) if layer else None
74
+
75
+
76
+ def _group_of(meta: dict, tags: list, content: str):
77
+ sig, kind = _sig_of(content)
78
+ if sig:
79
+ return GROUP_TEMPLATE, sig, kind
80
+ lin = _lineage_key(meta)
81
+ if lin:
82
+ return GROUP_LINEAGE, lin, None
83
+ top = _topic_key(meta, tags)
84
+ if top:
85
+ return GROUP_TOPIC, top, None
86
+ return GROUP_BATCH, "misc", None
87
+
88
+
89
+ def _load_content(root: str, meta: dict, limit: int):
90
+ if not root or not meta:
91
+ return None
92
+ rel = str(meta.get("path") or "")
93
+ if not rel:
94
+ return None
95
+ p = os.path.join(root, rel.replace("\\", os.sep).replace("/", os.sep))
96
+ try:
97
+ with open(p, encoding="utf-8", errors="replace") as f:
98
+ return f.read(int(limit))
99
+ except OSError:
100
+ return None
101
+
102
+
103
+ def _entry(cand: dict, meta: dict, content: str, limit: int) -> dict:
104
+ return {"ref": cand.get("key"), "node_id": cand.get("node_id"),
105
+ "proposal_id": cand.get("proposal_id"), "origin": cand.get("origin"),
106
+ "layer": cand.get("layer"), "tags": list(cand.get("tags") or []),
107
+ "role": (meta or {}).get("role"),
108
+ "importance": (meta or {}).get("importance"),
109
+ "evidence_count": (meta or {}).get("evidence_count"),
110
+ "verification_basis": (meta or {}).get("verification_basis"),
111
+ "lifecycle_state": (meta or {}).get("lifecycle_state"),
112
+ "content_hash": cand.get("content_hash") or (meta or {}).get("content_hash"),
113
+ "issue_kinds": list(cand.get("issue_kinds") or []),
114
+ "evidence": list(cand.get("evidence") or []),
115
+ "excerpt": (content or "")[:int(limit)] or None}
116
+
117
+
118
+ def _chunks(ids: list, size: int) -> list:
119
+ size = max(1, int(size))
120
+ return [ids[i:i + size] for i in range(0, len(ids), size)]
121
+
122
+
123
+ def bundle(candidates: list, nodes=None, root=None, *, max_per_bundle=50,
124
+ min_group=2, read_content=True, content_limit=3000) -> dict:
125
+ """候选清单 → 待评包(含条目上下文与机械字段,供级 3 装配与级 4 spec)。"""
126
+ nodes = nodes or {}
127
+ bykey = {c.get("key"): c for c in candidates}
128
+ groups, contents = {}, {}
129
+ for c in candidates:
130
+ nid = c.get("node_id")
131
+ meta = (nodes.get(nid) or {}) if nid else {}
132
+ content = c.get("content") or ""
133
+ if not content and read_content and nid:
134
+ content = _load_content(root, meta, content_limit) or ""
135
+ content = (content or "")[:int(content_limit)]
136
+ kind, key, sig_kind = _group_of(meta, c.get("tags") or [], content)
137
+ g = groups.get((kind, key))
138
+ if g is None:
139
+ g = groups[(kind, key)] = {"ids": [], "sig_kind": sig_kind}
140
+ g["ids"].append(c.get("key"))
141
+ contents[c.get("key")] = content
142
+
143
+ # 小组降级并入 batch(不误聚、不丢弃)
144
+ demoted = []
145
+ for k in [k for k, g in groups.items()
146
+ if k[0] != GROUP_BATCH and len(g["ids"]) < int(min_group)]:
147
+ demoted += groups.pop(k)["ids"]
148
+ for k in [k for k in groups if k[0] == GROUP_BATCH]:
149
+ demoted += groups.pop(k)["ids"]
150
+ demoted = sorted(demoted)
151
+ if demoted:
152
+ groups[(GROUP_BATCH, "misc")] = {"ids": demoted, "sig_kind": None}
153
+
154
+ bundles = []
155
+ for (kind, key) in sorted(groups):
156
+ g = groups[(kind, key)]
157
+ for i, chunk in enumerate(_chunks(sorted(g["ids"]), int(max_per_bundle))):
158
+ bid = "b1_%s_%s_c%d" % (kind, hashlib.sha1(str(key).encode("utf-8")).hexdigest()[:8], i)
159
+ bundles.append({
160
+ "bundle_id": bid, "group_kind": kind, "group_key": str(key),
161
+ "group_sig_kind": g["sig_kind"], "size": len(chunk), "refs": chunk,
162
+ "entries": [_entry(bykey[r], nodes.get(bykey[r].get("node_id") or "") or {},
163
+ contents.get(r) or "", content_limit)
164
+ for r in chunk]})
165
+ stats = {"candidates": len(candidates), "bundles": len(bundles),
166
+ "largest": max([b["size"] for b in bundles] or [0]),
167
+ "by_group_kind": dict(collections.Counter(b["group_kind"] for b in bundles))}
168
+ return {"bundles": bundles, "stats": stats,
169
+ "content_missing": sum(1 for c in candidates
170
+ if not contents.get(c.get("key")))}
@@ -0,0 +1,253 @@
1
+ # -*- coding: utf-8 -*-
2
+ """记忆评审流水线 · 级 1:候选生成(确定性,零写入)。
3
+
4
+ 真源:docs/记忆评审系统_立项设计与施工交接_20260915.md §3 第 1 级。
5
+
6
+ 四源(可单独关闭):
7
+
8
+ * ``assertion`` 断言集超限项的**条目级展开**——复用 conformance 的**同一判据函数**
9
+ (`_coverage_metrics` / `unanalyzed` / `_dup_metrics` / `_edge_metrics`),
10
+ 保证「报告聚合数」与「条目清单」两个入口同口径(见 test_mr_m1 T4)。
11
+ * ``defer_queue`` 闸门待裁决提案(inbox 未被 decisions 覆盖)+遗忘日志 DEFER 留痕;
12
+ 未落盘的 node_id 计入 ``skipped``,**如实上报不冒充候选**。
13
+ * ``cold`` access log 触达集合之外、且重要度不低的节点(久未触达者更值得评审)。
14
+ * ``patrol`` sha1(node_id) 排序切片轮巡(offset 决定窗口位置),多轮不重不漏。
15
+
16
+ 纪律:零写入;输出顺序由 (优先级, node_id) 决定;每条候选必带证据。
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import collections
21
+ import hashlib
22
+ import json
23
+ import os
24
+ import time
25
+
26
+ from .. import conformance as CF
27
+ from ..datapath import mdcg_root
28
+
29
+ SOURCES = ("assertion", "defer_queue", "cold", "patrol")
30
+ _PRIORITY = {"assertion": 0, "defer_queue": 1, "cold": 2, "patrol": 3}
31
+ FORGET_LOG = "_forgetting.jsonl"
32
+ PATROL_EPOCH = 3600.0 # 轮巡窗口推进周期(秒):默认每窗口周期轮转一格
33
+ COLD_IMPORTANCE_MIN = 0.6 # 冷节点抽样门槛(importance 解析失败按 0 计)
34
+
35
+
36
+ def _read_jsonl(path: str) -> list:
37
+ if not os.path.exists(path):
38
+ return []
39
+ out = []
40
+ with open(path, encoding="utf-8", errors="replace") as f:
41
+ for line in f:
42
+ line = line.strip()
43
+ if not line:
44
+ continue
45
+ try:
46
+ out.append(json.loads(line))
47
+ except json.JSONDecodeError:
48
+ continue
49
+ return out
50
+
51
+
52
+ def _touched(root: str, nodes: dict) -> set:
53
+ """access log 触达集(与 conformance._reach_metrics 同一解析口径:ids / id / node_id / nid)。"""
54
+ p = os.path.join(root, CF.ACCESS_LOG)
55
+ seen = set()
56
+ if not os.path.exists(p):
57
+ return seen
58
+ with open(p, encoding="utf-8", errors="replace") as f:
59
+ for line in f:
60
+ line = line.strip()
61
+ if not line:
62
+ continue
63
+ try:
64
+ rec = json.loads(line)
65
+ except json.JSONDecodeError:
66
+ continue
67
+ got = rec.get("ids")
68
+ if isinstance(got, (list, tuple)):
69
+ seen.update(str(x) for x in got if x)
70
+ nid = rec.get("id") or rec.get("node_id") or rec.get("nid")
71
+ if nid:
72
+ seen.add(str(nid))
73
+ return seen & set(nodes)
74
+
75
+
76
+ def _assertion_items(nodes: dict) -> list:
77
+ """断言集超限项 → 条目级(判据函数与 conformance 同源,不另立口径)。"""
78
+ out = []
79
+ kn = {nid: r for nid, r in nodes.items() if str(r.get("layer")) == "knowledge"}
80
+ for nid, r in kn.items():
81
+ if not r.get("role"):
82
+ out.append({"node_id": nid, "issue_kind": "missing_field",
83
+ "evidence": "role 为空(口径=conformance.stratum.role_coverage,knowledge 层分母)"})
84
+ if CF._as_int(r.get("evidence_count")) == 0:
85
+ out.append({"node_id": nid, "issue_kind": "missing_field",
86
+ "evidence": "evidence_count=0(口径=conformance.stratum.evidence_coverage)"})
87
+ if {"doc", "code"} & CF._tag_prefixes(r):
88
+ out.append({"node_id": nid, "issue_kind": "mixed_layer",
89
+ "evidence": "knowledge 层带 doc/code 标签(口径=conformance.stratum.mixed_ratio)"})
90
+ for nid, r in nodes.items():
91
+ if (CF._as_int(r.get("evidence_count")) == 0 and not CF._basis_of(r)
92
+ and not r.get("lifecycle_state")):
93
+ out.append({"node_id": nid, "issue_kind": "unanalyzed",
94
+ "evidence": "无一维分析痕迹(口径=conformance.unanalyzed 派生判据)"})
95
+ ch = collections.Counter(str(r.get("content_hash")) for r in nodes.values())
96
+ dup = {h for h, c in ch.items() if c > 1 and h not in ("None", "")}
97
+ if dup:
98
+ for nid, r in nodes.items():
99
+ h = str(r.get("content_hash"))
100
+ if h in dup:
101
+ out.append({"node_id": nid, "issue_kind": "dup_content",
102
+ "evidence": "content_hash 重复组 %s(%d 条同指纹;口径=conformance.dup.content)" % (h, ch[h])})
103
+ for nid, r in nodes.items():
104
+ es = r.get("edges") or []
105
+ if not isinstance(es, (list, tuple)):
106
+ continue
107
+ for e in es:
108
+ if not isinstance(e, dict):
109
+ continue
110
+ keys = [k for k in CF.CANONICAL_EDGE_KEYS + ("type",) if e.get(k)]
111
+ if not any(k in CF.CANONICAL_EDGE_KEYS for k in keys):
112
+ out.append({"node_id": nid, "issue_kind": "edge_non_canonical",
113
+ "evidence": "边键非规范:%s(口径=conformance.edge.key.canonical)"
114
+ % ("+".join(keys) or "<无关系键>")})
115
+ break
116
+ return out
117
+
118
+
119
+ def _defer_items(root: str, nodes: dict):
120
+ """闸门待裁决提案 + 遗忘日志 DEFER 留痕。返回 (items, skipped)。"""
121
+ ib = _read_jsonl(os.path.join(root, CF.INBOX_LOG))
122
+ dc = _read_jsonl(os.path.join(root, CF.DECISION_LOG))
123
+ done = {str(x.get("pid")) for x in dc if x.get("pid")}
124
+ out, skipped = [], []
125
+ for r in ib:
126
+ pid = str(r.get("pid") or "")
127
+ if not pid or pid in done:
128
+ continue
129
+ out.append({"node_id": str(r.get("id") or "") or None, "origin": "proposal",
130
+ "proposal_id": pid, "issue_kind": "gate_defer",
131
+ "layer": r.get("layer"), "content": r.get("content") or "",
132
+ "tags": r.get("tags") or [], "content_hash": r.get("payload_hash") or "",
133
+ "evidence": "闸门待裁决提案(inbox 中 %s 未被 decisions 覆盖)" % pid})
134
+ for r in _read_jsonl(os.path.join(root, FORGET_LOG)):
135
+ if str(r.get("verdict")) != "DEFER":
136
+ continue
137
+ nid = str(r.get("node_id") or "")
138
+ if nid and nid in nodes:
139
+ out.append({"node_id": nid, "issue_kind": "gate_defer",
140
+ "evidence": "遗忘闸门 DEFER 留痕:%s" % str(r.get("reason"))})
141
+ else:
142
+ skipped.append({"source": "defer_queue", "reason": "node_not_landed",
143
+ "detail": nid or str(r.get("reason"))})
144
+ return out, skipped
145
+
146
+
147
+ def _cold_items(root: str, nodes: dict, limit: int) -> list:
148
+ seen = _touched(root, nodes)
149
+
150
+ def _imp(r):
151
+ try:
152
+ return float(r.get("importance") or 0.0)
153
+ except (TypeError, ValueError):
154
+ return 0.0
155
+
156
+ rows = [(nid, r) for nid, r in nodes.items() if nid not in seen]
157
+ rows.sort(key=lambda kv: (-_imp(kv[1]), kv[0]))
158
+ picked = [(nid, r) for nid, r in rows if _imp(r) >= COLD_IMPORTANCE_MIN][:int(limit)]
159
+ return [{"node_id": nid, "issue_kind": "cold",
160
+ "evidence": "access log 未触达(importance=%.2f,排名第 %d)" % (_imp(r), i + 1)}
161
+ for i, (nid, r) in enumerate(picked)]
162
+
163
+
164
+ def _patrol_items(nodes: dict, window: int, offset: int) -> list:
165
+ if int(window) <= 0 or not nodes:
166
+ return []
167
+ ordered = sorted(nodes, key=lambda nid: hashlib.sha1(nid.encode("utf-8")).hexdigest())
168
+ n = len(ordered)
169
+ start = (int(offset) * int(window)) % n
170
+ picked = ordered[start:start + int(window)]
171
+ if len(picked) < int(window): # 环绕补足:多轮不重不漏
172
+ picked = picked + ordered[:int(window) - len(picked)]
173
+ return [{"node_id": nid, "issue_kind": "patrol",
174
+ "evidence": "轮巡窗口 #%d(窗口 %d 条,按 sha1(node_id) 排序切片)" % (int(offset), int(window))}
175
+ for nid in picked]
176
+
177
+
178
+ def _merge(items: list, nodes: dict) -> list:
179
+ """按条目(节点/提案)聚合:一条候选 = 一个待评条目,多个问题挂在 issue_kinds 上。"""
180
+ acc = {}
181
+ for it in items:
182
+ nid = it.get("node_id")
183
+ key = str(nid) if nid else "pid:" + str(it.get("proposal_id"))
184
+ rec = acc.get(key)
185
+ if rec is None:
186
+ meta = nodes.get(nid) or {} if nid else {}
187
+ rec = acc[key] = {
188
+ "key": key, "node_id": nid or None, "proposal_id": it.get("proposal_id") or None,
189
+ "origin": it.get("origin") or "node",
190
+ "layer": it.get("layer") or meta.get("layer"),
191
+ "sources": [], "issue_kinds": [], "evidence": [],
192
+ "content": it.get("content") or "",
193
+ "tags": list(it.get("tags") or meta.get("tags") or []),
194
+ "content_hash": it.get("content_hash") or str(meta.get("content_hash") or ""),
195
+ }
196
+ src = it.get("source")
197
+ if src and src not in rec["sources"]:
198
+ rec["sources"].append(src)
199
+ ik = it.get("issue_kind")
200
+ if ik and ik not in rec["issue_kinds"]:
201
+ rec["issue_kinds"].append(ik)
202
+ ev = it.get("evidence")
203
+ if ev and len(rec["evidence"]) < 8:
204
+ rec["evidence"].append(ev)
205
+ out = list(acc.values())
206
+ for r in out:
207
+ r["sources"] = sorted(set(r["sources"])) or ["-"]
208
+ r["priority"] = min(_PRIORITY.get(s, 9) for s in r["sources"])
209
+ r["issue_kinds"] = sorted(r["issue_kinds"])
210
+ out.sort(key=lambda r: (r["priority"], str(r["key"])))
211
+ return out
212
+
213
+
214
+ def generate(root=None, *, sources=SOURCES, nodes=None, limit=None, cold_limit=500,
215
+ patrol_window=200, patrol_offset=None, with_report=False, now=None) -> dict:
216
+ """四源候选生成。nodes 可注入(测试用合成库,免读真源)。"""
217
+ root = root or mdcg_root()
218
+ if nodes is None:
219
+ nodes = CF.load_index(root) or {} # load_index 直接返回 nodes 字典
220
+ wanted = [s for s in SOURCES if s in (sources or SOURCES)]
221
+ items, skipped, by_source = [], [], {}
222
+ if "assertion" in wanted:
223
+ got = _assertion_items(nodes)
224
+ by_source["assertion"] = len(got)
225
+ items += [dict(x, source="assertion") for x in got]
226
+ if "defer_queue" in wanted:
227
+ got, sk = _defer_items(root, nodes)
228
+ by_source["defer_queue"] = len(got)
229
+ items += [dict(x, source="defer_queue") for x in got]
230
+ skipped += sk
231
+ if "cold" in wanted:
232
+ got = _cold_items(root, nodes, cold_limit)
233
+ by_source["cold"] = len(got)
234
+ items += [dict(x, source="cold") for x in got]
235
+ if "patrol" in wanted:
236
+ off = int(patrol_offset) if patrol_offset is not None \
237
+ else int((now if now is not None else time.time()) // PATROL_EPOCH)
238
+ got = _patrol_items(nodes, patrol_window, off)
239
+ by_source["patrol"] = len(got)
240
+ items += [dict(x, source="patrol") for x in got]
241
+ merged = _merge(items, nodes)
242
+ if limit:
243
+ merged = merged[:int(limit)]
244
+ rep = {"root": root, "generated_at": float(now if now is not None else time.time()),
245
+ "sources": wanted, "by_source": by_source, "total_raw": len(items),
246
+ "total": len(merged), "candidates": merged, "skipped": skipped}
247
+ if with_report:
248
+ rep0 = CF.check(root, check_paths=False)
249
+ rep["report"] = CF.metrics_of(rep0)
250
+ rep["assertions"] = {"verdict": rep0.get("verdict"), "counts": rep0.get("counts"),
251
+ "failed": [c.get("id") for c in rep0.get("checks") or []
252
+ if not c.get("ok")]}
253
+ return rep