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/procedures.py ADDED
@@ -0,0 +1,233 @@
1
+ """Procedural graphs (after Lu et al., arXiv:2609.09153) stored in OKF concepts.
2
+
3
+ A ``type: Procedure`` concept carries, in frontmatter::
4
+
5
+ start: <node id> # optional; default: first node
6
+ nodes: [{id, label, description?}]
7
+ edges: [{from, to, relation, condition?, guidance?, pitfalls?}]
8
+ proposals: [{id, by, at, rationale, edits: [...], state}] # pending/applied/rejected
9
+
10
+ Agents retrieve only the neighbourhood of their current step; edits to a graph
11
+ are proposed by agents and applied or rejected by the developer. Rejected
12
+ proposals are kept as negative evidence for future proposals.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections import deque
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from .okf import Bundle, Concept, now, render_concept, split_frontmatter
23
+ from .store import concept_path
24
+
25
+ RELATIONS = ("LEADS_TO", "TRIGGERS", "PROVIDES_INPUT_FOR", "CONVERGES_TO")
26
+ ATTRIBUTES = ("condition", "guidance", "pitfalls")
27
+ EDIT_OPS = ("add_node", "update_node", "delete_node", "add_edge", "update_edge", "delete_edge")
28
+
29
+
30
+ class ProcedureError(ValueError):
31
+ pass
32
+
33
+
34
+ def is_procedure(c: Concept) -> bool:
35
+ return c.type.lower() == "procedure"
36
+
37
+
38
+ @dataclass
39
+ class Graph:
40
+ nodes: dict[str, dict[str, Any]]
41
+ edges: list[dict[str, Any]]
42
+ start: str | None
43
+ issues: list[str] = field(default_factory=list)
44
+
45
+ @classmethod
46
+ def from_meta(cls, meta: dict[str, Any]) -> "Graph":
47
+ issues: list[str] = []
48
+ nodes: dict[str, dict[str, Any]] = {}
49
+ for raw in meta.get("nodes") or []:
50
+ if isinstance(raw, str):
51
+ raw = {"id": raw}
52
+ if not isinstance(raw, dict) or not raw.get("id"):
53
+ issues.append(f"node without id: {raw!r}")
54
+ continue
55
+ nid = str(raw["id"])
56
+ if nid in nodes:
57
+ issues.append(f"duplicate node {nid!r}")
58
+ nodes[nid] = {"label": str(raw.get("label") or nid), **{k: v for k, v in raw.items() if k not in ("id", "label")}}
59
+ edges: list[dict[str, Any]] = []
60
+ for raw in meta.get("edges") or []:
61
+ if not isinstance(raw, dict):
62
+ issues.append(f"edge is not a mapping: {raw!r}")
63
+ continue
64
+ src, dst = str(raw.get("from", "")), str(raw.get("to", ""))
65
+ relation = str(raw.get("relation") or "LEADS_TO")
66
+ for end in (src, dst):
67
+ if end not in nodes:
68
+ issues.append(f"edge {src!r} -> {dst!r} references unknown node {end!r}")
69
+ if relation not in RELATIONS:
70
+ issues.append(f"edge {src!r} -> {dst!r} has unknown relation {relation!r}")
71
+ edges.append({**raw, "from": src, "to": dst, "relation": relation})
72
+ start = meta.get("start")
73
+ if start is not None and str(start) not in nodes:
74
+ issues.append(f"start node {start!r} does not exist")
75
+ start = str(start) if start is not None else (next(iter(nodes)) if nodes else None)
76
+ return cls(nodes, edges, start, issues)
77
+
78
+ def to_meta(self) -> dict[str, Any]:
79
+ return {
80
+ "nodes": [{"id": nid, **attrs} for nid, attrs in self.nodes.items()],
81
+ "edges": self.edges,
82
+ }
83
+
84
+ def match(self, step: str | None) -> str | None:
85
+ """Localise the current step: exact id, then case-insensitive label."""
86
+ if not step:
87
+ return self.start
88
+ if step in self.nodes:
89
+ return step
90
+ wanted = step.strip().lower()
91
+ for nid, attrs in self.nodes.items():
92
+ if nid.lower() == wanted or str(attrs.get("label", "")).lower() == wanted:
93
+ return nid
94
+ return None
95
+
96
+ def neighbourhood(self, node: str, hops: int = 2) -> "Graph":
97
+ """``node`` and everything reachable within ``hops`` outgoing transitions."""
98
+ seen = {node: 0}
99
+ queue = deque([node])
100
+ while queue:
101
+ current = queue.popleft()
102
+ if seen[current] >= hops:
103
+ continue
104
+ for e in self.edges:
105
+ if e["from"] == current and e["to"] not in seen:
106
+ seen[e["to"]] = seen[current] + 1
107
+ queue.append(e["to"])
108
+ nodes = {nid: self.nodes[nid] for nid in seen if nid in self.nodes}
109
+ edges = [e for e in self.edges if e["from"] in seen and e["to"] in seen and seen[e["from"]] < hops]
110
+ return Graph(nodes, edges, node)
111
+
112
+
113
+ def graph_of(c: Concept) -> Graph:
114
+ return Graph.from_meta(c.meta)
115
+
116
+
117
+ def lint(bundle: Bundle) -> list[tuple[str, str]]:
118
+ out = []
119
+ for c in bundle.concepts.values():
120
+ if is_procedure(c):
121
+ out.extend((c.path, msg) for msg in graph_of(c).issues)
122
+ return out
123
+
124
+
125
+ def describe(proc: Concept, graph: Graph, current: str | None) -> dict[str, Any]:
126
+ """Compact, agent-facing description of a (sub)graph."""
127
+ def edge(e: dict[str, Any]) -> dict[str, Any]:
128
+ out = {"from": e["from"], "to": e["to"], "relation": e["relation"]}
129
+ out.update({k: e[k] for k in ATTRIBUTES if e.get(k)})
130
+ return out
131
+
132
+ return {
133
+ "procedure": proc.id,
134
+ "title": proc.title,
135
+ "current": current,
136
+ "steps": {nid: attrs.get("label", nid) for nid, attrs in graph.nodes.items()},
137
+ "transitions": [edge(e) for e in graph.edges],
138
+ }
139
+
140
+
141
+ # --------------------------------------------------------------------------- #
142
+ # Proposals
143
+ # --------------------------------------------------------------------------- #
144
+
145
+
146
+ def _load(root: Path, cid: str) -> tuple[Path, dict[str, Any], str]:
147
+ path = concept_path(root, cid)
148
+ if not path.exists():
149
+ raise ProcedureError(f"no such procedure: {cid}")
150
+ meta, body = split_frontmatter(path.read_text(encoding="utf-8"))
151
+ meta = meta or {}
152
+ if str(meta.get("type", "")).lower() != "procedure":
153
+ raise ProcedureError(f"{cid} is not a Procedure concept")
154
+ return path, meta, body
155
+
156
+
157
+ def _validate_edits(edits: list[dict[str, Any]]) -> None:
158
+ if not edits:
159
+ raise ProcedureError("a proposal needs at least one edit")
160
+ for e in edits:
161
+ op = e.get("op")
162
+ if op not in EDIT_OPS:
163
+ raise ProcedureError(f"unknown edit op {op!r}; use one of {', '.join(EDIT_OPS)}")
164
+ if op.endswith("_node") and not e.get("id"):
165
+ raise ProcedureError(f"{op} needs an 'id'")
166
+ if op.endswith("_edge") and not (e.get("from") and e.get("to")):
167
+ raise ProcedureError(f"{op} needs 'from' and 'to'")
168
+
169
+
170
+ def propose(root: Path, cid: str, *, edits: list[dict[str, Any]], rationale: str, actor: str) -> dict[str, Any]:
171
+ _validate_edits(edits)
172
+ path, meta, body = _load(root, cid)
173
+ proposals = list(meta.get("proposals") or [])
174
+ pid = max((int(p.get("id", 0)) for p in proposals), default=0) + 1
175
+ proposal = {"id": pid, "by": actor, "at": now(), "state": "pending", "rationale": rationale, "edits": edits}
176
+ # Dry-run so structurally invalid proposals are refused up front.
177
+ trial = apply_edits(Graph.from_meta(meta), edits)
178
+ if trial.issues:
179
+ raise ProcedureError("proposal would leave the graph invalid: " + "; ".join(trial.issues))
180
+ proposals.append(proposal)
181
+ meta["proposals"] = proposals
182
+ path.write_text(render_concept(meta, body), encoding="utf-8")
183
+ return {"procedure": cid, "proposal": pid, "state": "pending"}
184
+
185
+
186
+ def apply_edits(graph: Graph, edits: list[dict[str, Any]]) -> Graph:
187
+ nodes = {k: dict(v) for k, v in graph.nodes.items()}
188
+ edges = [dict(e) for e in graph.edges]
189
+ for e in edits:
190
+ op = e["op"]
191
+ attrs = {k: v for k, v in e.items() if k not in ("op",)}
192
+ if op == "add_node":
193
+ nodes[str(e["id"])] = {"label": str(e.get("label") or e["id"]), **{k: v for k, v in attrs.items() if k not in ("id", "label")}}
194
+ elif op == "update_node":
195
+ nodes.setdefault(str(e["id"]), {}).update({k: v for k, v in attrs.items() if k != "id"})
196
+ elif op == "delete_node":
197
+ nodes.pop(str(e["id"]), None)
198
+ edges = [x for x in edges if str(e["id"]) not in (x["from"], x["to"])]
199
+ elif op == "add_edge":
200
+ edges.append({"from": e["from"], "to": e["to"], "relation": e.get("relation", "LEADS_TO"),
201
+ **{k: e[k] for k in ATTRIBUTES if e.get(k)}})
202
+ elif op == "update_edge":
203
+ for x in edges:
204
+ if (x["from"], x["to"]) == (e["from"], e["to"]) and (not e.get("relation") or x["relation"] == e["relation"]):
205
+ x.update({k: e[k] for k in (*ATTRIBUTES, "relation") if k in e})
206
+ elif op == "delete_edge":
207
+ edges = [x for x in edges if not ((x["from"], x["to"]) == (e["from"], e["to"])
208
+ and (not e.get("relation") or x["relation"] == e["relation"]))]
209
+ meta = {"nodes": [{"id": k, **v} for k, v in nodes.items()], "edges": edges}
210
+ if graph.start in nodes:
211
+ meta["start"] = graph.start
212
+ return Graph.from_meta(meta)
213
+
214
+
215
+ def resolve(root: Path, cid: str, pid: int, *, accept: bool, actor: str) -> dict[str, Any]:
216
+ path, meta, body = _load(root, cid)
217
+ proposals = list(meta.get("proposals") or [])
218
+ match = next((p for p in proposals if int(p.get("id", 0)) == pid), None)
219
+ if match is None:
220
+ raise ProcedureError(f"no proposal {pid} on {cid}")
221
+ if match.get("state") != "pending":
222
+ raise ProcedureError(f"proposal {pid} is already {match.get('state')}")
223
+ if accept:
224
+ graph = apply_edits(Graph.from_meta(meta), match["edits"])
225
+ if graph.issues:
226
+ raise ProcedureError("cannot apply: " + "; ".join(graph.issues))
227
+ meta.update(graph.to_meta())
228
+ meta["generated"] = {"by": actor, "at": now()}
229
+ match["state"] = "applied" if accept else "rejected"
230
+ match["resolved"] = {"by": actor, "at": now()}
231
+ meta["proposals"] = proposals
232
+ path.write_text(render_concept(meta, body), encoding="utf-8")
233
+ return {"procedure": cid, "proposal": pid, "state": match["state"]}
rdstudio/references.py ADDED
@@ -0,0 +1,228 @@
1
+ """Optional papis backend: reference stubs in the bundle, and cheap access to
2
+ bibliographic metadata and PDF text for agents.
3
+
4
+ papis stores one directory per document with an ``info.yaml``; this module
5
+ reads those files directly, so papis itself is not a dependency. Configure in
6
+ ``rdstudio.toml``::
7
+
8
+ [references]
9
+ backend = "papis"
10
+ library = "thesis" # a papis library name (from ~/.config/papis/config)
11
+ # path = "~/papis/thesis" # or the library directory itself
12
+ directory = "references" # where stubs live in the knowledge bundle
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import configparser
18
+ import os
19
+ import re
20
+ import shutil
21
+ import subprocess
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ import yaml
27
+
28
+ from .config import Config
29
+ from .okf import _Loader as YAML_LOADER, now, render_concept
30
+ from .search import tokenize
31
+
32
+ _SAFE = re.compile(r"[^A-Za-z0-9._-]+")
33
+
34
+
35
+ class ReferenceError(RuntimeError):
36
+ pass
37
+
38
+
39
+ @dataclass
40
+ class Entry:
41
+ ref: str
42
+ folder: Path
43
+ info: dict[str, Any] = field(repr=False)
44
+
45
+ @property
46
+ def title(self) -> str:
47
+ return " ".join(str(self.info.get("title") or self.ref).split())
48
+
49
+ @property
50
+ def authors(self) -> list[str]:
51
+ out = []
52
+ for a in self.info.get("author_list") or []:
53
+ if isinstance(a, dict):
54
+ name = " ".join(str(x) for x in (a.get("given"), a.get("family")) if x)
55
+ if name:
56
+ out.append(name)
57
+ if not out and self.info.get("author"):
58
+ out = [s.strip() for s in str(self.info["author"]).split(" and ")]
59
+ return out
60
+
61
+ @property
62
+ def year(self) -> str:
63
+ return str(self.info.get("year") or "")
64
+
65
+ @property
66
+ def venue(self) -> str:
67
+ return str(self.info.get("journal") or self.info.get("booktitle") or self.info.get("publisher") or "")
68
+
69
+ @property
70
+ def url(self) -> str:
71
+ doi = self.info.get("doi")
72
+ return f"https://doi.org/{doi}" if doi else str(self.info.get("url") or "")
73
+
74
+ @property
75
+ def pdfs(self) -> list[Path]:
76
+ files = self.info.get("files") or []
77
+ return [self.folder / f for f in files if str(f).lower().endswith(".pdf") and (self.folder / f).exists()]
78
+
79
+ def citation(self) -> str:
80
+ names = self.authors
81
+ who = names[0].split()[-1] + (" et al." if len(names) > 2 else f" and {names[1].split()[-1]}" if len(names) == 2 else "") if names else "Anon."
82
+ return f"{who} ({self.year or 'n.d.'}). {self.title}." + (f" {self.venue}." if self.venue else "")
83
+
84
+ def summary(self) -> dict[str, Any]:
85
+ return {
86
+ "ref": self.ref, "title": self.title, "authors": self.authors, "year": self.year,
87
+ "venue": self.venue, "url": self.url, "tags": _tags(self.info), "has_pdf": bool(self.pdfs),
88
+ }
89
+
90
+
91
+ def _tags(info: dict[str, Any]) -> list[str]:
92
+ tags = info.get("tags") or []
93
+ if isinstance(tags, str):
94
+ tags = re.split(r"[,\s]+", tags)
95
+ return [str(t) for t in tags if t]
96
+
97
+
98
+ def library_path(cfg: Config) -> Path:
99
+ ref_cfg = cfg.raw.get("references", {})
100
+ if ref_cfg.get("path"):
101
+ return Path(ref_cfg["path"]).expanduser()
102
+ papis_cfg = Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config") / "papis" / "config"
103
+ parser = configparser.ConfigParser()
104
+ parser.read(papis_cfg)
105
+ name = ref_cfg.get("library") or parser.get("settings", "default-library", fallback=None)
106
+ if name and parser.has_section(name) and parser.has_option(name, "dir"):
107
+ return Path(parser.get(name, "dir")).expanduser()
108
+ raise ReferenceError("no papis library configured: set [references] library or path in rdstudio.toml")
109
+
110
+
111
+ def enabled(cfg: Config) -> bool:
112
+ return cfg.raw.get("references", {}).get("backend") == "papis"
113
+
114
+
115
+ def stub_dir(cfg: Config) -> str:
116
+ return str(cfg.raw.get("references", {}).get("directory", "references")).strip("/")
117
+
118
+
119
+ def load(cfg: Config) -> dict[str, Entry]:
120
+ root = library_path(cfg)
121
+ if not root.is_dir():
122
+ raise ReferenceError(f"papis library not found at {root}")
123
+ entries: dict[str, Entry] = {}
124
+ for info_path in sorted(root.glob("*/info.yaml")):
125
+ try:
126
+ info = yaml.load(info_path.read_text(encoding="utf-8"), Loader=YAML_LOADER) or {} # YAML 1.2, as for notes
127
+ except yaml.YAMLError:
128
+ continue
129
+ ref = str(info.get("ref") or info_path.parent.name)
130
+ entries[ref] = Entry(ref, info_path.parent, info)
131
+ return entries
132
+
133
+
134
+ def stub_id(cfg: Config, ref: str) -> str:
135
+ return f"{stub_dir(cfg)}/{_SAFE.sub('-', ref)}"
136
+
137
+
138
+ def sync_stubs(cfg: Config) -> list[str]:
139
+ """Create a ``type: Reference`` concept for every papis entry that lacks one.
140
+ Existing concepts are never modified."""
141
+ created = []
142
+ for ref, e in load(cfg).items():
143
+ cid = stub_id(cfg, ref)
144
+ path = cfg.knowledge_dir / f"{cid}.md"
145
+ if path.exists():
146
+ continue
147
+ meta: dict[str, Any] = {
148
+ "type": "Reference",
149
+ "title": e.title,
150
+ "description": e.citation(),
151
+ }
152
+ if e.url:
153
+ meta["resource"] = e.url
154
+ tags = _tags(e.info)
155
+ if tags:
156
+ meta["tags"] = tags
157
+ meta["papis"] = {"ref": ref}
158
+ meta["generated"] = {"by": "process:rdstudio-refs", "at": now()}
159
+ body = (
160
+ "# Summary\n\nNot yet summarised.\n\n"
161
+ "# Relevance\n\nHow this bears on the project: which decisions, questions or designs it supports or challenges.\n"
162
+ )
163
+ path.parent.mkdir(parents=True, exist_ok=True)
164
+ path.write_text(render_concept(meta, body), encoding="utf-8")
165
+ created.append(cid)
166
+ return created
167
+
168
+
169
+ def search(cfg: Config, query: str, limit: int = 8) -> list[dict[str, Any]]:
170
+ terms = set(tokenize(query))
171
+ scored = []
172
+ for e in load(cfg).values():
173
+ fields = [e.title] * 3 + [" ".join(e.authors)] * 2 + [" ".join(_tags(e.info)), str(e.info.get("abstract") or ""), e.venue]
174
+ tokens = tokenize(" ".join(fields))
175
+ score = sum(tokens.count(t) for t in terms)
176
+ if e.ref.lower() in query.lower():
177
+ score += 10
178
+ if score:
179
+ scored.append((score, e))
180
+ scored.sort(key=lambda x: (-x[0], x[1].ref))
181
+ return [{**e.summary(), "concept": stub_id(cfg, e.ref)} for _, e in scored[:limit]]
182
+
183
+
184
+ def text_pages(cfg: Config, ref: str) -> list[str]:
185
+ """Plain text of an entry's first PDF, one string per page, cached under the
186
+ build output directory."""
187
+ entries = load(cfg)
188
+ if ref not in entries:
189
+ raise ReferenceError(f"no papis entry {ref!r}")
190
+ e = entries[ref]
191
+ if not e.pdfs:
192
+ raise ReferenceError(f"{ref} has no PDF attached")
193
+ pdf = e.pdfs[0]
194
+ cache = cfg.output_dir / "cache" / "text" / f"{_SAFE.sub('-', ref)}.txt"
195
+ if not cache.exists() or cache.stat().st_mtime < pdf.stat().st_mtime:
196
+ if not shutil.which("pdftotext"):
197
+ raise ReferenceError("pdftotext (poppler) is not installed")
198
+ cache.parent.mkdir(parents=True, exist_ok=True)
199
+ subprocess.run(["pdftotext", "-layout", str(pdf), str(cache)], check=True, capture_output=True, timeout=120)
200
+ pages = cache.read_text(encoding="utf-8", errors="replace").split("\f")
201
+ if pages and not pages[-1].strip():
202
+ pages.pop() # pdftotext ends every page, including the last, with a form feed
203
+ return pages
204
+
205
+
206
+ def text(cfg: Config, ref: str, *, pages: str | None = None, query: str | None = None, max_chars: int = 6000) -> str:
207
+ all_pages = text_pages(cfg, ref)
208
+ n = len(all_pages)
209
+ chosen: list[int] = []
210
+ if pages:
211
+ for part in pages.split(","):
212
+ if "-" in part:
213
+ a, b = part.split("-", 1)
214
+ chosen.extend(range(int(a), int(b) + 1))
215
+ elif part.strip():
216
+ chosen.append(int(part))
217
+ elif query:
218
+ terms = set(tokenize(query))
219
+ ranked = sorted(range(1, n + 1), key=lambda p: -sum(tokenize(all_pages[p - 1]).count(t) for t in terms))
220
+ chosen = sorted(p for p in ranked[:2] if any(t in tokenize(all_pages[p - 1]) for t in terms))
221
+ else:
222
+ chosen = [1]
223
+ out = []
224
+ for p in chosen:
225
+ if 1 <= p <= n:
226
+ out.append(f"--- page {p} of {n} ---\n{all_pages[p - 1].strip()}")
227
+ result = "\n\n".join(out) or f"No matching pages (the PDF has {n} pages)."
228
+ return result if len(result) <= max_chars else result[:max_chars] + f"\n[truncated; ask for fewer pages]"
rdstudio/reports.py ADDED
@@ -0,0 +1,75 @@
1
+ """HTML reports: metadata from <meta name="rdstudio:*"> tags and links into knowledge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import posixpath
7
+ import re
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ _TITLE = re.compile(r"<title[^>]*>(.*?)</title>", re.I | re.S)
13
+ _META = re.compile(r"<meta\s+[^>]*>", re.I)
14
+ _ATTR = re.compile(r'(\w[\w:-]*)\s*=\s*("([^"]*)"|\'([^\']*)\')')
15
+ _HREF = re.compile(r'href\s*=\s*("([^"]*)"|\'([^\']*)\')', re.I)
16
+ _H1 = re.compile(r"<h1[^>]*>(.*?)</h1>", re.I | re.S)
17
+ _TAGS = re.compile(r"<[^>]+>")
18
+
19
+
20
+ def _meta(text: str) -> dict[str, str]:
21
+ out: dict[str, str] = {}
22
+ for tag in _META.findall(text):
23
+ attrs = {m.group(1).lower(): html.unescape(m.group(3) if m.group(3) is not None else m.group(4) or "")
24
+ for m in _ATTR.finditer(tag)}
25
+ name = attrs.get("name", "")
26
+ if name.startswith("rdstudio:") or name == "description":
27
+ out[name.removeprefix("rdstudio:")] = attrs.get("content", "")
28
+ return out
29
+
30
+
31
+ def knowledge_links(text: str, report_rel: str, knowledge: str, reports: str) -> list[str]:
32
+ """Concept ids linked from a report (``/knowledge/x.md`` or relative paths into it)."""
33
+ ids: list[str] = []
34
+ here = posixpath.dirname(posixpath.join(reports, report_rel))
35
+ for m in _HREF.finditer(text):
36
+ href = html.unescape(m.group(2) if m.group(2) is not None else m.group(3) or "")
37
+ if href.startswith("#/k/"): # a dashboard route
38
+ ids.append(href[4:])
39
+ continue
40
+ href = href.split("#", 1)[0].split("?", 1)[0]
41
+ if not href or re.match(r"^[a-z][a-z0-9+.-]*:", href, re.I):
42
+ continue
43
+ path = href.lstrip("/") if href.startswith("/") else posixpath.normpath(posixpath.join(here, href))
44
+ prefix = knowledge.strip("/") + "/"
45
+ if path.startswith(prefix) and path.endswith(".md"):
46
+ ids.append(path[len(prefix) : -3])
47
+ return sorted(set(ids))
48
+
49
+
50
+ def scan(reports_dir: Path, knowledge: str, reports: str) -> list[dict[str, Any]]:
51
+ if not reports_dir.is_dir():
52
+ return []
53
+ items = []
54
+ for path in sorted(reports_dir.rglob("*.html")):
55
+ rel = path.relative_to(reports_dir).as_posix()
56
+ if any(part.startswith((".", "_")) for part in rel.split("/")):
57
+ continue
58
+ text = path.read_text(encoding="utf-8", errors="replace")
59
+ meta = _meta(text)
60
+ title = meta.get("title")
61
+ if not title:
62
+ m = _TITLE.search(text) or _H1.search(text)
63
+ title = html.unescape(_TAGS.sub("", m.group(1))).strip() if m else path.stem
64
+ mtime = datetime.fromtimestamp(path.stat().st_mtime, timezone.utc)
65
+ items.append({
66
+ "path": rel,
67
+ "title": " ".join(title.split()),
68
+ "date": meta.get("date") or mtime.strftime("%Y-%m-%d"),
69
+ "author": meta.get("author", ""),
70
+ "description": meta.get("description", ""),
71
+ "activity": meta.get("activity", ""),
72
+ "links": knowledge_links(text, rel, knowledge, reports),
73
+ })
74
+ items.sort(key=lambda r: (r["date"], r["path"]), reverse=True)
75
+ return items