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/learner.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""The private learner record: what a person has done to learn a project.
|
|
2
|
+
|
|
3
|
+
Kept apart from the project (``~/.local/share/rdstudio/learners/<project>/`` by
|
|
4
|
+
default, or under ``[learner] path`` in the user config), one JSON event per
|
|
5
|
+
line in ``record.jsonl``, only ever appended. Off until ``[learner] enabled``.
|
|
6
|
+
|
|
7
|
+
Every event has an ``id``: a ULID (a millisecond timestamp, then randomness, in
|
|
8
|
+
26 sortable characters), and the ``device`` that wrote it. Records from several
|
|
9
|
+
devices merge as a union by id, in any order and as often as needed; sorting by
|
|
10
|
+
id puts them in time order. Corrections are new events that name the id they
|
|
11
|
+
correct, so nothing is ever rewritten.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import secrets
|
|
21
|
+
import subprocess
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from .config import Config, _read_toml, user_config_path
|
|
28
|
+
from .okf import iso, now, to_datetime
|
|
29
|
+
|
|
30
|
+
KINDS = ("autodidactic", "interactive", "ai")
|
|
31
|
+
MAX_EVENT_BYTES = 64 * 1024
|
|
32
|
+
_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
33
|
+
ID_RE = re.compile(r"^[0-9A-HJKMNP-TV-Z]{26}$")
|
|
34
|
+
DEVICE_RE = re.compile(r"^[a-z0-9]{4,16}$")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LearnerError(ValueError):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def settings() -> dict[str, Any]:
|
|
42
|
+
return _read_toml(user_config_path()).get("learner", {})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def enabled(cfg: Config) -> bool:
|
|
46
|
+
return bool(settings().get("enabled", False)) and cfg.is_project
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def project_id(root: Path) -> str:
|
|
50
|
+
"""The repository's first root commit, the same in every clone; outside git,
|
|
51
|
+
a hash of the path."""
|
|
52
|
+
try:
|
|
53
|
+
out = subprocess.run(["git", "rev-list", "--max-parents=0", "HEAD"], cwd=root,
|
|
54
|
+
capture_output=True, text=True, timeout=10)
|
|
55
|
+
roots = sorted(out.stdout.split())
|
|
56
|
+
if out.returncode == 0 and roots:
|
|
57
|
+
return roots[0][:16]
|
|
58
|
+
except (OSError, subprocess.SubprocessError):
|
|
59
|
+
pass
|
|
60
|
+
return "path-" + hashlib.sha256(str(root.resolve()).encode()).hexdigest()[:12]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def learners_root() -> Path:
|
|
64
|
+
base = settings().get("path")
|
|
65
|
+
if base:
|
|
66
|
+
return Path(base).expanduser()
|
|
67
|
+
data = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
|
|
68
|
+
return Path(data) / "rdstudio" / "learners"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def record_dir(cfg: Config) -> Path:
|
|
72
|
+
return learners_root() / project_id(cfg.root)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def device_id() -> str:
|
|
76
|
+
"""This machine's id in learner records, made once and kept beside them."""
|
|
77
|
+
path = learners_root() / "device"
|
|
78
|
+
try:
|
|
79
|
+
dev = path.read_text(encoding="utf-8").strip()
|
|
80
|
+
if DEVICE_RE.match(dev):
|
|
81
|
+
return dev
|
|
82
|
+
except OSError:
|
|
83
|
+
pass
|
|
84
|
+
dev = secrets.token_hex(4)
|
|
85
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
path.write_text(dev + "\n", encoding="utf-8")
|
|
87
|
+
return dev
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _encode(n: int, length: int) -> str:
|
|
91
|
+
return "".join(_CROCKFORD[(n >> (5 * i)) & 31] for i in reversed(range(length)))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
_last = [0, 0] # the last id's time and random part, for ids made in the same millisecond
|
|
95
|
+
_lock = threading.Lock()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def new_id(ms: int | None = None) -> str:
|
|
99
|
+
"""A ULID. Within one millisecond the random part counts up, so ids from one
|
|
100
|
+
device sort in the order they were made."""
|
|
101
|
+
ms = time.time_ns() // 1_000_000 if ms is None else ms
|
|
102
|
+
with _lock:
|
|
103
|
+
rand = _last[1] + 1 if ms == _last[0] else secrets.randbits(80)
|
|
104
|
+
_last[:] = [ms, rand]
|
|
105
|
+
return _encode(ms, 10) + _encode(rand & ((1 << 80) - 1), 16)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def legacy_id(line: str, at: object) -> str:
|
|
109
|
+
"""A fixed id for an event written before events had ids: its time, and a
|
|
110
|
+
hash of the line in place of randomness, so every device derives the same one."""
|
|
111
|
+
when = to_datetime(at)
|
|
112
|
+
ms = int(when.timestamp() * 1000) if when else 0
|
|
113
|
+
return _encode(ms, 10) + _encode(int(hashlib.sha256(line.encode()).hexdigest()[:20], 16), 16)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def content_hash(body: str) -> str:
|
|
117
|
+
"""The version of a note an event refers to."""
|
|
118
|
+
return hashlib.sha256(body.strip().encode("utf-8")).hexdigest()[:12]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _clean(event: dict[str, Any]) -> dict[str, Any]:
|
|
122
|
+
if not isinstance(event, dict):
|
|
123
|
+
raise LearnerError("an event is a JSON object")
|
|
124
|
+
name = event.get("event")
|
|
125
|
+
if not isinstance(name, str) or not name or len(name) > 40:
|
|
126
|
+
raise LearnerError("an event needs an 'event' name")
|
|
127
|
+
kind = event.get("kind")
|
|
128
|
+
if kind is not None and kind not in KINDS:
|
|
129
|
+
raise LearnerError(f"'kind' is one of {', '.join(KINDS)}")
|
|
130
|
+
eid = event.get("id")
|
|
131
|
+
if eid is not None and not (isinstance(eid, str) and ID_RE.match(eid)):
|
|
132
|
+
raise LearnerError("'id' is a ULID (26 characters)") # given when made offline
|
|
133
|
+
dev = event.get("device")
|
|
134
|
+
if dev is not None and not (isinstance(dev, str) and DEVICE_RE.match(dev)):
|
|
135
|
+
raise LearnerError("'device' is 4 to 16 lowercase letters and digits")
|
|
136
|
+
rest = {k: v for k, v in event.items() if k not in ("at", "id", "device")}
|
|
137
|
+
out = {"id": eid or new_id(), "at": iso(now()), "device": dev or device_id(), **rest}
|
|
138
|
+
if len(json.dumps(out, ensure_ascii=False, separators=(",", ":")).encode()) > MAX_EVENT_BYTES:
|
|
139
|
+
raise LearnerError("event too large")
|
|
140
|
+
return out
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def append(cfg: Config, event: dict[str, Any]) -> dict[str, Any]:
|
|
144
|
+
"""Add one event to the record and return it as stored."""
|
|
145
|
+
out = _clean(event)
|
|
146
|
+
path = record_dir(cfg) / "record.jsonl"
|
|
147
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
149
|
+
fh.write(json.dumps(out, ensure_ascii=False, separators=(",", ":")) + "\n")
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def read(path: Path) -> list[dict[str, Any]]:
|
|
154
|
+
"""The events in one record file, in the order written, each once (a retried
|
|
155
|
+
write can repeat one); lines that do not parse are skipped."""
|
|
156
|
+
if not path.is_file():
|
|
157
|
+
return []
|
|
158
|
+
out, seen = [], set()
|
|
159
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
160
|
+
try:
|
|
161
|
+
e = json.loads(line)
|
|
162
|
+
except ValueError:
|
|
163
|
+
continue
|
|
164
|
+
if not isinstance(e, dict):
|
|
165
|
+
continue
|
|
166
|
+
if not isinstance(e.get("id"), str):
|
|
167
|
+
e = {"id": legacy_id(line, e.get("at")), **e}
|
|
168
|
+
if e["id"] not in seen:
|
|
169
|
+
seen.add(e["id"])
|
|
170
|
+
out.append(e)
|
|
171
|
+
return out
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def merge(*records: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
175
|
+
"""The union of several devices' records, by id, in time order. The same in
|
|
176
|
+
any order and however often it is repeated."""
|
|
177
|
+
by_id: dict[str, dict[str, Any]] = {}
|
|
178
|
+
for record in records:
|
|
179
|
+
for e in record:
|
|
180
|
+
by_id.setdefault(e["id"], e)
|
|
181
|
+
return [by_id[k] for k in sorted(by_id)]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def events(cfg: Config, *, concept: str | None = None) -> list[dict[str, Any]]:
|
|
185
|
+
"""Every event, oldest first."""
|
|
186
|
+
out = read(record_dir(cfg) / "record.jsonl")
|
|
187
|
+
return [e for e in out if concept is None or e.get("concept") == concept]
|
rdstudio/mcp_server.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Local stdio MCP server over the project knowledge bundle.
|
|
2
|
+
|
|
3
|
+
Every tool returns compact text so an agent spends as little context as
|
|
4
|
+
possible: search returns ids and one-line snippets, outline returns headings,
|
|
5
|
+
and read can return a single section.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from mcp.server.mcpserver import MCPServer
|
|
14
|
+
|
|
15
|
+
from . import __version__, classify, procedures, references, scopes
|
|
16
|
+
from .brief import brief as project_brief
|
|
17
|
+
from .config import Config
|
|
18
|
+
from .okf import Bundle, dump_frontmatter, headings, jsonable, section
|
|
19
|
+
from .search import Index
|
|
20
|
+
from .store import StoreError, record as store_record
|
|
21
|
+
|
|
22
|
+
INSTRUCTIONS = """\
|
|
23
|
+
Project knowledge base (OKF markdown bundle). Retrieve progressively:
|
|
24
|
+
search -> outline -> read(section). Prefer reading one section over a whole
|
|
25
|
+
concept. Before recording, search for an existing concept to update rather
|
|
26
|
+
than creating a duplicate. Record decisions, answered questions, findings and
|
|
27
|
+
procedures as they happen. Mark an edit significant=false only for trivial or
|
|
28
|
+
dictated changes; significant edits flag human-reviewed concepts for re-review.
|
|
29
|
+
Never claim human verification; only humans verify (rdstudio verify)."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _fmt(obj: Any) -> str:
|
|
33
|
+
return json.dumps(jsonable(obj), ensure_ascii=False, indent=1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_server(cfg: Config) -> MCPServer:
|
|
37
|
+
server = MCPServer("rdstudio", instructions=INSTRUCTIONS, version=__version__)
|
|
38
|
+
classifier = classify.from_config(cfg.raw)
|
|
39
|
+
|
|
40
|
+
def bundle() -> Bundle:
|
|
41
|
+
return Bundle.load(cfg.knowledge_dir)
|
|
42
|
+
|
|
43
|
+
def locate(ref: str) -> tuple[str, Config, Bundle, str | None]:
|
|
44
|
+
"""Resolve "id" (project) or "global:id" (global knowledge base)."""
|
|
45
|
+
if ref.startswith("global:"):
|
|
46
|
+
g = scopes.global_config(cfg)
|
|
47
|
+
if g is None:
|
|
48
|
+
raise scopes.ScopeError("no global knowledge base")
|
|
49
|
+
b = Bundle.load(g.knowledge_dir)
|
|
50
|
+
return "global", g, b, b.resolve_id(ref[len("global:"):])
|
|
51
|
+
b = bundle()
|
|
52
|
+
return "project", cfg, b, b.resolve_id(ref)
|
|
53
|
+
|
|
54
|
+
@server.tool()
|
|
55
|
+
def brief() -> str:
|
|
56
|
+
"""A short orientation to the project knowledge base: what exists, active
|
|
57
|
+
tasks, what awaits the developer and recent commits. Call it at the start
|
|
58
|
+
of a session unless one was already provided."""
|
|
59
|
+
return project_brief(cfg)
|
|
60
|
+
|
|
61
|
+
@server.tool()
|
|
62
|
+
def search(query: str, type: str | None = None, tags: list[str] | None = None,
|
|
63
|
+
under: str | None = None, limit: int = 8, scope: str = "project") -> str:
|
|
64
|
+
"""Keyword (BM25) search over the knowledge base. Returns concept ids, titles,
|
|
65
|
+
descriptions and a one-line snippet. Filter by concept type (e.g. "Decision"),
|
|
66
|
+
tags (all must match) or a directory prefix (e.g. "design"). scope: "project"
|
|
67
|
+
(default), "global" (the developer's cross-project knowledge base) or "all";
|
|
68
|
+
global ids are prefixed "global:"."""
|
|
69
|
+
try:
|
|
70
|
+
targets = scopes.scoped(cfg, scope)
|
|
71
|
+
except scopes.ScopeError as exc:
|
|
72
|
+
return str(exc)
|
|
73
|
+
limit = max(1, min(limit, 25))
|
|
74
|
+
results = []
|
|
75
|
+
for name, c in targets:
|
|
76
|
+
for h in Index(Bundle.load(c.knowledge_dir)).search(query, limit=limit, type=type, tags=tags, under=under):
|
|
77
|
+
d = h.as_dict()
|
|
78
|
+
if name == "global":
|
|
79
|
+
d["id"] = "global:" + d["id"]
|
|
80
|
+
results.append(d)
|
|
81
|
+
results.sort(key=lambda d: -d["score"])
|
|
82
|
+
if not results:
|
|
83
|
+
return "No matches. Try other words, drop filters, or list a directory."
|
|
84
|
+
return _fmt(results[:limit])
|
|
85
|
+
|
|
86
|
+
@server.tool()
|
|
87
|
+
def outline(id: str) -> str:
|
|
88
|
+
"""Metadata and heading outline of one concept, without its body. Use it to
|
|
89
|
+
decide which section to read. Prefix the id with "global:" for the global base."""
|
|
90
|
+
try:
|
|
91
|
+
_, _, b, cid = locate(id)
|
|
92
|
+
except scopes.ScopeError as exc:
|
|
93
|
+
return str(exc)
|
|
94
|
+
if cid is None:
|
|
95
|
+
return f"No concept {id!r}."
|
|
96
|
+
c = b.concepts[cid]
|
|
97
|
+
return _fmt({
|
|
98
|
+
"id": c.id, "title": c.title, "type": c.type, "description": c.description,
|
|
99
|
+
"tags": c.tags, "status": c.status, "trust": c.trust,
|
|
100
|
+
"verification_stale": c.verification_stale,
|
|
101
|
+
"generated": c.meta.get("generated"),
|
|
102
|
+
"chars": len(c.body),
|
|
103
|
+
"headings": ["#" * h.level + " " + h.text for h in headings(c.body)],
|
|
104
|
+
"links_to": sorted({l.target for l in c.links if not l.broken}),
|
|
105
|
+
"linked_from": b.backlinks(cid),
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
@server.tool()
|
|
109
|
+
def read(id: str, section_heading: str | None = None, frontmatter: bool = False) -> str:
|
|
110
|
+
"""Read a concept's body, or only the section under one heading (matched by
|
|
111
|
+
text, case-insensitive). Set frontmatter=true to include the YAML metadata.
|
|
112
|
+
Prefix the id with "global:" for the global base."""
|
|
113
|
+
try:
|
|
114
|
+
_, _, b, cid = locate(id)
|
|
115
|
+
except scopes.ScopeError as exc:
|
|
116
|
+
return str(exc)
|
|
117
|
+
if cid is None:
|
|
118
|
+
return f"No concept {id!r}."
|
|
119
|
+
c = b.concepts[cid]
|
|
120
|
+
text = c.body
|
|
121
|
+
if section_heading:
|
|
122
|
+
text = section(c.body, section_heading)
|
|
123
|
+
if text is None:
|
|
124
|
+
options = ", ".join(h.text for h in headings(c.body)) or "none"
|
|
125
|
+
return f"No section {section_heading!r} in {cid}. Headings: {options}."
|
|
126
|
+
if frontmatter:
|
|
127
|
+
return f"---\n{dump_frontmatter(c.meta)}---\n\n{text}"
|
|
128
|
+
return f"# {c.title} ({cid})\n\n{text}" if not section_heading else text
|
|
129
|
+
|
|
130
|
+
@server.tool()
|
|
131
|
+
def list_concepts(directory: str = "", scope: str = "project") -> str:
|
|
132
|
+
"""List a directory of the knowledge base: its concepts (id, type, title,
|
|
133
|
+
description) and subdirectories. Use "" for the root. scope: "project" or
|
|
134
|
+
"global"."""
|
|
135
|
+
try:
|
|
136
|
+
b = Bundle.load(scopes.scoped(cfg, scope)[-1][1].knowledge_dir) if scope == "global" else bundle()
|
|
137
|
+
except scopes.ScopeError as exc:
|
|
138
|
+
return str(exc)
|
|
139
|
+
d = b.directories.get(directory.strip("/"))
|
|
140
|
+
if d is None:
|
|
141
|
+
return f"No directory {directory!r}. Top level: {sorted(b.directories[''].children)}"
|
|
142
|
+
return _fmt({
|
|
143
|
+
"directory": d.id,
|
|
144
|
+
"subdirectories": sorted(d.children),
|
|
145
|
+
"concepts": [
|
|
146
|
+
{"id": cid, "type": b.concepts[cid].type, "title": b.concepts[cid].title,
|
|
147
|
+
"description": b.concepts[cid].description}
|
|
148
|
+
for cid in sorted(d.concepts)
|
|
149
|
+
],
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
@server.tool()
|
|
153
|
+
def record(id: str, type: str | None = None, title: str | None = None,
|
|
154
|
+
description: str | None = None, tags: list[str] | None = None,
|
|
155
|
+
body: str | None = None, section_heading: str | None = None,
|
|
156
|
+
append: str | None = None, significant: bool | None = None,
|
|
157
|
+
meta: dict[str, Any] | None = None, actor: str | None = None,
|
|
158
|
+
scope: str = "project") -> str:
|
|
159
|
+
"""Create or update a concept at `id` (path without .md, e.g.
|
|
160
|
+
"decisions/activation-function"). New concepts need `type`, and should have a
|
|
161
|
+
title and one-sentence description. `body` replaces the whole body, or only the
|
|
162
|
+
section under `section_heading`; `append` adds to the end. Existing frontmatter
|
|
163
|
+
is preserved; `meta` sets extra keys (null deletes). Links between concepts use
|
|
164
|
+
bundle-absolute paths like [text](/design/model.md). `significant`: true for a
|
|
165
|
+
meaningful change, false for trivial or dictated edits (keeps provenance and
|
|
166
|
+
review state), or omit it to let rdstudio judge from the change. scope="global"
|
|
167
|
+
writes to the developer's global knowledge base: only for knowledge that is
|
|
168
|
+
not specific to this project, and only when the developer asked for it."""
|
|
169
|
+
updates: dict[str, Any] = dict(meta or {})
|
|
170
|
+
for key, value in (("type", type), ("title", title), ("description", description), ("tags", tags)):
|
|
171
|
+
if value is not None:
|
|
172
|
+
updates[key] = value
|
|
173
|
+
try:
|
|
174
|
+
target = cfg if scope == "project" else scopes.scoped(cfg, "global")[0][1]
|
|
175
|
+
result = store_record(
|
|
176
|
+
target.knowledge_dir, id.removeprefix("global:"), actor=actor or cfg.agent, body=body,
|
|
177
|
+
meta=updates, section=section_heading, append=append, significant=significant,
|
|
178
|
+
classifier=classifier,
|
|
179
|
+
)
|
|
180
|
+
except (StoreError, scopes.ScopeError) as exc:
|
|
181
|
+
return f"Not recorded: {exc}"
|
|
182
|
+
b = Bundle.load(target.knowledge_dir)
|
|
183
|
+
b.write_indexes()
|
|
184
|
+
issues = [f"{i.level}: {i.message}" for i in b.lint() if i.path == result.path]
|
|
185
|
+
out = result.as_dict()
|
|
186
|
+
if issues:
|
|
187
|
+
out["issues"] = issues
|
|
188
|
+
return _fmt(out)
|
|
189
|
+
|
|
190
|
+
@server.tool()
|
|
191
|
+
def backlinks(id: str) -> str:
|
|
192
|
+
"""Concepts that link to the given concept."""
|
|
193
|
+
b = bundle()
|
|
194
|
+
cid = b.resolve_id(id)
|
|
195
|
+
if cid is None:
|
|
196
|
+
return f"No concept {id!r}."
|
|
197
|
+
return _fmt([{"id": x, "title": b.concepts[x].title} for x in b.backlinks(cid)])
|
|
198
|
+
|
|
199
|
+
@server.tool()
|
|
200
|
+
def study_path(id: str) -> str:
|
|
201
|
+
"""What to read before a concept: everything it requires (links titled
|
|
202
|
+
"requires"), directly or through a chain, in reading order. ``depth`` is the
|
|
203
|
+
longest chain of prerequisites below each concept."""
|
|
204
|
+
b = bundle()
|
|
205
|
+
cid = b.resolve_id(id)
|
|
206
|
+
if cid is None:
|
|
207
|
+
return f"No concept {id!r}."
|
|
208
|
+
order = b.prerequisite_order()
|
|
209
|
+
return _fmt([{"id": x, "title": b.concepts[x].title, "depth": order[x]["depth"]}
|
|
210
|
+
for x in [*b.prerequisites(cid), cid]])
|
|
211
|
+
|
|
212
|
+
@server.tool()
|
|
213
|
+
def review_queue(limit: int = 10) -> str:
|
|
214
|
+
"""What needs human attention: concepts changed since human review, open
|
|
215
|
+
questions, unverified concepts, and format errors."""
|
|
216
|
+
b = bundle()
|
|
217
|
+
cs = list(b.concepts.values())
|
|
218
|
+
brief = lambda c: {"id": c.id, "title": c.title, "type": c.type} # noqa: E731
|
|
219
|
+
questions = [c for c in cs if c.type.lower() == "question" and "answered" not in c.tags]
|
|
220
|
+
return _fmt({
|
|
221
|
+
"changed_since_review": [brief(c) for c in cs if c.verification_stale][:limit],
|
|
222
|
+
"open_questions": [brief(c) for c in questions][:limit],
|
|
223
|
+
"unverified_count": sum(c.trust == "unverified" for c in cs),
|
|
224
|
+
"errors": [f"{i.path}: {i.message}" for i in b.lint() if i.level == "error"][:limit],
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
@server.tool()
|
|
228
|
+
def procedure_next(procedure: str, step: str | None = None, hops: int = 2) -> str:
|
|
229
|
+
"""Guidance for a recorded procedure (type: Procedure). Give the step you just
|
|
230
|
+
completed (node id or label); returns the steps reachable within `hops`
|
|
231
|
+
transitions, with each transition's condition, guidance and pitfalls. With no
|
|
232
|
+
step, starts at the beginning. Use it to follow a procedure step by step
|
|
233
|
+
without loading the whole graph."""
|
|
234
|
+
b = bundle()
|
|
235
|
+
cid = b.resolve_id(procedure) or b.resolve_id("procedures/" + procedure)
|
|
236
|
+
if cid is None or not procedures.is_procedure(b.concepts[cid]):
|
|
237
|
+
names = [c.id for c in b.concepts.values() if procedures.is_procedure(c)]
|
|
238
|
+
return f"No procedure {procedure!r}. Procedures: {names or 'none recorded'}."
|
|
239
|
+
c = b.concepts[cid]
|
|
240
|
+
graph = procedures.graph_of(c)
|
|
241
|
+
node = graph.match(step)
|
|
242
|
+
matched_by = "exact"
|
|
243
|
+
if node is None and step:
|
|
244
|
+
steps = {nid: str(attrs.get("label", nid)) for nid, attrs in graph.nodes.items()}
|
|
245
|
+
decision = classifier.choose("procedure_step", list(steps), {"description": step, "steps": steps})
|
|
246
|
+
node, matched_by = decision.choice, f"{decision.backend} (confidence {decision.confidence})"
|
|
247
|
+
if node is None:
|
|
248
|
+
out = procedures.describe(c, graph, None)
|
|
249
|
+
out["note"] = f"Step {step!r} is not in this procedure; showing the whole graph."
|
|
250
|
+
return _fmt(out)
|
|
251
|
+
out = procedures.describe(c, graph.neighbourhood(node, max(1, min(hops, 4))), node)
|
|
252
|
+
if matched_by != "exact":
|
|
253
|
+
out["matched_by"] = matched_by
|
|
254
|
+
return _fmt(out)
|
|
255
|
+
|
|
256
|
+
@server.tool()
|
|
257
|
+
def procedure_propose(procedure: str, edits: list[dict[str, Any]], rationale: str,
|
|
258
|
+
actor: str | None = None) -> str:
|
|
259
|
+
"""Propose changes to a procedure's graph for the developer to accept or reject.
|
|
260
|
+
Each edit: {"op": add_node|update_node|delete_node|add_edge|update_edge|delete_edge,
|
|
261
|
+
...}. Nodes take id and label; edges take from, to, relation (LEADS_TO, TRIGGERS,
|
|
262
|
+
PROVIDES_INPUT_FOR, CONVERGES_TO) and condition, guidance, pitfalls. Explain
|
|
263
|
+
in `rationale` what went wrong or right that motivates the change. Check the
|
|
264
|
+
procedure's rejected proposals first (read it with frontmatter=true) and do not
|
|
265
|
+
repeat them."""
|
|
266
|
+
b = bundle()
|
|
267
|
+
cid = b.resolve_id(procedure) or b.resolve_id("procedures/" + procedure)
|
|
268
|
+
if cid is None:
|
|
269
|
+
return f"No procedure {procedure!r}."
|
|
270
|
+
try:
|
|
271
|
+
return _fmt(procedures.propose(cfg.knowledge_dir, cid, edits=edits, rationale=rationale,
|
|
272
|
+
actor=actor or cfg.agent))
|
|
273
|
+
except procedures.ProcedureError as exc:
|
|
274
|
+
return f"Not proposed: {exc}"
|
|
275
|
+
|
|
276
|
+
@server.tool()
|
|
277
|
+
def promote(id: str, as_id: str | None = None, keep: bool = False) -> str:
|
|
278
|
+
"""Move a project concept into the developer's global knowledge base (for
|
|
279
|
+
knowledge that applies beyond this project). Only do this when the developer
|
|
280
|
+
has agreed. keep=true copies instead of moving. Refuses to move a concept other
|
|
281
|
+
project concepts link to unless keep=true."""
|
|
282
|
+
try:
|
|
283
|
+
return _fmt(scopes.promote(cfg, id, as_id=as_id, keep=keep))
|
|
284
|
+
except (scopes.ScopeError, StoreError) as exc:
|
|
285
|
+
return f"Not promoted: {exc}"
|
|
286
|
+
|
|
287
|
+
if references.enabled(cfg):
|
|
288
|
+
@server.tool()
|
|
289
|
+
def ref_search(query: str, limit: int = 8) -> str:
|
|
290
|
+
"""Search the bibliography (papis library) by title, authors, tags and
|
|
291
|
+
abstract. Returns citekeys (`ref`), metadata, whether a PDF is attached, and
|
|
292
|
+
the id of the Reference concept for notes."""
|
|
293
|
+
try:
|
|
294
|
+
hits = references.search(cfg, query, limit=max(1, min(limit, 25)))
|
|
295
|
+
except references.ReferenceError as exc:
|
|
296
|
+
return str(exc)
|
|
297
|
+
return _fmt(hits) if hits else "No matching references."
|
|
298
|
+
|
|
299
|
+
@server.tool()
|
|
300
|
+
def ref_text(ref: str, pages: str | None = None, query: str | None = None) -> str:
|
|
301
|
+
"""Read part of a reference's PDF as plain text: `pages` like "1" or "3-4,7",
|
|
302
|
+
or `query` to get the two pages that best match. Read the pages you need
|
|
303
|
+
rather than the whole paper."""
|
|
304
|
+
try:
|
|
305
|
+
return references.text(cfg, ref, pages=pages, query=query)
|
|
306
|
+
except (references.ReferenceError, ValueError) as exc:
|
|
307
|
+
return str(exc)
|
|
308
|
+
|
|
309
|
+
return server
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def run(cfg: Config) -> None:
|
|
313
|
+
create_server(cfg).run()
|