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/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
|