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.
- rdstudio/__init__.py +3 -0
- rdstudio/_node/rdstudio.mjs +59351 -0
- rdstudio/brief.py +46 -0
- rdstudio/build.py +218 -0
- rdstudio/classify.py +131 -0
- rdstudio/cli.py +369 -0
- rdstudio/config.py +115 -0
- rdstudio/gitlog.py +92 -0
- rdstudio/launcher.py +63 -0
- rdstudio/learner.py +187 -0
- rdstudio/mcp_server.py +313 -0
- rdstudio/okf.py +775 -0
- rdstudio/procedures.py +233 -0
- rdstudio/references.py +228 -0
- rdstudio/reports.py +75 -0
- rdstudio/scaffold.py +227 -0
- rdstudio/scopes.py +162 -0
- rdstudio/search.py +149 -0
- rdstudio/serve.py +214 -0
- rdstudio/store.py +161 -0
- rdstudio/templates/agents/critic.md +20 -0
- rdstudio/templates/agents/librarian.md +21 -0
- rdstudio/templates/agents/searcher.md +21 -0
- rdstudio/templates/agents_section.md +31 -0
- rdstudio/templates/claude_section.md +3 -0
- rdstudio/templates/knowledge/overview.md +20 -0
- rdstudio/templates/knowledge/tasks/bootstrap-knowledge-base.md +45 -0
- rdstudio/templates/skills/decision/SKILL.md +47 -0
- rdstudio/templates/skills/handoff/SKILL.md +18 -0
- rdstudio/templates/skills/ingest-ref/SKILL.md +32 -0
- rdstudio/templates/skills/lint-okf/SKILL.md +15 -0
- rdstudio/templates/skills/promote/SKILL.md +25 -0
- rdstudio/templates/skills/question/SKILL.md +33 -0
- rdstudio/templates/skills/record-okf/SKILL.md +61 -0
- rdstudio/templates/skills/report/SKILL.md +46 -0
- rdstudio/templates/skills/report/template.html +74 -0
- rdstudio/templates/skills/search-okf/SKILL.md +36 -0
- rdstudio/templates/skills/task/SKILL.md +51 -0
- rdstudio/web/app.js +140 -0
- rdstudio/web/fonts.css +48 -0
- rdstudio/web/icon-192.png +0 -0
- rdstudio/web/icon-512.png +0 -0
- rdstudio/web/index.html +109 -0
- rdstudio/web/js/data.js +125 -0
- rdstudio/web/js/diagrams.js +116 -0
- rdstudio/web/js/graph.js +396 -0
- rdstudio/web/js/knowledge.js +212 -0
- rdstudio/web/js/layout-worker.js +15 -0
- rdstudio/web/js/layout.js +226 -0
- rdstudio/web/js/learn.js +83 -0
- rdstudio/web/js/map.js +1293 -0
- rdstudio/web/js/markdown.js +173 -0
- rdstudio/web/js/pages.js +219 -0
- rdstudio/web/js/procedures.js +262 -0
- rdstudio/web/js/settings.js +95 -0
- rdstudio/web/js/util.js +99 -0
- rdstudio/web/report.css +35 -0
- rdstudio/web/report.js +104 -0
- rdstudio/web/style.css +566 -0
- rdstudio/web/sw.js +166 -0
- rdstudio/web/themes/cyber.css +139 -0
- rdstudio/web/themes/map.css +133 -0
- rdstudio/web/themes/notebook.css +134 -0
- rdstudio/web/themes/space.css +135 -0
- rdstudio/web/themes/studio.css +110 -0
- rdstudio/web/vendor/d3.min.js +2 -0
- rdstudio/web/vendor/fonts/atkinson-hyperlegible-next-latin-ext-wght-italic.woff2 +0 -0
- rdstudio/web/vendor/fonts/atkinson-hyperlegible-next-latin-ext-wght-normal.woff2 +0 -0
- rdstudio/web/vendor/fonts/atkinson-hyperlegible-next-latin-wght-italic.woff2 +0 -0
- rdstudio/web/vendor/fonts/atkinson-hyperlegible-next-latin-wght-normal.woff2 +0 -0
- rdstudio/web/vendor/fonts/caveat-latin-wght-normal.woff2 +0 -0
- rdstudio/web/vendor/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- rdstudio/web/vendor/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- rdstudio/web/vendor/fonts/ibm-plex-sans-latin-wght-italic.woff2 +0 -0
- rdstudio/web/vendor/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
- rdstudio/web/vendor/fonts/inter-latin-opsz-italic.woff2 +0 -0
- rdstudio/web/vendor/fonts/inter-latin-opsz-normal.woff2 +0 -0
- rdstudio/web/vendor/fonts/jetbrains-mono-latin-wght-italic.woff2 +0 -0
- rdstudio/web/vendor/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- rdstudio/web/vendor/fonts/literata-latin-ext-opsz-italic.woff2 +0 -0
- rdstudio/web/vendor/fonts/literata-latin-ext-opsz-normal.woff2 +0 -0
- rdstudio/web/vendor/fonts/literata-latin-opsz-italic.woff2 +0 -0
- rdstudio/web/vendor/fonts/literata-latin-opsz-normal.woff2 +0 -0
- rdstudio/web/vendor/fonts/source-sans-3-latin-wght-italic.woff2 +0 -0
- rdstudio/web/vendor/fonts/source-sans-3-latin-wght-normal.woff2 +0 -0
- rdstudio/web/vendor/fonts/source-serif-4-latin-opsz-italic.woff2 +0 -0
- rdstudio/web/vendor/fonts/source-serif-4-latin-opsz-normal.woff2 +0 -0
- rdstudio/web/vendor/hljs/github-dark.min.css +10 -0
- rdstudio/web/vendor/hljs/github.min.css +10 -0
- rdstudio/web/vendor/hljs/highlight.min.js +1262 -0
- rdstudio/web/vendor/hljs/julia.min.js +18 -0
- rdstudio/web/vendor/hljs/latex.min.js +33 -0
- rdstudio/web/vendor/hljs/matlab.min.js +15 -0
- rdstudio/web/vendor/katex/auto-render.min.js +1 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- rdstudio/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- rdstudio/web/vendor/katex/katex.min.css +1 -0
- rdstudio/web/vendor/katex/katex.min.js +1 -0
- rdstudio/web/vendor/licenses/atkinson-hyperlegible-next-OFL.txt +93 -0
- rdstudio/web/vendor/licenses/caveat-OFL.txt +93 -0
- rdstudio/web/vendor/licenses/d3-7.9.0.txt +13 -0
- rdstudio/web/vendor/licenses/highlightjs-cdn-assets-11.12.0.txt +29 -0
- rdstudio/web/vendor/licenses/ibm-plex-mono-OFL.txt +93 -0
- rdstudio/web/vendor/licenses/ibm-plex-sans-OFL.txt +93 -0
- rdstudio/web/vendor/licenses/inter-OFL.txt +93 -0
- rdstudio/web/vendor/licenses/jetbrains-mono-OFL.txt +93 -0
- rdstudio/web/vendor/licenses/katex-0.18.7.txt +21 -0
- rdstudio/web/vendor/licenses/literata-OFL.txt +93 -0
- rdstudio/web/vendor/licenses/markdown-it-15.0.2.txt +22 -0
- rdstudio/web/vendor/licenses/markdown-it-footnote-4.0.0.txt +22 -0
- rdstudio/web/vendor/licenses/markdown-it-texmath-1.0.0.txt +21 -0
- rdstudio/web/vendor/licenses/mermaid-12.0.0.txt +21 -0
- rdstudio/web/vendor/licenses/source-sans-3-OFL.txt +93 -0
- rdstudio/web/vendor/licenses/source-serif-4-OFL.txt +93 -0
- rdstudio/web/vendor/licenses/vega-6.4.0.txt +27 -0
- rdstudio/web/vendor/licenses/vega-embed-7.2.0.txt +27 -0
- rdstudio/web/vendor/licenses/vega-lite-6.4.3.txt +27 -0
- rdstudio/web/vendor/markdown-it-footnote.min.js +2 -0
- rdstudio/web/vendor/markdown-it.min.js +14 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/abnfDiagram-DGLNOSUI.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/architecture-7GRP2DOG-HF4J73XS.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/architectureDiagram-IU6H5EVY.mjs +36 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/blockDiagram-FSX6DT2V.mjs +140 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/c4Diagram-YNRVYILN.mjs +38 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-22SYA5HA.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-2FTYKNOQ.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-36VM3LGA.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-3SEI7E7V.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-3T2MNPEU.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-44AU3KAP.mjs +174 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-4MZOTL6B.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-52IMQVNL.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-5NYYYZGL.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-BOZ75642.mjs +15 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-CVBX6EWK.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-D36F7FYG.mjs +14 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-DEGLBX6O.mjs +213 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-F6PIGZHG.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-FE53TMRB.mjs +133 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-FNR2RGIF.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-GDOATIN3.mjs +32 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-H7BX327W.mjs +2 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-HI3EPXIC.mjs +2 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-JCBDXJ5W.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-JGXDHUYS.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-L2B47AAZ.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-LIEGGFIX.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-LLWYPJEZ.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-MFYRHTYG.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-MIE3LDYP.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-MLQVGYRW.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-MNT3HHAC.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-NRRTYJAG.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-OCMJQKYE.mjs +10 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-PKJCTQEK.mjs +339 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-QADHU5CF.mjs +2 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-QVIEFLGF.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-QX7LGXZU.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-R2KH53MI.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-RXM7Z7NK.mjs +2 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-S4TE5DX6.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-SDIBIOXT.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-TDFULVG6.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-TECWHJ6D.mjs +62 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-TINYC7RT.mjs +70 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-V3KJR4XU.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-VTMXW4ES.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-Y5NT6LW5.mjs +88 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-Y5O5OPKC.mjs +3 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-YZG4BXY6.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZZ52EIBP.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-v2-VV4INRHB.mjs +217 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/cose-bilkent-MSIMRU2B.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/cynefin-OW5HDTMX-KMPDV6GS.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/cynefinDiagram-KTGLXFXK.mjs +62 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/dagre-QJJCSHY4.mjs +4 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-2ZIM67RP.mjs +3 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-FYHH5O6R.mjs +24 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-LYLDACMS.mjs +200 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-TYT3HRBC.mjs +24 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-XVIN6YVD.mjs +30 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-ZO2NLLYC.mjs +41 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/ebnfDiagram-TU3FGH35.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/elk-GHAXNSLK.mjs +27 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/erDiagram-XID3VV23.mjs +99 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/eventmodeling-NTZA5JFV-P5QJSWSK.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/flowDiagram-WPHQAKTU.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/ganttDiagram-QACGSKG6.mjs +292 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/gitGraph-4MIJSDKK-V5NEQQYP.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/gitGraphDiagram-EB76O4HC.mjs +106 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/info-A6RAGUB7-IGBHH2L7.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/infoDiagram-343ZKIDN.mjs +2 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/ishikawaDiagram-UEEU5D75.mjs +70 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/journeyDiagram-4QTZOVLF.mjs +139 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/kanban-definition-VEJL4RG2.mjs +89 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/katex-QVRI4OIV.mjs +257 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/mindmap-definition-C5K5ZT7T.mjs +96 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/packet-AYTQ26CC-PJTKDECJ.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/pegDiagram-FHIILVJB.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/pie-WAS4IAKB-U3JFQXZN.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/pieDiagram-S76BU6V7.mjs +39 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/quadrantDiagram-HZBLWUZE.mjs +7 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/radar-RG4KPBEZ-7T2UW32D.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroad-74A4TZTK-DZY7ARDL.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroad-abnf-HS5TGJTU-LWS535ZK.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroad-ebnf-LZEXJU2U-EA3S4ZFM.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroad-peg-WCYAUIDC-DUKZONPX.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroadDiagram-R6RP3MQ2.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/requirementDiagram-SGBABQV6.mjs +84 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/sankeyDiagram-IM6M5BOW.mjs +40 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/sequenceDiagram-FGJOIFJ2.mjs +169 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/sizeCapture-ERV5D3W5.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-v2-6KDGCZ5J.mjs +291 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/swimlanes-OJBUSBEF.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/swimlanesDiagram-LNXYTHDW.mjs +8 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/timeline-definition-7G6YQRTY.mjs +120 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/treeView-Q6P3EWNA-AABPVERA.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/treemap-WGGIJYW6-F2JSY3V3.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/usecaseDiagram-6Y6TMLBO.mjs +383 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/vennDiagram-6RODYPCK.mjs +34 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/wardley-WFR3VGLG-EIT5NWVN.mjs +1 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/wardleyDiagram-RFZKLAPA.mjs +78 -0
- rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/xychartDiagram-TWWI2QJZ.mjs +7 -0
- rdstudio/web/vendor/mermaid/mermaid.esm.min.mjs +14 -0
- rdstudio/web/vendor/texmath.js +368 -0
- rdstudio/web/vendor/vega/vega-embed.min.js +7 -0
- rdstudio/web/vendor/vega/vega-lite.min.js +2 -0
- rdstudio/web/vendor/vega/vega.min.js +1 -0
- rdstudio-0.2.0.dist-info/METADATA +201 -0
- rdstudio-0.2.0.dist-info/RECORD +251 -0
- rdstudio-0.2.0.dist-info/WHEEL +4 -0
- rdstudio-0.2.0.dist-info/entry_points.txt +3 -0
- 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]
|