rdstudio 0.2.0__py3-none-any.whl

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 (251) hide show
  1. rdstudio/__init__.py +3 -0
  2. rdstudio/_node/rdstudio.mjs +59351 -0
  3. rdstudio/brief.py +46 -0
  4. rdstudio/build.py +218 -0
  5. rdstudio/classify.py +131 -0
  6. rdstudio/cli.py +369 -0
  7. rdstudio/config.py +115 -0
  8. rdstudio/gitlog.py +92 -0
  9. rdstudio/launcher.py +63 -0
  10. rdstudio/learner.py +187 -0
  11. rdstudio/mcp_server.py +313 -0
  12. rdstudio/okf.py +775 -0
  13. rdstudio/procedures.py +233 -0
  14. rdstudio/references.py +228 -0
  15. rdstudio/reports.py +75 -0
  16. rdstudio/scaffold.py +227 -0
  17. rdstudio/scopes.py +162 -0
  18. rdstudio/search.py +149 -0
  19. rdstudio/serve.py +214 -0
  20. rdstudio/store.py +161 -0
  21. rdstudio/templates/agents/critic.md +20 -0
  22. rdstudio/templates/agents/librarian.md +21 -0
  23. rdstudio/templates/agents/searcher.md +21 -0
  24. rdstudio/templates/agents_section.md +31 -0
  25. rdstudio/templates/claude_section.md +3 -0
  26. rdstudio/templates/knowledge/overview.md +20 -0
  27. rdstudio/templates/knowledge/tasks/bootstrap-knowledge-base.md +45 -0
  28. rdstudio/templates/skills/decision/SKILL.md +47 -0
  29. rdstudio/templates/skills/handoff/SKILL.md +18 -0
  30. rdstudio/templates/skills/ingest-ref/SKILL.md +32 -0
  31. rdstudio/templates/skills/lint-okf/SKILL.md +15 -0
  32. rdstudio/templates/skills/promote/SKILL.md +25 -0
  33. rdstudio/templates/skills/question/SKILL.md +33 -0
  34. rdstudio/templates/skills/record-okf/SKILL.md +61 -0
  35. rdstudio/templates/skills/report/SKILL.md +46 -0
  36. rdstudio/templates/skills/report/template.html +74 -0
  37. rdstudio/templates/skills/search-okf/SKILL.md +36 -0
  38. rdstudio/templates/skills/task/SKILL.md +51 -0
  39. rdstudio/web/app.js +140 -0
  40. rdstudio/web/fonts.css +48 -0
  41. rdstudio/web/icon-192.png +0 -0
  42. rdstudio/web/icon-512.png +0 -0
  43. rdstudio/web/index.html +109 -0
  44. rdstudio/web/js/data.js +125 -0
  45. rdstudio/web/js/diagrams.js +116 -0
  46. rdstudio/web/js/graph.js +396 -0
  47. rdstudio/web/js/knowledge.js +212 -0
  48. rdstudio/web/js/layout-worker.js +15 -0
  49. rdstudio/web/js/layout.js +226 -0
  50. rdstudio/web/js/learn.js +83 -0
  51. rdstudio/web/js/map.js +1293 -0
  52. rdstudio/web/js/markdown.js +173 -0
  53. rdstudio/web/js/pages.js +219 -0
  54. rdstudio/web/js/procedures.js +262 -0
  55. rdstudio/web/js/settings.js +95 -0
  56. rdstudio/web/js/util.js +99 -0
  57. rdstudio/web/report.css +35 -0
  58. rdstudio/web/report.js +104 -0
  59. rdstudio/web/style.css +566 -0
  60. rdstudio/web/sw.js +166 -0
  61. rdstudio/web/themes/cyber.css +139 -0
  62. rdstudio/web/themes/map.css +133 -0
  63. rdstudio/web/themes/notebook.css +134 -0
  64. rdstudio/web/themes/space.css +135 -0
  65. rdstudio/web/themes/studio.css +110 -0
  66. rdstudio/web/vendor/d3.min.js +2 -0
  67. rdstudio/web/vendor/fonts/atkinson-hyperlegible-next-latin-ext-wght-italic.woff2 +0 -0
  68. rdstudio/web/vendor/fonts/atkinson-hyperlegible-next-latin-ext-wght-normal.woff2 +0 -0
  69. rdstudio/web/vendor/fonts/atkinson-hyperlegible-next-latin-wght-italic.woff2 +0 -0
  70. rdstudio/web/vendor/fonts/atkinson-hyperlegible-next-latin-wght-normal.woff2 +0 -0
  71. rdstudio/web/vendor/fonts/caveat-latin-wght-normal.woff2 +0 -0
  72. rdstudio/web/vendor/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  73. rdstudio/web/vendor/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  74. rdstudio/web/vendor/fonts/ibm-plex-sans-latin-wght-italic.woff2 +0 -0
  75. rdstudio/web/vendor/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
  76. rdstudio/web/vendor/fonts/inter-latin-opsz-italic.woff2 +0 -0
  77. rdstudio/web/vendor/fonts/inter-latin-opsz-normal.woff2 +0 -0
  78. rdstudio/web/vendor/fonts/jetbrains-mono-latin-wght-italic.woff2 +0 -0
  79. rdstudio/web/vendor/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  80. rdstudio/web/vendor/fonts/literata-latin-ext-opsz-italic.woff2 +0 -0
  81. rdstudio/web/vendor/fonts/literata-latin-ext-opsz-normal.woff2 +0 -0
  82. rdstudio/web/vendor/fonts/literata-latin-opsz-italic.woff2 +0 -0
  83. rdstudio/web/vendor/fonts/literata-latin-opsz-normal.woff2 +0 -0
  84. rdstudio/web/vendor/fonts/source-sans-3-latin-wght-italic.woff2 +0 -0
  85. rdstudio/web/vendor/fonts/source-sans-3-latin-wght-normal.woff2 +0 -0
  86. rdstudio/web/vendor/fonts/source-serif-4-latin-opsz-italic.woff2 +0 -0
  87. rdstudio/web/vendor/fonts/source-serif-4-latin-opsz-normal.woff2 +0 -0
  88. rdstudio/web/vendor/hljs/github-dark.min.css +10 -0
  89. rdstudio/web/vendor/hljs/github.min.css +10 -0
  90. rdstudio/web/vendor/hljs/highlight.min.js +1262 -0
  91. rdstudio/web/vendor/hljs/julia.min.js +18 -0
  92. rdstudio/web/vendor/hljs/latex.min.js +33 -0
  93. rdstudio/web/vendor/hljs/matlab.min.js +15 -0
  94. rdstudio/web/vendor/katex/auto-render.min.js +1 -0
  95. rdstudio/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  96. rdstudio/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  97. rdstudio/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  98. rdstudio/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  99. rdstudio/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  100. rdstudio/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  101. rdstudio/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  102. rdstudio/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  103. rdstudio/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  104. rdstudio/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  105. rdstudio/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  106. rdstudio/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  107. rdstudio/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  108. rdstudio/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  109. rdstudio/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  110. rdstudio/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  111. rdstudio/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  112. rdstudio/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  113. rdstudio/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  114. rdstudio/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  115. rdstudio/web/vendor/katex/katex.min.css +1 -0
  116. rdstudio/web/vendor/katex/katex.min.js +1 -0
  117. rdstudio/web/vendor/licenses/atkinson-hyperlegible-next-OFL.txt +93 -0
  118. rdstudio/web/vendor/licenses/caveat-OFL.txt +93 -0
  119. rdstudio/web/vendor/licenses/d3-7.9.0.txt +13 -0
  120. rdstudio/web/vendor/licenses/highlightjs-cdn-assets-11.12.0.txt +29 -0
  121. rdstudio/web/vendor/licenses/ibm-plex-mono-OFL.txt +93 -0
  122. rdstudio/web/vendor/licenses/ibm-plex-sans-OFL.txt +93 -0
  123. rdstudio/web/vendor/licenses/inter-OFL.txt +93 -0
  124. rdstudio/web/vendor/licenses/jetbrains-mono-OFL.txt +93 -0
  125. rdstudio/web/vendor/licenses/katex-0.18.7.txt +21 -0
  126. rdstudio/web/vendor/licenses/literata-OFL.txt +93 -0
  127. rdstudio/web/vendor/licenses/markdown-it-15.0.2.txt +22 -0
  128. rdstudio/web/vendor/licenses/markdown-it-footnote-4.0.0.txt +22 -0
  129. rdstudio/web/vendor/licenses/markdown-it-texmath-1.0.0.txt +21 -0
  130. rdstudio/web/vendor/licenses/mermaid-12.0.0.txt +21 -0
  131. rdstudio/web/vendor/licenses/source-sans-3-OFL.txt +93 -0
  132. rdstudio/web/vendor/licenses/source-serif-4-OFL.txt +93 -0
  133. rdstudio/web/vendor/licenses/vega-6.4.0.txt +27 -0
  134. rdstudio/web/vendor/licenses/vega-embed-7.2.0.txt +27 -0
  135. rdstudio/web/vendor/licenses/vega-lite-6.4.3.txt +27 -0
  136. rdstudio/web/vendor/markdown-it-footnote.min.js +2 -0
  137. rdstudio/web/vendor/markdown-it.min.js +14 -0
  138. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/abnfDiagram-DGLNOSUI.mjs +1 -0
  139. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/architecture-7GRP2DOG-HF4J73XS.mjs +1 -0
  140. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/architectureDiagram-IU6H5EVY.mjs +36 -0
  141. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/blockDiagram-FSX6DT2V.mjs +140 -0
  142. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/c4Diagram-YNRVYILN.mjs +38 -0
  143. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-22SYA5HA.mjs +1 -0
  144. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-2FTYKNOQ.mjs +1 -0
  145. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-36VM3LGA.mjs +1 -0
  146. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-3SEI7E7V.mjs +1 -0
  147. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-3T2MNPEU.mjs +1 -0
  148. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-44AU3KAP.mjs +174 -0
  149. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-4MZOTL6B.mjs +1 -0
  150. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-52IMQVNL.mjs +1 -0
  151. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-5NYYYZGL.mjs +1 -0
  152. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-BOZ75642.mjs +15 -0
  153. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-CVBX6EWK.mjs +1 -0
  154. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-D36F7FYG.mjs +14 -0
  155. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-DEGLBX6O.mjs +213 -0
  156. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-F6PIGZHG.mjs +1 -0
  157. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-FE53TMRB.mjs +133 -0
  158. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-FNR2RGIF.mjs +1 -0
  159. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-GDOATIN3.mjs +32 -0
  160. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-H7BX327W.mjs +2 -0
  161. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-HI3EPXIC.mjs +2 -0
  162. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-JCBDXJ5W.mjs +1 -0
  163. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-JGXDHUYS.mjs +1 -0
  164. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-L2B47AAZ.mjs +1 -0
  165. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-LIEGGFIX.mjs +1 -0
  166. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-LLWYPJEZ.mjs +1 -0
  167. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-MFYRHTYG.mjs +1 -0
  168. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-MIE3LDYP.mjs +1 -0
  169. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-MLQVGYRW.mjs +1 -0
  170. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-MNT3HHAC.mjs +1 -0
  171. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-NRRTYJAG.mjs +1 -0
  172. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-OCMJQKYE.mjs +10 -0
  173. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-PKJCTQEK.mjs +339 -0
  174. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-QADHU5CF.mjs +2 -0
  175. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-QVIEFLGF.mjs +1 -0
  176. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-QX7LGXZU.mjs +1 -0
  177. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-R2KH53MI.mjs +1 -0
  178. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-RXM7Z7NK.mjs +2 -0
  179. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-S4TE5DX6.mjs +1 -0
  180. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-SDIBIOXT.mjs +1 -0
  181. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-TDFULVG6.mjs +1 -0
  182. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-TECWHJ6D.mjs +62 -0
  183. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-TINYC7RT.mjs +70 -0
  184. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-V3KJR4XU.mjs +1 -0
  185. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-VTMXW4ES.mjs +1 -0
  186. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-Y5NT6LW5.mjs +88 -0
  187. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-Y5O5OPKC.mjs +3 -0
  188. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-YZG4BXY6.mjs +1 -0
  189. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZZ52EIBP.mjs +1 -0
  190. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-v2-VV4INRHB.mjs +217 -0
  191. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/cose-bilkent-MSIMRU2B.mjs +1 -0
  192. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/cynefin-OW5HDTMX-KMPDV6GS.mjs +1 -0
  193. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/cynefinDiagram-KTGLXFXK.mjs +62 -0
  194. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/dagre-QJJCSHY4.mjs +4 -0
  195. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-2ZIM67RP.mjs +3 -0
  196. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-FYHH5O6R.mjs +24 -0
  197. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-LYLDACMS.mjs +200 -0
  198. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-TYT3HRBC.mjs +24 -0
  199. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-XVIN6YVD.mjs +30 -0
  200. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-ZO2NLLYC.mjs +41 -0
  201. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/ebnfDiagram-TU3FGH35.mjs +1 -0
  202. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/elk-GHAXNSLK.mjs +27 -0
  203. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/erDiagram-XID3VV23.mjs +99 -0
  204. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/eventmodeling-NTZA5JFV-P5QJSWSK.mjs +1 -0
  205. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/flowDiagram-WPHQAKTU.mjs +1 -0
  206. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/ganttDiagram-QACGSKG6.mjs +292 -0
  207. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/gitGraph-4MIJSDKK-V5NEQQYP.mjs +1 -0
  208. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/gitGraphDiagram-EB76O4HC.mjs +106 -0
  209. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/info-A6RAGUB7-IGBHH2L7.mjs +1 -0
  210. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/infoDiagram-343ZKIDN.mjs +2 -0
  211. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/ishikawaDiagram-UEEU5D75.mjs +70 -0
  212. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/journeyDiagram-4QTZOVLF.mjs +139 -0
  213. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/kanban-definition-VEJL4RG2.mjs +89 -0
  214. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/katex-QVRI4OIV.mjs +257 -0
  215. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/mindmap-definition-C5K5ZT7T.mjs +96 -0
  216. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/packet-AYTQ26CC-PJTKDECJ.mjs +1 -0
  217. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/pegDiagram-FHIILVJB.mjs +1 -0
  218. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/pie-WAS4IAKB-U3JFQXZN.mjs +1 -0
  219. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/pieDiagram-S76BU6V7.mjs +39 -0
  220. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/quadrantDiagram-HZBLWUZE.mjs +7 -0
  221. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/radar-RG4KPBEZ-7T2UW32D.mjs +1 -0
  222. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroad-74A4TZTK-DZY7ARDL.mjs +1 -0
  223. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroad-abnf-HS5TGJTU-LWS535ZK.mjs +1 -0
  224. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroad-ebnf-LZEXJU2U-EA3S4ZFM.mjs +1 -0
  225. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroad-peg-WCYAUIDC-DUKZONPX.mjs +1 -0
  226. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroadDiagram-R6RP3MQ2.mjs +1 -0
  227. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/requirementDiagram-SGBABQV6.mjs +84 -0
  228. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/sankeyDiagram-IM6M5BOW.mjs +40 -0
  229. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/sequenceDiagram-FGJOIFJ2.mjs +169 -0
  230. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/sizeCapture-ERV5D3W5.mjs +1 -0
  231. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-v2-6KDGCZ5J.mjs +291 -0
  232. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/swimlanes-OJBUSBEF.mjs +1 -0
  233. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/swimlanesDiagram-LNXYTHDW.mjs +8 -0
  234. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/timeline-definition-7G6YQRTY.mjs +120 -0
  235. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/treeView-Q6P3EWNA-AABPVERA.mjs +1 -0
  236. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/treemap-WGGIJYW6-F2JSY3V3.mjs +1 -0
  237. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/usecaseDiagram-6Y6TMLBO.mjs +383 -0
  238. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/vennDiagram-6RODYPCK.mjs +34 -0
  239. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/wardley-WFR3VGLG-EIT5NWVN.mjs +1 -0
  240. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/wardleyDiagram-RFZKLAPA.mjs +78 -0
  241. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/xychartDiagram-TWWI2QJZ.mjs +7 -0
  242. rdstudio/web/vendor/mermaid/mermaid.esm.min.mjs +14 -0
  243. rdstudio/web/vendor/texmath.js +368 -0
  244. rdstudio/web/vendor/vega/vega-embed.min.js +7 -0
  245. rdstudio/web/vendor/vega/vega-lite.min.js +2 -0
  246. rdstudio/web/vendor/vega/vega.min.js +1 -0
  247. rdstudio-0.2.0.dist-info/METADATA +201 -0
  248. rdstudio-0.2.0.dist-info/RECORD +251 -0
  249. rdstudio-0.2.0.dist-info/WHEEL +4 -0
  250. rdstudio-0.2.0.dist-info/entry_points.txt +3 -0
  251. rdstudio-0.2.0.dist-info/licenses/LICENSE +21 -0
rdstudio/scaffold.py ADDED
@@ -0,0 +1,227 @@
1
+ """``rdstudio init``: scaffold rdstudio into a repository, idempotently."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import getpass
6
+ import json
7
+ import re
8
+ import shutil
9
+ from pathlib import Path
10
+ from typing import Iterator
11
+
12
+ from . import config as config_mod
13
+ from .okf import Bundle, dump_frontmatter, split_frontmatter
14
+
15
+ TEMPLATES = Path(__file__).parent / "templates"
16
+ SECTION_START = "<!-- rdstudio:start"
17
+ SECTION_END = "<!-- rdstudio:end -->"
18
+ HOOK_COMMAND = "rdstudio brief"
19
+ OPENCODE_AGENT = "opencode/unknown"
20
+
21
+ # OpenCode subagent permissions; Claude Code's equivalent is each agent's `tools` list.
22
+ OPENCODE_PERMISSIONS: dict[str, dict[str, str]] = {
23
+ "librarian": {"edit": "deny", "bash": "deny", "webfetch": "deny"},
24
+ "critic": {"edit": "deny", "bash": "deny", "webfetch": "deny"},
25
+ "searcher": {"edit": "deny"},
26
+ }
27
+
28
+
29
+ def _fill(text: str, values: dict[str, str]) -> str:
30
+ for key, value in values.items():
31
+ text = text.replace("{" + key + "}", value)
32
+ return text
33
+
34
+
35
+ def _write(path: Path, content: str, root: Path, *, force: bool = False) -> str | None:
36
+ rel = path.relative_to(root).as_posix()
37
+ if path.exists():
38
+ if not force or path.read_text(encoding="utf-8") == content:
39
+ return None
40
+ path.write_text(content, encoding="utf-8")
41
+ return f"updated {rel}"
42
+ path.parent.mkdir(parents=True, exist_ok=True)
43
+ path.write_text(content, encoding="utf-8")
44
+ return f"created {rel}"
45
+
46
+
47
+ def _uses_dev_dependency(root: Path) -> bool:
48
+ pyproject = root / "pyproject.toml"
49
+ return pyproject.is_file() and re.search(r"[\"']rdstudio\b", pyproject.read_text(encoding="utf-8")) is not None
50
+
51
+
52
+ def mcp_command(root: Path) -> dict[str, object]:
53
+ if _uses_dev_dependency(root) or not shutil.which("rdstudio"):
54
+ return {"command": "uv", "args": ["run", "rdstudio", "mcp"]}
55
+ return {"command": "rdstudio", "args": ["mcp"]}
56
+
57
+
58
+ def opencode_agent(src: str, name: str) -> str:
59
+ """Translate a Claude Code subagent file into an OpenCode one."""
60
+ meta, body = split_frontmatter(src)
61
+ meta = meta or {}
62
+ out = {"description": meta.get("description", name), "mode": "subagent"}
63
+ if name in OPENCODE_PERMISSIONS:
64
+ out["permission"] = OPENCODE_PERMISSIONS[name]
65
+ return f"---\n{dump_frontmatter(out)}---\n{body}"
66
+
67
+
68
+ def _merge_json(path: Path, root: Path, update) -> str | None:
69
+ data = {}
70
+ if path.exists():
71
+ try:
72
+ data = json.loads(path.read_text(encoding="utf-8") or "{}")
73
+ except json.JSONDecodeError:
74
+ return f"skipped {path.relative_to(root).as_posix()} (not valid JSON; add rdstudio by hand)"
75
+ before = json.dumps(data, sort_keys=True)
76
+ update(data)
77
+ if json.dumps(data, sort_keys=True) == before:
78
+ return None
79
+ existed = path.exists()
80
+ path.parent.mkdir(parents=True, exist_ok=True)
81
+ path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
82
+ return f"{'updated' if existed else 'created'} {path.relative_to(root).as_posix()}"
83
+
84
+
85
+ def _managed_section(path: Path, section: str, root: Path, *, create: bool) -> str | None:
86
+ rel = path.relative_to(root).as_posix()
87
+ if not path.exists():
88
+ if not create:
89
+ return None
90
+ path.write_text(f"# {root.name}\n\n{section}", encoding="utf-8")
91
+ return f"created {rel}"
92
+ text = path.read_text(encoding="utf-8")
93
+ if SECTION_START in text and SECTION_END in text:
94
+ start = text.index(SECTION_START)
95
+ end = text.index(SECTION_END) + len(SECTION_END)
96
+ new = text[:start] + section.strip() + text[end:]
97
+ else:
98
+ new = text.rstrip() + "\n\n" + section
99
+ if new == text:
100
+ return None
101
+ path.write_text(new, encoding="utf-8")
102
+ return f"updated {rel}"
103
+
104
+
105
+ def init(root: Path, *, title: str | None = None, human: str | None = None, force: bool = False) -> Iterator[str]:
106
+ root = root.resolve()
107
+ root.mkdir(parents=True, exist_ok=True)
108
+
109
+ toml = root / config_mod.PROJECT_FILE
110
+ if not toml.exists():
111
+ human = human or config_mod.load(root).human or f"human:{getpass.getuser()}"
112
+ if not human.startswith("human:"):
113
+ human = f"human:{human}"
114
+ toml.write_text(
115
+ "# rdstudio project configuration\n"
116
+ "[project]\n"
117
+ f'title = "{title or root.name}"\n\n'
118
+ "[paths]\n"
119
+ 'knowledge = "knowledge"\n'
120
+ 'reports = "reports"\n\n'
121
+ "[actors]\n"
122
+ f'human = "{human}"\n'
123
+ 'agent = "claude-code/claude"\n\n'
124
+ "# Which files count as which kind of change in the Changes tab.\n"
125
+ "# [changes.categories]\n"
126
+ '# code = ["src/**", "tests/**"]\n',
127
+ encoding="utf-8",
128
+ )
129
+ yield f"created {config_mod.PROJECT_FILE}"
130
+ cfg = config_mod.load(root)
131
+ values = {
132
+ "title": cfg.title,
133
+ "knowledge": cfg.knowledge,
134
+ "reports": cfg.reports,
135
+ "human": cfg.human or "human:<you>",
136
+ "agent": cfg.agent,
137
+ }
138
+
139
+ # Knowledge bundle placeholders (never overwritten).
140
+ for src in sorted((TEMPLATES / "knowledge").rglob("*.md")):
141
+ dst = cfg.knowledge_dir / src.relative_to(TEMPLATES / "knowledge")
142
+ if msg := _write(dst, _fill(src.read_text(encoding="utf-8"), values), root):
143
+ yield msg
144
+
145
+ cfg.reports_dir.mkdir(parents=True, exist_ok=True)
146
+ if msg := _write(cfg.reports_dir / ".gitkeep", "", root):
147
+ yield msg
148
+
149
+ # Skills and agents (rdstudio-managed: --force refreshes them).
150
+ for src in sorted((TEMPLATES / "skills").rglob("*")):
151
+ if src.is_file():
152
+ dst = root / ".claude" / "skills" / src.relative_to(TEMPLATES / "skills")
153
+ if msg := _write(dst, _fill(src.read_text(encoding="utf-8"), values), root, force=force):
154
+ yield msg
155
+ for src in sorted((TEMPLATES / "agents").glob("*.md")):
156
+ text = _fill(src.read_text(encoding="utf-8"), values)
157
+ if msg := _write(root / ".claude" / "agents" / src.name, text, root, force=force):
158
+ yield msg
159
+ if msg := _write(root / ".opencode" / "agents" / src.name, opencode_agent(text, src.stem), root, force=force):
160
+ yield msg
161
+
162
+ # MCP registration and Claude Code settings.
163
+ command = mcp_command(root)
164
+
165
+ def add_server(data: dict) -> None:
166
+ data.setdefault("mcpServers", {})["rdstudio"] = command
167
+
168
+ if msg := _merge_json(root / ".mcp.json", root, add_server):
169
+ yield msg
170
+
171
+ def add_settings(data: dict) -> None:
172
+ enabled = data.setdefault("enabledMcpjsonServers", [])
173
+ if "rdstudio" not in enabled:
174
+ enabled.append("rdstudio")
175
+ allow = data.setdefault("permissions", {}).setdefault("allow", [])
176
+ if "mcp__rdstudio" not in allow:
177
+ allow.append("mcp__rdstudio")
178
+ hooks = data.setdefault("hooks", {}).setdefault("SessionStart", [])
179
+ brief = HOOK_COMMAND if command["command"] == "rdstudio" else "uv run " + HOOK_COMMAND
180
+ present = any(h.get("command", "").endswith(HOOK_COMMAND) for entry in hooks for h in entry.get("hooks", []))
181
+ if not present:
182
+ hooks.append({"hooks": [{"type": "command", "command": brief}]})
183
+
184
+ if msg := _merge_json(root / ".claude" / "settings.json", root, add_settings):
185
+ yield msg
186
+
187
+ # OpenCode: MCP registration. It reads skills from .claude/skills directly.
188
+ def add_opencode(data: dict) -> None:
189
+ data.setdefault("$schema", "https://opencode.ai/config.json")
190
+ data.setdefault("mcp", {})["rdstudio"] = {
191
+ "type": "local",
192
+ "command": [command["command"], *command["args"], "--agent", OPENCODE_AGENT],
193
+ "enabled": True,
194
+ }
195
+
196
+ jsonc = root / "opencode.jsonc"
197
+ if jsonc.exists() and not (root / "opencode.json").exists():
198
+ msg = _merge_json(jsonc, root, add_opencode)
199
+ else:
200
+ msg = _merge_json(root / "opencode.json", root, add_opencode)
201
+ if msg:
202
+ yield msg
203
+
204
+ # Instructions: the shared section lives in AGENTS.md (read by OpenCode, Codex
205
+ # and others); CLAUDE.md imports it.
206
+ section = _fill((TEMPLATES / "agents_section.md").read_text(encoding="utf-8"), values)
207
+ if msg := _managed_section(root / "AGENTS.md", section, root, create=True):
208
+ yield msg
209
+ claude_md = root / "CLAUDE.md"
210
+ if not (claude_md.is_symlink() and claude_md.resolve() == (root / "AGENTS.md").resolve()):
211
+ section = (TEMPLATES / "claude_section.md").read_text(encoding="utf-8")
212
+ if msg := _managed_section(claude_md, section, root, create=True):
213
+ yield msg
214
+
215
+ gitignore = root / ".gitignore"
216
+ ignore_line = cfg.output.strip("/") + "/"
217
+ lines = gitignore.read_text(encoding="utf-8").splitlines() if gitignore.exists() else []
218
+ if ignore_line not in lines and f"/{ignore_line}" not in lines:
219
+ with gitignore.open("a", encoding="utf-8") as fh:
220
+ prefix = "\n" if lines and lines[-1].strip() else ""
221
+ fh.write(f"{prefix}# rdstudio build output\n{ignore_line}\n")
222
+ yield "updated .gitignore"
223
+
224
+ for rel in Bundle.load(cfg.knowledge_dir).write_indexes():
225
+ yield f"wrote {cfg.knowledge}/{rel}"
226
+
227
+ yield "done. Next: `rdstudio serve` for the dashboard; start an agent session (Claude Code, OpenCode, ...) and run the bootstrap task."
rdstudio/scopes.py ADDED
@@ -0,0 +1,162 @@
1
+ """The global knowledge base, promotion between bundles, and skill scopes.
2
+
3
+ The global knowledge base is an ordinary rdstudio root (by default
4
+ ``~/knowledge``, a private git repository) named in the user configuration::
5
+
6
+ # ~/.config/rdstudio/config.toml
7
+ [global]
8
+ path = "~/knowledge"
9
+
10
+ Bundles never link to each other. Knowledge moves from a project to the global
11
+ base by promotion; the project export never includes global content.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import shutil
17
+ import subprocess
18
+ from pathlib import Path
19
+ from typing import Any, Iterator
20
+
21
+ from . import config as config_mod
22
+ from .config import Config
23
+ from .okf import Bundle, render_concept, split_frontmatter
24
+ from .store import concept_path
25
+
26
+
27
+ class ScopeError(RuntimeError):
28
+ pass
29
+
30
+
31
+ def global_config(cfg: Config) -> Config | None:
32
+ """The global knowledge base's configuration, if one is set up and enabled."""
33
+ if not cfg.use_global or cfg.global_bundle is None:
34
+ return None
35
+ root = cfg.global_bundle.expanduser()
36
+ if not (root / config_mod.PROJECT_FILE).is_file():
37
+ return None
38
+ if cfg.is_project and root.resolve() == cfg.root.resolve():
39
+ return None # we are the global base
40
+ return config_mod.load(root)
41
+
42
+
43
+ def scoped(cfg: Config, scope: str) -> list[tuple[str, Config]]:
44
+ """``[(name, config)]`` for ``project`` | ``global`` | ``all``."""
45
+ if scope not in ("project", "global", "all"):
46
+ raise ScopeError(f"unknown scope {scope!r}; use project, global or all")
47
+ out: list[tuple[str, Config]] = []
48
+ if scope in ("project", "all"):
49
+ out.append(("project", cfg))
50
+ if scope in ("global", "all"):
51
+ g = global_config(cfg)
52
+ if g is None:
53
+ if scope == "global":
54
+ raise ScopeError("no global knowledge base (run `rdstudio global init`)")
55
+ else:
56
+ out.append(("global", g))
57
+ return out
58
+
59
+
60
+ def _set_user_global(path: Path) -> str:
61
+ user = config_mod.user_config_path()
62
+ text = user.read_text(encoding="utf-8") if user.exists() else ""
63
+ line = f'path = "{path}"'
64
+ if "[global]" in text:
65
+ lines = text.splitlines()
66
+ start = lines.index("[global]")
67
+ end = next((i for i in range(start + 1, len(lines)) if lines[i].startswith("[")), len(lines))
68
+ block = [l for l in lines[start + 1 : end] if not l.strip().startswith("path")]
69
+ lines[start + 1 : end] = [line, *block]
70
+ text = "\n".join(lines) + "\n"
71
+ else:
72
+ text = (text.rstrip() + "\n\n" if text.strip() else "") + f"[global]\n{line}\n"
73
+ user.parent.mkdir(parents=True, exist_ok=True)
74
+ user.write_text(text, encoding="utf-8")
75
+ return f"set [global] path in {user}"
76
+
77
+
78
+ def init_global(path: Path, *, human: str | None = None) -> Iterator[str]:
79
+ from .scaffold import init
80
+
81
+ path = path.expanduser().resolve()
82
+ yield from init(path, title="Global knowledge", human=human)
83
+ if not (path / ".git").exists():
84
+ subprocess.run(["git", "init", "-q", "-b", "main", str(path)], check=False)
85
+ yield f"initialised a git repository in {path} (private: add no remote unless you mean to)"
86
+ yield _set_user_global(path)
87
+
88
+
89
+ def promote(cfg: Config, cid: str, *, as_id: str | None = None, keep: bool = False,
90
+ force: bool = False) -> dict[str, Any]:
91
+ """Move (or copy, with ``keep``) a project concept into the global base."""
92
+ g = global_config(cfg)
93
+ if g is None:
94
+ raise ScopeError("no global knowledge base (run `rdstudio global init`)")
95
+ project = Bundle.load(cfg.knowledge_dir)
96
+ source_id = project.resolve_id(cid)
97
+ if source_id is None:
98
+ raise ScopeError(f"no project concept {cid!r}")
99
+ backlinks = project.backlinks(source_id)
100
+ if backlinks and not keep and not force:
101
+ raise ScopeError(
102
+ "project concepts link to it: " + ", ".join(backlinks)
103
+ + ". Use --keep to copy instead of move, or --force to move anyway."
104
+ )
105
+ target = concept_path(g.knowledge_dir, as_id or source_id)
106
+ if target.exists():
107
+ raise ScopeError(f"the global base already has {target.relative_to(g.knowledge_dir)}")
108
+ src = concept_path(cfg.knowledge_dir, source_id)
109
+ meta, body = split_frontmatter(src.read_text(encoding="utf-8"))
110
+ meta = dict(meta or {})
111
+ sources = list(meta.get("sources") or [])
112
+ sources.append({"resource": f"project {cfg.title}: {source_id}", "title": "Promoted from a project"})
113
+ meta["sources"] = sources
114
+ target.parent.mkdir(parents=True, exist_ok=True)
115
+ target.write_text(render_concept(meta, body), encoding="utf-8")
116
+ if not keep:
117
+ src.unlink()
118
+ new_id = target.relative_to(g.knowledge_dir).as_posix()[:-3]
119
+ global_bundle = Bundle.load(g.knowledge_dir)
120
+ unresolved = [l.target for l in global_bundle.concepts[new_id].links if l.broken]
121
+ global_bundle.write_indexes()
122
+ Bundle.load(cfg.knowledge_dir).write_indexes()
123
+ return {
124
+ "from": source_id,
125
+ "to": new_id,
126
+ "moved": not keep,
127
+ "broken_links_in_global": unresolved,
128
+ "project_backlinks": backlinks,
129
+ }
130
+
131
+
132
+ # --------------------------------------------------------------------------- #
133
+ # Skill scopes
134
+ # --------------------------------------------------------------------------- #
135
+
136
+
137
+ def user_skills_dir() -> Path:
138
+ return Path.home() / ".claude" / "skills"
139
+
140
+
141
+ def skill_dirs(cfg: Config) -> dict[str, dict[str, Path]]:
142
+ out: dict[str, dict[str, Path]] = {"project": {}, "user": {}}
143
+ for scope, base in (("project", cfg.root / ".claude" / "skills"), ("user", user_skills_dir())):
144
+ if base.is_dir():
145
+ for d in sorted(base.iterdir()):
146
+ if (d / "SKILL.md").is_file():
147
+ out[scope][d.name] = d
148
+ return out
149
+
150
+
151
+ def move_skill(cfg: Config, name: str, to: str) -> str:
152
+ dirs = skill_dirs(cfg)
153
+ src_scope = "project" if to == "user" else "user"
154
+ if name not in dirs[src_scope]:
155
+ raise ScopeError(f"no {src_scope} skill {name!r}")
156
+ if name in dirs[to]:
157
+ raise ScopeError(f"a {to} skill {name!r} already exists")
158
+ dest_base = user_skills_dir() if to == "user" else cfg.root / ".claude" / "skills"
159
+ dest_base.mkdir(parents=True, exist_ok=True)
160
+ shutil.move(str(dirs[src_scope][name]), dest_base / name)
161
+ return f"moved skill {name} to {to} scope ({dest_base / name})"
162
+
rdstudio/search.py ADDED
@@ -0,0 +1,149 @@
1
+ """Deterministic BM25 search over an OKF bundle.
2
+
3
+ Results carry only what an agent needs to decide what to open next (id, title,
4
+ description, score, a short snippet); bodies are fetched separately by section.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import re
11
+ from collections import Counter
12
+ from dataclasses import dataclass
13
+ from typing import Any, Iterable
14
+
15
+ from .okf import Bundle, Concept, headings
16
+
17
+ _TOKEN = re.compile(r"[a-z0-9]+(?:['’][a-z]+)?")
18
+ _STOP = frozenset(
19
+ "a an and are as at be by for from has have in is it its of on or that the this to was were "
20
+ "will with not but if then than so do does did can could should would into about".split()
21
+ )
22
+ FIELD_WEIGHTS = {"title": 4, "tags": 3, "description": 2, "headings": 2, "type": 1, "body": 1}
23
+ K1, B = 1.4, 0.75
24
+
25
+
26
+ def _stem(token: str) -> str:
27
+ for suffix in ("ness", "ing"):
28
+ if token.endswith(suffix) and len(token) - len(suffix) >= 4:
29
+ return token[: -len(suffix)]
30
+ if len(token) > 4 and token.endswith("ies"):
31
+ return token[:-3] + "y"
32
+ if len(token) > 3 and token.endswith("s") and not token.endswith(("ss", "us", "is")):
33
+ return token[:-1]
34
+ return token
35
+
36
+
37
+ def tokenize(text: str) -> list[str]:
38
+ return [_stem(t) for t in _TOKEN.findall(text.lower().replace("’", "'")) if t not in _STOP]
39
+
40
+
41
+ @dataclass
42
+ class Hit:
43
+ concept: Concept
44
+ score: float
45
+ snippet: str
46
+
47
+ def as_dict(self) -> dict[str, Any]:
48
+ c = self.concept
49
+ return {
50
+ "id": c.id,
51
+ "title": c.title,
52
+ "type": c.type,
53
+ "description": c.description,
54
+ "trust": c.trust,
55
+ "score": round(self.score, 3),
56
+ "snippet": self.snippet,
57
+ }
58
+
59
+
60
+ class Index:
61
+ def __init__(self, bundle: Bundle):
62
+ self.bundle = bundle
63
+ self.docs: dict[str, Counter[str]] = {}
64
+ self.lengths: dict[str, int] = {}
65
+ self.df: Counter[str] = Counter()
66
+ for cid, concept in bundle.concepts.items():
67
+ tf: Counter[str] = Counter()
68
+ fields = {
69
+ "title": concept.title,
70
+ "tags": " ".join(concept.tags),
71
+ "description": concept.description,
72
+ "headings": " ".join(h.text for h in headings(concept.body)),
73
+ "type": concept.type,
74
+ "body": concept.body,
75
+ }
76
+ for name, text in fields.items():
77
+ for token in tokenize(text):
78
+ tf[token] += FIELD_WEIGHTS[name]
79
+ self.docs[cid] = tf
80
+ self.lengths[cid] = sum(tf.values())
81
+ self.df.update(tf.keys())
82
+ self.avg_len = (sum(self.lengths.values()) / len(self.lengths)) if self.lengths else 0.0
83
+
84
+ def _idf(self, token: str) -> float:
85
+ n = len(self.docs)
86
+ df = self.df.get(token, 0)
87
+ return math.log(1 + (n - df + 0.5) / (df + 0.5))
88
+
89
+ def search(
90
+ self,
91
+ query: str,
92
+ *,
93
+ limit: int = 8,
94
+ type: str | None = None,
95
+ tags: Iterable[str] | None = None,
96
+ trust: str | None = None,
97
+ status: str | None = None,
98
+ under: str | None = None,
99
+ ) -> list[Hit]:
100
+ terms = tokenize(query)
101
+ wanted_tags = {t.lower() for t in (tags or [])}
102
+ hits: list[Hit] = []
103
+ for cid, tf in self.docs.items():
104
+ c = self.bundle.concepts[cid]
105
+ if type and c.type.lower() != type.lower():
106
+ continue
107
+ if wanted_tags and not wanted_tags <= {t.lower() for t in c.tags}:
108
+ continue
109
+ if trust and c.trust != trust:
110
+ continue
111
+ if status and c.status != status:
112
+ continue
113
+ if under and not (cid + "/").startswith(under.strip("/") + "/"):
114
+ continue
115
+ score = 0.0
116
+ norm = K1 * (1 - B + B * self.lengths[cid] / self.avg_len) if self.avg_len else K1
117
+ for term in terms:
118
+ f = tf.get(term, 0)
119
+ if f:
120
+ score += self._idf(term) * f * (K1 + 1) / (f + norm)
121
+ if terms and score <= 0:
122
+ continue
123
+ hits.append(Hit(c, score, snippet(c, terms)))
124
+ hits.sort(key=lambda h: (-h.score, h.concept.id))
125
+ return hits[:limit]
126
+
127
+
128
+ def snippet(concept: Concept, terms: list[str], width: int = 160) -> str:
129
+ """The first body line mentioning a query term, trimmed around the match."""
130
+ term_set = set(terms)
131
+ in_fence = False
132
+ for raw in concept.body.split("\n"):
133
+ line = raw.strip()
134
+ if line.startswith(("```", "~~~")):
135
+ in_fence = not in_fence
136
+ continue
137
+ if in_fence or not line:
138
+ continue
139
+ tokens = tokenize(line)
140
+ if term_set & set(tokens):
141
+ lower = line.lower()
142
+ pos = min((lower.find(t) for t in term_set if lower.find(t) >= 0), default=0)
143
+ start = max(0, pos - width // 3)
144
+ if start:
145
+ space = line.find(" ", start)
146
+ start = space + 1 if 0 <= space < pos else start
147
+ text = line[start : start + width]
148
+ return ("…" if start else "") + text + ("…" if start + width < len(line) else "")
149
+ return concept.description[:width]