mednotes-opencode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (430) hide show
  1. package/.opencode/agents/med-chat-triager.md +204 -0
  2. package/.opencode/agents/med-flashcard-maker.md +63 -0
  3. package/.opencode/agents/med-knowledge-architect.md +230 -0
  4. package/.opencode/agents/med-link-graph-curator.md +177 -0
  5. package/.opencode/agents/med-publish-guard.md +62 -0
  6. package/.opencode/commands/flashcards.md +25 -0
  7. package/.opencode/commands/mednotes/create.md +25 -0
  8. package/.opencode/commands/mednotes/enrich.md +27 -0
  9. package/.opencode/commands/mednotes/fix-wiki.md +27 -0
  10. package/.opencode/commands/mednotes/history.md +22 -0
  11. package/.opencode/commands/mednotes/link-body.md +25 -0
  12. package/.opencode/commands/mednotes/link-related.md +27 -0
  13. package/.opencode/commands/mednotes/link.md +27 -0
  14. package/.opencode/commands/mednotes/pdf-library.md +27 -0
  15. package/.opencode/commands/mednotes/process-chats.md +23 -0
  16. package/.opencode/commands/mednotes/setup.md +21 -0
  17. package/.opencode/commands/mednotes/status.md +27 -0
  18. package/.opencode/commands/mednotes/telemetry.md +27 -0
  19. package/.opencode/commands/report.md +26 -0
  20. package/.opencode/mednotes/AGENTS.md +57 -0
  21. package/.opencode/mednotes/agents/med-chat-triager.md +197 -0
  22. package/.opencode/mednotes/agents/med-flashcard-maker.md +56 -0
  23. package/.opencode/mednotes/agents/med-knowledge-architect.md +224 -0
  24. package/.opencode/mednotes/agents/med-link-graph-curator.md +171 -0
  25. package/.opencode/mednotes/agents/med-publish-guard.md +55 -0
  26. package/.opencode/mednotes/contracts/.gitkeep +1 -0
  27. package/.opencode/mednotes/contracts/agents.json +116 -0
  28. package/.opencode/mednotes/contracts/opencode-plugin.json +70 -0
  29. package/.opencode/mednotes/docs/agent-prompt-hardening.md +567 -0
  30. package/.opencode/mednotes/docs/agent-role-contracts.md +94 -0
  31. package/.opencode/mednotes/docs/anki-mcp-twenty-rules.md +214 -0
  32. package/.opencode/mednotes/docs/anki-templates/README.md +39 -0
  33. package/.opencode/mednotes/docs/anki-templates/cloze.back.html +23 -0
  34. package/.opencode/mednotes/docs/anki-templates/cloze.front.html +14 -0
  35. package/.opencode/mednotes/docs/anki-templates/qa.back.html +24 -0
  36. package/.opencode/mednotes/docs/anki-templates/qa.front.html +14 -0
  37. package/.opencode/mednotes/docs/anki-templates/style.css +182 -0
  38. package/.opencode/mednotes/docs/atomicity-splitting-policy.md +113 -0
  39. package/.opencode/mednotes/docs/extension-docs.md +40 -0
  40. package/.opencode/mednotes/docs/flashcard-ingestion.md +278 -0
  41. package/.opencode/mednotes/docs/knowledge-architect.md +208 -0
  42. package/.opencode/mednotes/docs/merge-policy.md +110 -0
  43. package/.opencode/mednotes/docs/public-vocabulary.md +104 -0
  44. package/.opencode/mednotes/docs/semantic-linker.md +141 -0
  45. package/.opencode/mednotes/docs/taxonomy-policy.md +90 -0
  46. package/.opencode/mednotes/docs/triage-policy.md +187 -0
  47. package/.opencode/mednotes/docs/vault-version-control.md +758 -0
  48. package/.opencode/mednotes/docs/vocabulary-db-recovery.md +58 -0
  49. package/.opencode/mednotes/docs/workflow-output-contract.md +779 -0
  50. package/.opencode/mednotes/hooks/hooks.json +79 -0
  51. package/.opencode/mednotes/package-lock.json +6361 -0
  52. package/.opencode/mednotes/package.json +15 -0
  53. package/.opencode/mednotes/pyproject.toml +48 -0
  54. package/.opencode/mednotes/scripts/bootstrap_windows_python_uv.cmd +13 -0
  55. package/.opencode/mednotes/scripts/bootstrap_windows_python_uv.ps1 +172 -0
  56. package/.opencode/mednotes/scripts/enrich_notes.py +23 -0
  57. package/.opencode/mednotes/scripts/full_reset_windows_python_uv.cmd +13 -0
  58. package/.opencode/mednotes/scripts/hooks/antigravity_hook_status.mjs +212 -0
  59. package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/antigravity.mjs +169 -0
  60. package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/harness_payload.mjs +103 -0
  61. package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/opencode_plugin.mjs +341 -0
  62. package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/opencode_user_config_sync.mjs +177 -0
  63. package/.opencode/mednotes/scripts/hooks/mednotes_hook/anki_preflight.mjs +214 -0
  64. package/.opencode/mednotes/scripts/hooks/mednotes_hook/cli.mjs +143 -0
  65. package/.opencode/mednotes/scripts/hooks/mednotes_hook/diagnostics.mjs +11 -0
  66. package/.opencode/mednotes/scripts/hooks/mednotes_hook/domain/agent_directive_core.mjs +160 -0
  67. package/.opencode/mednotes/scripts/hooks/mednotes_hook/fsm_directive.mjs +1470 -0
  68. package/.opencode/mednotes/scripts/hooks/mednotes_hook/hook_errors.mjs +120 -0
  69. package/.opencode/mednotes/scripts/hooks/mednotes_hook/retention.mjs +114 -0
  70. package/.opencode/mednotes/scripts/hooks/mednotes_hook/runtime.mjs +174 -0
  71. package/.opencode/mednotes/scripts/hooks/mednotes_hook/telemetry_capture.mjs +511 -0
  72. package/.opencode/mednotes/scripts/hooks/mednotes_hook/vault_guard.mjs +624 -0
  73. package/.opencode/mednotes/scripts/hooks/mednotes_hook.mjs +5 -0
  74. package/.opencode/mednotes/scripts/mednotes/_runtime_paths.py +24 -0
  75. package/.opencode/mednotes/scripts/mednotes/anki_model_validator.py +18 -0
  76. package/.opencode/mednotes/scripts/mednotes/capture_extension_diff.py +1562 -0
  77. package/.opencode/mednotes/scripts/mednotes/feedback_report.py +16 -0
  78. package/.opencode/mednotes/scripts/mednotes/flashcard_index.py +18 -0
  79. package/.opencode/mednotes/scripts/mednotes/flashcard_pipeline.py +18 -0
  80. package/.opencode/mednotes/scripts/mednotes/flashcard_report.py +18 -0
  81. package/.opencode/mednotes/scripts/mednotes/flashcard_sources.py +18 -0
  82. package/.opencode/mednotes/scripts/mednotes/obsidian/README.md +6 -0
  83. package/.opencode/mednotes/scripts/mednotes/obsidian_note_utils.py +20 -0
  84. package/.opencode/mednotes/scripts/mednotes/pdf_library/cli.py +16 -0
  85. package/.opencode/mednotes/scripts/mednotes/project_fsm.py +229 -0
  86. package/.opencode/mednotes/scripts/mednotes/setup_telemetry_email.py +404 -0
  87. package/.opencode/mednotes/scripts/mednotes/sync_anki_twenty_rules.py +18 -0
  88. package/.opencode/mednotes/scripts/mednotes/sync_opencode_user_config.py +36 -0
  89. package/.opencode/mednotes/scripts/mednotes/wiki/cli.py +20 -0
  90. package/.opencode/mednotes/scripts/mednotes/wiki_graph.py +18 -0
  91. package/.opencode/mednotes/scripts/mednotes/wiki_tree.py +134 -0
  92. package/.opencode/mednotes/scripts/reset_windows_python_uv.ps1 +625 -0
  93. package/.opencode/mednotes/scripts/run_python.mjs +109 -0
  94. package/.opencode/mednotes/scripts/vault/vault_commit.ps1 +19 -0
  95. package/.opencode/mednotes/scripts/vault/vault_commit.sh +18 -0
  96. package/.opencode/mednotes/scripts/vault/vault_git.ps1 +19 -0
  97. package/.opencode/mednotes/scripts/vault/vault_git.py +3107 -0
  98. package/.opencode/mednotes/scripts/vault/vault_git.sh +18 -0
  99. package/.opencode/mednotes/scripts/vault/vault_precommit.ps1 +19 -0
  100. package/.opencode/mednotes/scripts/vault/vault_precommit.sh +18 -0
  101. package/.opencode/mednotes/skills/THIRD_PARTY_NOTICES.md +45 -0
  102. package/.opencode/mednotes/skills/create-medical-flashcards/SKILL.md +113 -0
  103. package/.opencode/mednotes/skills/create-medical-note/SKILL.md +90 -0
  104. package/.opencode/mednotes/skills/enrich-medical-note/SKILL.md +120 -0
  105. package/.opencode/mednotes/skills/fix-medical-wiki/SKILL.md +559 -0
  106. package/.opencode/mednotes/skills/link-medical-wiki/SKILL.md +224 -0
  107. package/.opencode/mednotes/skills/obsidian-cli/SKILL.md +118 -0
  108. package/.opencode/mednotes/skills/obsidian-markdown/SKILL.md +207 -0
  109. package/.opencode/mednotes/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
  110. package/.opencode/mednotes/skills/obsidian-markdown/references/EMBEDS.md +63 -0
  111. package/.opencode/mednotes/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
  112. package/.opencode/mednotes/skills/obsidian-ops/SKILL.md +136 -0
  113. package/.opencode/mednotes/skills/pdf-library/SKILL.md +45 -0
  114. package/.opencode/mednotes/skills/process-medical-chats/SKILL.md +246 -0
  115. package/.opencode/mednotes/skills/workflow-report/SKILL.md +100 -0
  116. package/.opencode/mednotes/src/mednotes/__init__.py +5 -0
  117. package/.opencode/mednotes/src/mednotes/domains/__init__.py +5 -0
  118. package/.opencode/mednotes/src/mednotes/domains/flashcards/README.md +26 -0
  119. package/.opencode/mednotes/src/mednotes/domains/flashcards/__init__.py +2 -0
  120. package/.opencode/mednotes/src/mednotes/domains/flashcards/build_demo_apkg.py +177 -0
  121. package/.opencode/mednotes/src/mednotes/domains/flashcards/contracts.py +385 -0
  122. package/.opencode/mednotes/src/mednotes/domains/flashcards/flashcards_machine.py +522 -0
  123. package/.opencode/mednotes/src/mednotes/domains/flashcards/fsm.py +817 -0
  124. package/.opencode/mednotes/src/mednotes/domains/flashcards/index.py +630 -0
  125. package/.opencode/mednotes/src/mednotes/domains/flashcards/install_models.py +445 -0
  126. package/.opencode/mednotes/src/mednotes/domains/flashcards/model.py +359 -0
  127. package/.opencode/mednotes/src/mednotes/domains/flashcards/obsidian_links.py +135 -0
  128. package/.opencode/mednotes/src/mednotes/domains/flashcards/obsidian_note_utils.py +546 -0
  129. package/.opencode/mednotes/src/mednotes/domains/flashcards/pipeline.py +580 -0
  130. package/.opencode/mednotes/src/mednotes/domains/flashcards/report.py +510 -0
  131. package/.opencode/mednotes/src/mednotes/domains/flashcards/sources.py +682 -0
  132. package/.opencode/mednotes/src/mednotes/domains/flashcards/sync_rules.py +184 -0
  133. package/.opencode/mednotes/src/mednotes/domains/history/__init__.py +1 -0
  134. package/.opencode/mednotes/src/mednotes/domains/history/history_fsm.py +852 -0
  135. package/.opencode/mednotes/src/mednotes/domains/history/history_machine.py +453 -0
  136. package/.opencode/mednotes/src/mednotes/domains/setup/__init__.py +7 -0
  137. package/.opencode/mednotes/src/mednotes/domains/setup/setup_fsm.py +808 -0
  138. package/.opencode/mednotes/src/mednotes/domains/setup/setup_machine.py +973 -0
  139. package/.opencode/mednotes/src/mednotes/domains/wiki/README.md +64 -0
  140. package/.opencode/mednotes/src/mednotes/domains/wiki/__init__.py +1 -0
  141. package/.opencode/mednotes/src/mednotes/domains/wiki/api.py +668 -0
  142. package/.opencode/mednotes/src/mednotes/domains/wiki/batch_state.py +102 -0
  143. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/__init__.py +1 -0
  144. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/atomicity/__init__.py +1 -0
  145. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/atomicity/atomicity.py +877 -0
  146. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/body_link/__init__.py +1 -0
  147. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/body_link/body_linker.py +1562 -0
  148. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/__init__.py +1 -0
  149. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/effect_adapters.py +949 -0
  150. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/fix_wiki_runtime_adapters.py +433 -0
  151. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/__init__.py +1 -0
  152. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/coverage.py +413 -0
  153. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/graph.py +396 -0
  154. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/graph_fixes.py +161 -0
  155. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/hygiene/__init__.py +1 -0
  156. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/hygiene/hygiene.py +483 -0
  157. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/__init__.py +2 -0
  158. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/anchors.py +185 -0
  159. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/__init__.py +0 -0
  160. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/cache.py +223 -0
  161. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/config.py +131 -0
  162. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/download.py +224 -0
  163. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/frontmatter.py +59 -0
  164. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/insert.py +227 -0
  165. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/local_import.py +54 -0
  166. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/__init__.py +42 -0
  167. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/web_profiles.py +99 -0
  168. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/web_search.py +203 -0
  169. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/wikimedia.py +102 -0
  170. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/__init__.py +1 -0
  171. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_db_adapter.mjs +434 -0
  172. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_node_runtime.py +274 -0
  173. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_query.py +227 -0
  174. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/__init__.py +1 -0
  175. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/artifacts.py +605 -0
  176. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/canonical_merge.py +277 -0
  177. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/markdown_zones.py +85 -0
  178. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/meaning_planner.py +307 -0
  179. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_iter.py +67 -0
  180. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_merge.py +278 -0
  181. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_plan.py +409 -0
  182. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_policy.py +22 -0
  183. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/__init__.py +79 -0
  184. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/fixes.py +264 -0
  185. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/frontmatter.py +435 -0
  186. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/models.py +208 -0
  187. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/prompts.py +37 -0
  188. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/tables.py +236 -0
  189. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/validate.py +404 -0
  190. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/provenance.py +478 -0
  191. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/raw_chats.py +273 -0
  192. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/sources_backfill.py +235 -0
  193. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/__init__.py +10 -0
  194. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/anchors.py +16 -0
  195. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/captions.py +47 -0
  196. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/cli.py +179 -0
  197. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/cloud.py +52 -0
  198. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/config.py +196 -0
  199. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/context_packets.py +76 -0
  200. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/db.py +81 -0
  201. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/doctor.py +102 -0
  202. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/figure_ids.py +42 -0
  203. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/ingest.py +326 -0
  204. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/insert.py +316 -0
  205. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/mentions.py +57 -0
  206. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/ocr.py +71 -0
  207. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/paths.py +35 -0
  208. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/pdf_engine.py +77 -0
  209. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/schema.py +155 -0
  210. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/search.py +188 -0
  211. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/__init__.py +1 -0
  212. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/app.py +89 -0
  213. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/image_backend.py +29 -0
  214. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/state.py +65 -0
  215. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/__init__.py +1 -0
  216. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish.py +1139 -0
  217. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish_receipts.py +365 -0
  218. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish_recovery.py +240 -0
  219. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/__init__.py +1 -0
  220. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_behavior_corpus.py +2069 -0
  221. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_report_validation.py +4448 -0
  222. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_run_audit.py +852 -0
  223. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/architect_prompt_eval.py +341 -0
  224. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/body_linker_eval.py +240 -0
  225. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/curator_output_validation.py +175 -0
  226. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/curator_prompt_eval.py +865 -0
  227. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/triager_prompt_eval.py +1295 -0
  228. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/__init__.py +1 -0
  229. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/related_notes.py +1920 -0
  230. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/related_notes_headless.py +1186 -0
  231. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/__init__.py +1 -0
  232. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/plan_attestation.py +148 -0
  233. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_receipts.py +360 -0
  234. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_runtime.py +52 -0
  235. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_task_runner.py +2470 -0
  236. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/style/__init__.py +1 -0
  237. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/style/style.py +1952 -0
  238. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/subagents/__init__.py +1 -0
  239. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/subagents/agents.py +1767 -0
  240. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/__init__.py +1 -0
  241. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/alias_projection.py +331 -0
  242. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/link_terms.py +151 -0
  243. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/llm_disambiguation.py +182 -0
  244. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/__init__.py +116 -0
  245. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/audit.py +201 -0
  246. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/migration.py +314 -0
  247. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/normalize.py +72 -0
  248. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/policy.py +135 -0
  249. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/resolve.py +413 -0
  250. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/schema.py +157 -0
  251. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/status.py +137 -0
  252. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_bootstrap.py +509 -0
  253. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_curator_batch.py +1115 -0
  254. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_ingestion.py +632 -0
  255. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_map.py +930 -0
  256. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_recovery.py +1388 -0
  257. package/.opencode/mednotes/src/mednotes/domains/wiki/cli.py +6665 -0
  258. package/.opencode/mednotes/src/mednotes/domains/wiki/common.py +69 -0
  259. package/.opencode/mednotes/src/mednotes/domains/wiki/config.py +210 -0
  260. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/__init__.py +74 -0
  261. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agent_report.py +242 -0
  262. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agent_run_audit.py +196 -0
  263. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agents.py +601 -0
  264. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/curator.py +256 -0
  265. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/effect_payloads.py +519 -0
  266. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/happy_path.py +190 -0
  267. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/link_git.py +110 -0
  268. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/link_runtime_artifact.py +52 -0
  269. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/note_plan.py +75 -0
  270. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/paths.py +114 -0
  271. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/public_report.py +53 -0
  272. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/publish.py +111 -0
  273. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/raw_coverage.py +217 -0
  274. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes.py +136 -0
  275. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes_headless.py +153 -0
  276. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes_runtime.py +395 -0
  277. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/schema_registry.py +637 -0
  278. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/specialist.py +432 -0
  279. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/status.py +62 -0
  280. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/style_rewrite.py +568 -0
  281. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/vocabulary_ingestion.py +223 -0
  282. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_blockers.py +510 -0
  283. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_guardrails.py +637 -0
  284. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_outcomes.py +121 -0
  285. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_receipts.py +100 -0
  286. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/__init__.py +1 -0
  287. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/__init__.py +1 -0
  288. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/__main__.py +4 -0
  289. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/cli.py +275 -0
  290. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/__init__.py +2 -0
  291. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/candidates.py +193 -0
  292. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/cli.py +189 -0
  293. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/gemini.py +220 -0
  294. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/inputs.py +120 -0
  295. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/models.py +34 -0
  296. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/parsing.py +48 -0
  297. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/prompts.py +216 -0
  298. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/quality.py +54 -0
  299. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/reporting.py +24 -0
  300. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/runner.py +433 -0
  301. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/utils.py +39 -0
  302. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/vault_guard_bridge.py +17 -0
  303. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/__init__.py +1 -0
  304. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_context_packets.py +454 -0
  305. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_decision_projection.py +133 -0
  306. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_effects.py +1260 -0
  307. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_fsm.py +2768 -0
  308. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_machine.py +1588 -0
  309. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_plan.py +306 -0
  310. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_primary_objective.py +316 -0
  311. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_problem.py +153 -0
  312. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_receipt_evidence.py +306 -0
  313. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_states.py +290 -0
  314. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_user_report.py +342 -0
  315. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/health.py +6332 -0
  316. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/__init__.py +1 -0
  317. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_fsm.py +1119 -0
  318. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_git.py +638 -0
  319. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_machine.py +1106 -0
  320. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_retry_governance.py +374 -0
  321. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_runtime_result.py +485 -0
  322. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_triggers.py +183 -0
  323. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/linking.py +2758 -0
  324. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/reference_repair.py +718 -0
  325. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/related_notes_fsm.py +1855 -0
  326. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link_related/__init__.py +1 -0
  327. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link_related/link_related_machine.py +834 -0
  328. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/__init__.py +1 -0
  329. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_fsm.py +1592 -0
  330. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_machine.py +3097 -0
  331. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_primary_objective.py +28 -0
  332. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_runtime_result.py +185 -0
  333. package/.opencode/mednotes/src/mednotes/domains/wiki/performance.py +97 -0
  334. package/.opencode/mednotes/src/mednotes/kernel/__init__.py +6 -0
  335. package/.opencode/mednotes/src/mednotes/kernel/agent_directive.py +336 -0
  336. package/.opencode/mednotes/src/mednotes/kernel/base.py +51 -0
  337. package/.opencode/mednotes/src/mednotes/kernel/blockers.py +39 -0
  338. package/.opencode/mednotes/src/mednotes/kernel/effect_executor.py +55 -0
  339. package/.opencode/mednotes/src/mednotes/kernel/effect_intent.py +69 -0
  340. package/.opencode/mednotes/src/mednotes/kernel/effects.py +160 -0
  341. package/.opencode/mednotes/src/mednotes/kernel/errors.py +38 -0
  342. package/.opencode/mednotes/src/mednotes/kernel/fsm_event.py +35 -0
  343. package/.opencode/mednotes/src/mednotes/kernel/fsm_model.py +55 -0
  344. package/.opencode/mednotes/src/mednotes/kernel/fsm_transition_result.py +75 -0
  345. package/.opencode/mednotes/src/mednotes/kernel/guardrails.py +188 -0
  346. package/.opencode/mednotes/src/mednotes/kernel/progress.py +319 -0
  347. package/.opencode/mednotes/src/mednotes/kernel/public_report.py +346 -0
  348. package/.opencode/mednotes/src/mednotes/kernel/state_machine.py +164 -0
  349. package/.opencode/mednotes/src/mednotes/kernel/workflow.py +619 -0
  350. package/.opencode/mednotes/src/mednotes/platform/__init__.py +5 -0
  351. package/.opencode/mednotes/src/mednotes/platform/backup_policy.py +382 -0
  352. package/.opencode/mednotes/src/mednotes/platform/feedback/__init__.py +62 -0
  353. package/.opencode/mednotes/src/mednotes/platform/feedback/cli.py +275 -0
  354. package/.opencode/mednotes/src/mednotes/platform/feedback/contracts.py +83 -0
  355. package/.opencode/mednotes/src/mednotes/platform/feedback/core.py +4168 -0
  356. package/.opencode/mednotes/src/mednotes/platform/feedback/integrity.py +989 -0
  357. package/.opencode/mednotes/src/mednotes/platform/feedback/operational_contract.py +2293 -0
  358. package/.opencode/mednotes/src/mednotes/platform/feedback/telemetry.py +875 -0
  359. package/.opencode/mednotes/src/mednotes/platform/feedback/telemetry_config.py +65 -0
  360. package/.opencode/mednotes/src/mednotes/platform/opencode_runtime_config.py +182 -0
  361. package/.opencode/mednotes/src/mednotes/platform/paths/__init__.py +1560 -0
  362. package/.opencode/mednotes/src/mednotes/platform/secrets.py +89 -0
  363. package/.opencode/mednotes/src/mednotes/platform/user_config.py +103 -0
  364. package/.opencode/mednotes/src/mednotes/platform/vault_guard.py +214 -0
  365. package/.opencode/mednotes/uv.lock +932 -0
  366. package/.opencode/mednotes.generated.json +395 -0
  367. package/.opencode/opencode.json +31 -0
  368. package/.opencode/plugins/mednotes-fsm.mjs +7 -0
  369. package/.opencode/plugins/mednotes_hook/adapters/antigravity.mjs +169 -0
  370. package/.opencode/plugins/mednotes_hook/adapters/harness_payload.mjs +103 -0
  371. package/.opencode/plugins/mednotes_hook/adapters/opencode_plugin.mjs +341 -0
  372. package/.opencode/plugins/mednotes_hook/adapters/opencode_user_config_sync.mjs +177 -0
  373. package/.opencode/plugins/mednotes_hook/anki_preflight.mjs +214 -0
  374. package/.opencode/plugins/mednotes_hook/cli.mjs +143 -0
  375. package/.opencode/plugins/mednotes_hook/diagnostics.mjs +11 -0
  376. package/.opencode/plugins/mednotes_hook/domain/agent_directive_core.mjs +160 -0
  377. package/.opencode/plugins/mednotes_hook/fsm_directive.mjs +1470 -0
  378. package/.opencode/plugins/mednotes_hook/hook_errors.mjs +120 -0
  379. package/.opencode/plugins/mednotes_hook/retention.mjs +114 -0
  380. package/.opencode/plugins/mednotes_hook/runtime.mjs +174 -0
  381. package/.opencode/plugins/mednotes_hook/telemetry_capture.mjs +511 -0
  382. package/.opencode/plugins/mednotes_hook/vault_guard.mjs +624 -0
  383. package/AGENTS.md +57 -0
  384. package/README.md +194 -0
  385. package/adapters/antigravity/agents.json +80 -0
  386. package/adapters/antigravity/templates/med-chat-triager.md +214 -0
  387. package/adapters/antigravity/templates/med-flashcard-maker.md +72 -0
  388. package/adapters/antigravity/templates/med-knowledge-architect.md +241 -0
  389. package/adapters/antigravity/templates/med-link-graph-curator.md +187 -0
  390. package/adapters/antigravity/templates/med-publish-guard.md +71 -0
  391. package/adapters/gemini-cli/gemini-extension.json +14 -0
  392. package/adapters/gemini-cli/package.json +15 -0
  393. package/adapters/gemini-cli/pyproject.toml +48 -0
  394. package/bin/mednotes-opencode.mjs +155 -0
  395. package/contracts/agents.json +116 -0
  396. package/core/agents/med-chat-triager.md +197 -0
  397. package/core/agents/med-flashcard-maker.md +56 -0
  398. package/core/agents/med-knowledge-architect.md +224 -0
  399. package/core/agents/med-link-graph-curator.md +171 -0
  400. package/core/agents/med-publish-guard.md +55 -0
  401. package/core/commands/flashcards.toml +22 -0
  402. package/core/commands/mednotes/create.toml +22 -0
  403. package/core/commands/mednotes/enrich.toml +24 -0
  404. package/core/commands/mednotes/fix-wiki.toml +24 -0
  405. package/core/commands/mednotes/history.toml +19 -0
  406. package/core/commands/mednotes/link-body.toml +22 -0
  407. package/core/commands/mednotes/link-related.toml +24 -0
  408. package/core/commands/mednotes/link.toml +24 -0
  409. package/core/commands/mednotes/pdf-library.toml +24 -0
  410. package/core/commands/mednotes/process-chats.toml +20 -0
  411. package/core/commands/mednotes/setup.toml +18 -0
  412. package/core/commands/mednotes/status.toml +24 -0
  413. package/core/commands/mednotes/telemetry.toml +24 -0
  414. package/core/commands/report.toml +23 -0
  415. package/core/skills/THIRD_PARTY_NOTICES.md +45 -0
  416. package/core/skills/create-medical-flashcards/SKILL.md +113 -0
  417. package/core/skills/create-medical-note/SKILL.md +90 -0
  418. package/core/skills/enrich-medical-note/SKILL.md +120 -0
  419. package/core/skills/fix-medical-wiki/SKILL.md +559 -0
  420. package/core/skills/link-medical-wiki/SKILL.md +224 -0
  421. package/core/skills/obsidian-cli/SKILL.md +118 -0
  422. package/core/skills/obsidian-markdown/SKILL.md +207 -0
  423. package/core/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
  424. package/core/skills/obsidian-markdown/references/EMBEDS.md +63 -0
  425. package/core/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
  426. package/core/skills/obsidian-ops/SKILL.md +136 -0
  427. package/core/skills/pdf-library/SKILL.md +45 -0
  428. package/core/skills/process-medical-chats/SKILL.md +246 -0
  429. package/core/skills/workflow-report/SKILL.md +100 -0
  430. package/package.json +45 -0
@@ -0,0 +1,57 @@
1
+ """Figure mention extraction and linking."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from dataclasses import dataclass, field
6
+
7
+ from mednotes.domains.wiki.capabilities.pdf import figure_ids
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Mention:
12
+ page_number: int
13
+ figure_id: str
14
+ sentence: str
15
+ paragraph: str = ""
16
+ section_path_guess: list[str] = field(default_factory=list)
17
+ offset_start: int = 0
18
+ offset_end: int = 0
19
+
20
+
21
+ def extract_mentions(text: str, *, page_number: int, section_path_guess: list[str] | None = None) -> list[Mention]:
22
+ mentions: list[Mention] = []
23
+ for _raw, normalized, start, end in figure_ids.find_ids(text):
24
+ sentence = _sentence_around(text, start, end)
25
+ mentions.append(
26
+ Mention(
27
+ page_number=page_number,
28
+ figure_id=normalized,
29
+ sentence=sentence,
30
+ paragraph=sentence,
31
+ section_path_guess=list(section_path_guess or []),
32
+ offset_start=start,
33
+ offset_end=end,
34
+ )
35
+ )
36
+ return mentions
37
+
38
+
39
+ def link_mentions(caption_page: int, caption_section: list[str], candidates: list[Mention], *, figure_id: str, window_pages: int = 20) -> list[Mention]:
40
+ linked: list[Mention] = []
41
+ for mention in candidates:
42
+ if mention.figure_id != figure_id:
43
+ continue
44
+ same_section = bool(caption_section and mention.section_path_guess and caption_section[:1] == mention.section_path_guess[:1])
45
+ near = abs(mention.page_number - caption_page) <= window_pages
46
+ if same_section or near:
47
+ linked.append(mention)
48
+ return linked
49
+
50
+
51
+ def _sentence_around(text: str, start: int, end: int) -> str:
52
+ left = max(text.rfind(".", 0, start), text.rfind("\n", 0, start))
53
+ right_dot = text.find(".", end)
54
+ right_newline = text.find("\n", end)
55
+ candidates = [idx for idx in (right_dot, right_newline) if idx != -1]
56
+ right = min(candidates) if candidates else min(len(text), end + 180)
57
+ return re.sub(r"\s+", " ", text[left + 1 : right + 1]).strip()
@@ -0,0 +1,71 @@
1
+ """OCR state machine and Tesseract seam."""
2
+ from __future__ import annotations
3
+
4
+ import shutil
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Protocol
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class OcrOutcome:
12
+ status: str
13
+ text: str
14
+ confidence: float | None
15
+ error_code: str = ""
16
+ retry_eligible: bool = False
17
+
18
+
19
+ class OcrRunner(Protocol):
20
+ def run(self, image_path: Path, languages: list[str]) -> OcrOutcome:
21
+ ...
22
+
23
+
24
+ class MissingTesseractRunner:
25
+ def run(self, image_path: Path, languages: list[str]) -> OcrOutcome:
26
+ return OcrOutcome(status="blocked", text="", confidence=None, error_code="missing_binary", retry_eligible=True)
27
+
28
+
29
+ class TesseractRunner:
30
+ def run(self, image_path: Path, languages: list[str]) -> OcrOutcome:
31
+ if shutil.which("tesseract") is None:
32
+ return MissingTesseractRunner().run(image_path, languages)
33
+ try:
34
+ import pytesseract
35
+ from PIL import Image
36
+ except Exception:
37
+ return OcrOutcome("failed", "", None, "engine_error", True)
38
+ try:
39
+ text = str(pytesseract.image_to_string(Image.open(image_path), lang="+".join(languages)))
40
+ except pytesseract.TesseractError as exc:
41
+ message = str(exc).lower()
42
+ if "failed loading language" in message or "could not initialize tesseract" in message:
43
+ return OcrOutcome("blocked", "", None, "missing_language", True)
44
+ return OcrOutcome("failed", "", None, "engine_error", True)
45
+ except Exception:
46
+ return OcrOutcome("failed", "", None, "engine_error", True)
47
+ stripped = text.strip()
48
+ return OcrOutcome("complete" if stripped else "failed", stripped, None, "" if stripped else "engine_error", not bool(stripped))
49
+
50
+
51
+ def ocr_page(image_path: Path, *, languages: list[str], runner: OcrRunner | None = None) -> OcrOutcome:
52
+ return (runner or TesseractRunner()).run(image_path, languages)
53
+
54
+
55
+ def aggregate_status(outcomes: list[OcrOutcome]) -> tuple[str, str, int]:
56
+ if not outcomes:
57
+ return "not_needed", "", 0
58
+ statuses = {outcome.status for outcome in outcomes}
59
+ retry = 1 if any(outcome.retry_eligible for outcome in outcomes) else 0
60
+ error = next((outcome.error_code for outcome in outcomes if outcome.error_code), "")
61
+ if statuses == {"blocked"}:
62
+ return "blocked", error, retry
63
+ if statuses == {"failed"}:
64
+ return "failed", error, retry
65
+ if "complete" in statuses and statuses <= {"complete", "not_needed"}:
66
+ return "complete", "", retry
67
+ if "complete" in statuses:
68
+ return "partial", error, retry
69
+ if "blocked" in statuses:
70
+ return "blocked", error, retry
71
+ return "needed", error, retry
@@ -0,0 +1,35 @@
1
+ """Paths for the local PDF library state."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from pathlib import Path
6
+
7
+ APP_STATE_SUBDIR = Path(".mednotes")
8
+ PDF_LIBRARY_SUBDIR = Path("pdf-library")
9
+
10
+
11
+ def home_dir() -> Path:
12
+ return Path(os.environ.get("HOME") or os.environ.get("USERPROFILE") or Path.home()).expanduser()
13
+
14
+
15
+ def app_state_dir() -> Path:
16
+ return home_dir() / APP_STATE_SUBDIR
17
+
18
+
19
+ def app_home() -> Path:
20
+ return app_state_dir() / PDF_LIBRARY_SUBDIR
21
+
22
+
23
+ def app_config_path() -> Path:
24
+ return app_state_dir() / "config.toml"
25
+
26
+
27
+ def database_path(base: Path | None = None) -> Path:
28
+ root = base or app_home()
29
+ return root / "library.sqlite3"
30
+
31
+
32
+ def extension_root() -> Path:
33
+ from mednotes.platform.paths import extension_root as _root
34
+
35
+ return _root()
@@ -0,0 +1,77 @@
1
+ """Lazy PyMuPDF boundary."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class PdfImageBlock:
11
+ bbox: tuple[float, float, float, float]
12
+ image_sha256: str
13
+ crop_path: Path
14
+ width_px: int
15
+ height_px: int
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class PdfPageExtract:
20
+ pdf_sha256: str
21
+ page_number: int
22
+ width_px: int
23
+ height_px: int
24
+ text: str
25
+ has_text_layer: bool
26
+ image_blocks: list[PdfImageBlock] = field(default_factory=list)
27
+ thumbnail_path: Path | None = None
28
+
29
+
30
+ def sha256_file(path: Path) -> str:
31
+ digest = hashlib.sha256()
32
+ with path.open("rb") as handle:
33
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
34
+ digest.update(chunk)
35
+ return digest.hexdigest()
36
+
37
+
38
+ def extract_pages(pdf_path: Path, *, app_home: Path) -> tuple[str, int, list[PdfPageExtract]]:
39
+ import fitz
40
+
41
+ pdf_sha = sha256_file(pdf_path)
42
+ pages: list[PdfPageExtract] = []
43
+ thumb_dir = app_home / "thumbnails" / pdf_sha
44
+ thumb_dir.mkdir(parents=True, exist_ok=True)
45
+ doc = fitz.open(pdf_path)
46
+ try:
47
+ for page_index in range(len(doc)):
48
+ index = page_index + 1
49
+ page = doc[page_index]
50
+ raw_text = page.get_text("text")
51
+ text = raw_text if isinstance(raw_text, str) else ""
52
+ rect = page.rect
53
+ thumb_path = thumb_dir / f"page-{index}.png"
54
+ try:
55
+ pix = page.get_pixmap(matrix=fitz.Matrix(0.35, 0.35), alpha=False)
56
+ pix.save(str(thumb_path))
57
+ width_px = int(pix.width)
58
+ height_px = int(pix.height)
59
+ except Exception:
60
+ width_px = int(rect.width)
61
+ height_px = int(rect.height)
62
+ thumb_path = None
63
+ pages.append(
64
+ PdfPageExtract(
65
+ pdf_sha256=pdf_sha,
66
+ page_number=index,
67
+ width_px=width_px,
68
+ height_px=height_px,
69
+ text=text.strip(),
70
+ has_text_layer=bool(text.strip()),
71
+ image_blocks=[],
72
+ thumbnail_path=thumb_path,
73
+ )
74
+ )
75
+ finally:
76
+ doc.close()
77
+ return pdf_sha, len(pages), pages
@@ -0,0 +1,155 @@
1
+ """SQLite schema for the PDF library."""
2
+ from __future__ import annotations
3
+
4
+ SCHEMA_VERSION = "1"
5
+
6
+ SCHEMA_SQL = """
7
+ CREATE TABLE IF NOT EXISTS metadata (
8
+ key TEXT PRIMARY KEY,
9
+ value TEXT NOT NULL
10
+ );
11
+
12
+ CREATE TABLE IF NOT EXISTS documents (
13
+ pdf_sha256 TEXT PRIMARY KEY,
14
+ path TEXT NOT NULL,
15
+ filename TEXT NOT NULL,
16
+ title_guess TEXT NOT NULL DEFAULT '',
17
+ page_count INTEGER NOT NULL,
18
+ has_text_layer INTEGER NOT NULL DEFAULT 0,
19
+ ocr_status TEXT NOT NULL CHECK (ocr_status IN ('not_needed','needed','blocked','partial','complete','failed')),
20
+ ocr_error_code TEXT NOT NULL DEFAULT '',
21
+ ocr_retry_eligible INTEGER NOT NULL DEFAULT 0 CHECK (ocr_retry_eligible IN (0, 1)),
22
+ source_mtime_ns INTEGER NOT NULL,
23
+ source_size_bytes INTEGER NOT NULL,
24
+ indexed_at TEXT NOT NULL,
25
+ removed_at TEXT
26
+ );
27
+
28
+ CREATE TABLE IF NOT EXISTS pages (
29
+ pdf_sha256 TEXT NOT NULL REFERENCES documents(pdf_sha256) ON DELETE CASCADE,
30
+ page_number INTEGER NOT NULL,
31
+ text_source TEXT NOT NULL CHECK (text_source IN ('digital','ocr','none')),
32
+ text TEXT NOT NULL DEFAULT '',
33
+ ocr_confidence REAL,
34
+ ocr_status TEXT NOT NULL CHECK (ocr_status IN ('not_needed','needed','blocked','partial','complete','failed')),
35
+ ocr_error_code TEXT NOT NULL DEFAULT '',
36
+ ocr_retry_eligible INTEGER NOT NULL DEFAULT 0 CHECK (ocr_retry_eligible IN (0, 1)),
37
+ section_path_guess TEXT NOT NULL DEFAULT '[]',
38
+ page_thumbnail_path TEXT NOT NULL DEFAULT '',
39
+ width_px INTEGER NOT NULL DEFAULT 0,
40
+ height_px INTEGER NOT NULL DEFAULT 0,
41
+ updated_at TEXT NOT NULL,
42
+ PRIMARY KEY (pdf_sha256, page_number)
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS figures (
46
+ figure_uid TEXT PRIMARY KEY,
47
+ pdf_sha256 TEXT NOT NULL REFERENCES documents(pdf_sha256) ON DELETE CASCADE,
48
+ page_number INTEGER NOT NULL,
49
+ bbox_json TEXT NOT NULL DEFAULT '[]',
50
+ crop_path TEXT NOT NULL DEFAULT '',
51
+ thumbnail_path TEXT NOT NULL DEFAULT '',
52
+ image_sha256 TEXT NOT NULL DEFAULT '',
53
+ figure_id TEXT NOT NULL DEFAULT '',
54
+ display_label TEXT NOT NULL DEFAULT '',
55
+ caption TEXT NOT NULL DEFAULT '',
56
+ caption_bbox_json TEXT NOT NULL DEFAULT '[]',
57
+ visual_quality_score REAL NOT NULL DEFAULT 0,
58
+ evidence_level TEXT NOT NULL CHECK (evidence_level IN ('caption_and_mentions','caption_only','mentions_only','page_context_only','visual_only')),
59
+ is_low_confidence INTEGER NOT NULL DEFAULT 1,
60
+ conflict_reason TEXT NOT NULL DEFAULT '',
61
+ created_at TEXT NOT NULL,
62
+ updated_at TEXT NOT NULL
63
+ );
64
+
65
+ CREATE TABLE IF NOT EXISTS mentions (
66
+ mention_uid TEXT PRIMARY KEY,
67
+ pdf_sha256 TEXT NOT NULL REFERENCES documents(pdf_sha256) ON DELETE CASCADE,
68
+ figure_uid TEXT REFERENCES figures(figure_uid) ON DELETE SET NULL,
69
+ page_number INTEGER NOT NULL,
70
+ figure_id TEXT NOT NULL DEFAULT '',
71
+ display_label TEXT NOT NULL DEFAULT '',
72
+ sentence TEXT NOT NULL DEFAULT '',
73
+ paragraph TEXT NOT NULL DEFAULT '',
74
+ section_path_guess TEXT NOT NULL DEFAULT '[]',
75
+ offset_start INTEGER NOT NULL DEFAULT 0,
76
+ offset_end INTEGER NOT NULL DEFAULT 0,
77
+ confidence REAL NOT NULL DEFAULT 0,
78
+ created_at TEXT NOT NULL
79
+ );
80
+
81
+ CREATE TABLE IF NOT EXISTS anchor_cache (
82
+ cache_key TEXT NOT NULL,
83
+ note_sha256 TEXT NOT NULL,
84
+ anchor_id TEXT NOT NULL,
85
+ note_path TEXT NOT NULL DEFAULT '',
86
+ section_path_json TEXT NOT NULL DEFAULT '[]',
87
+ concept TEXT NOT NULL,
88
+ visual_type TEXT NOT NULL,
89
+ search_queries_json TEXT NOT NULL DEFAULT '[]',
90
+ provider TEXT NOT NULL DEFAULT '',
91
+ model_id TEXT NOT NULL DEFAULT '',
92
+ preferred_language TEXT NOT NULL DEFAULT '',
93
+ max_anchors INTEGER NOT NULL DEFAULT 0,
94
+ prompt_version TEXT NOT NULL DEFAULT 'pdf-library-anchors-v1',
95
+ created_at TEXT NOT NULL,
96
+ payload_json TEXT NOT NULL,
97
+ PRIMARY KEY (cache_key, anchor_id)
98
+ );
99
+
100
+ CREATE TABLE IF NOT EXISTS search_receipts (
101
+ receipt_uid TEXT PRIMARY KEY,
102
+ query_text TEXT NOT NULL DEFAULT '',
103
+ note_path TEXT NOT NULL DEFAULT '',
104
+ anchor_id TEXT NOT NULL DEFAULT '',
105
+ provider TEXT NOT NULL DEFAULT 'local',
106
+ status TEXT NOT NULL,
107
+ created_at TEXT NOT NULL,
108
+ payload_json TEXT NOT NULL
109
+ );
110
+
111
+ CREATE TABLE IF NOT EXISTS embedding_runs (
112
+ run_uid TEXT PRIMARY KEY,
113
+ provider TEXT NOT NULL,
114
+ model_id TEXT NOT NULL,
115
+ dimensions INTEGER NOT NULL DEFAULT 0,
116
+ status TEXT NOT NULL DEFAULT 'reserved',
117
+ created_at TEXT NOT NULL,
118
+ payload_json TEXT NOT NULL DEFAULT '{}'
119
+ );
120
+
121
+ CREATE VIRTUAL TABLE IF NOT EXISTS page_fts USING fts5(
122
+ pdf_sha256 UNINDEXED,
123
+ page_number UNINDEXED,
124
+ title_guess,
125
+ section_path_guess,
126
+ text,
127
+ tokenize = 'unicode61 remove_diacritics 2'
128
+ );
129
+
130
+ CREATE VIRTUAL TABLE IF NOT EXISTS figure_fts USING fts5(
131
+ figure_uid UNINDEXED,
132
+ pdf_sha256 UNINDEXED,
133
+ figure_id,
134
+ display_label,
135
+ caption,
136
+ tokenize = 'unicode61 remove_diacritics 2'
137
+ );
138
+
139
+ CREATE VIRTUAL TABLE IF NOT EXISTS mention_fts USING fts5(
140
+ mention_uid UNINDEXED,
141
+ pdf_sha256 UNINDEXED,
142
+ figure_id,
143
+ sentence,
144
+ paragraph,
145
+ section_path_guess,
146
+ tokenize = 'unicode61 remove_diacritics 2'
147
+ );
148
+
149
+ CREATE INDEX IF NOT EXISTS idx_documents_path ON documents(path);
150
+ CREATE INDEX IF NOT EXISTS idx_pages_pdf_page ON pages(pdf_sha256, page_number);
151
+ CREATE INDEX IF NOT EXISTS idx_figures_pdf_page ON figures(pdf_sha256, page_number);
152
+ CREATE INDEX IF NOT EXISTS idx_figures_figure_id ON figures(pdf_sha256, figure_id);
153
+ CREATE INDEX IF NOT EXISTS idx_mentions_figure_id ON mentions(pdf_sha256, figure_id);
154
+ CREATE INDEX IF NOT EXISTS idx_anchor_cache_note ON anchor_cache(note_sha256);
155
+ """
@@ -0,0 +1,188 @@
1
+ """Local PDF library search and ranking."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import sqlite3
6
+ import uuid
7
+ from dataclasses import dataclass, field
8
+ from datetime import UTC, datetime
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from mednotes.domains.wiki.capabilities.pdf import cloud, db, paths
13
+ from mednotes.kernel.base import JsonObject, JsonObjectAdapter, JsonValue
14
+
15
+ SCHEMA = "medical-notes-workbench.pdf-library-search-results.v1"
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class SearchHit:
20
+ figure_uid: str
21
+ score: float
22
+ why: list[str]
23
+ evidence_level: str
24
+ is_low_confidence: bool
25
+ provider_receipts: list[dict[str, object]] = field(default_factory=list)
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class SearchRequest:
30
+ query_text: str = ""
31
+ note_path: Path | None = None
32
+ anchor_id: str = ""
33
+ provider: str = "local"
34
+ top_k: int = 20
35
+
36
+
37
+ def search(request: SearchRequest, *, app_home: Path | None = None) -> dict[str, Any]:
38
+ provider = cloud.resolve_provider(request.provider)
39
+ if provider["status"] == "blocked":
40
+ return {
41
+ "schema": SCHEMA,
42
+ "status": "blocked",
43
+ "phase": "search",
44
+ "blocked_reason": provider["blocked_reason"],
45
+ "next_action": provider["next_action"],
46
+ "provider_receipts": [provider],
47
+ }
48
+ query = request.query_text.strip()
49
+ if not query and request.note_path is not None:
50
+ query = _query_from_note_anchor(request, app_home=app_home)
51
+ if not query:
52
+ return _blocked("anchor_not_found", "choose an existing anchor or pass --query")
53
+ if not query:
54
+ return _blocked("missing_query_or_note", "pass --query TEXT or --note NOTE")
55
+ root = app_home or paths.app_home()
56
+ conn = db.open_database(paths.database_path(root))
57
+ hits = _local_search(conn, query, request.top_k)
58
+ payload = {
59
+ "schema": SCHEMA,
60
+ "status": "ok",
61
+ "phase": "search",
62
+ "query_text": query,
63
+ "results": [hit.__dict__ for hit in hits],
64
+ "provider_receipts": [] if request.provider == "local" else [provider],
65
+ }
66
+ _record_receipt(conn, request, payload)
67
+ return payload
68
+
69
+
70
+ def _local_search(conn: sqlite3.Connection, query: str, top_k: int) -> list[SearchHit]:
71
+ scores: dict[str, float] = {}
72
+ why: dict[str, set[str]] = {}
73
+ for source, table, uid_col, reason in (
74
+ ("figure", "figure_fts", "figure_uid", "caption match"),
75
+ ("mention", "mention_fts", "mention_uid", "mention match"),
76
+ ("page", "page_fts", "page_number", "page text match"),
77
+ ):
78
+ try:
79
+ rows = conn.execute(f"SELECT * FROM {table} WHERE {table} MATCH ? LIMIT 100", (query,)).fetchall()
80
+ except sqlite3.OperationalError:
81
+ rows = []
82
+ for row in rows:
83
+ if source == "figure":
84
+ figure_uid = str(row[uid_col])
85
+ elif source == "mention":
86
+ mention = conn.execute("SELECT figure_uid FROM mentions WHERE mention_uid = ?", (row[uid_col],)).fetchone()
87
+ if not mention or not mention["figure_uid"]:
88
+ continue
89
+ figure_uid = str(mention["figure_uid"])
90
+ else:
91
+ figure = conn.execute(
92
+ "SELECT figure_uid FROM figures WHERE pdf_sha256 = ? AND page_number = ? LIMIT 1",
93
+ (row["pdf_sha256"], row["page_number"]),
94
+ ).fetchone()
95
+ if not figure:
96
+ continue
97
+ figure_uid = str(figure["figure_uid"])
98
+ scores[figure_uid] = scores.get(figure_uid, 0.0) + (7.0 if source == "figure" else 4.0 if source == "mention" else 1.5)
99
+ why.setdefault(figure_uid, set()).add(reason)
100
+ if not scores:
101
+ like = f"%{query}%"
102
+ for row in conn.execute(
103
+ """
104
+ SELECT figure_uid FROM figures
105
+ JOIN documents USING(pdf_sha256)
106
+ WHERE documents.removed_at IS NULL AND (caption LIKE ? OR display_label LIKE ?)
107
+ LIMIT 100
108
+ """,
109
+ (like, like),
110
+ ):
111
+ scores[str(row["figure_uid"])] = 3.0
112
+ why.setdefault(str(row["figure_uid"]), set()).add("caption contains query")
113
+ hits: list[SearchHit] = []
114
+ for figure_uid, score in scores.items():
115
+ row = conn.execute(
116
+ """
117
+ SELECT figures.figure_uid, figures.evidence_level, figures.is_low_confidence, figures.conflict_reason
118
+ FROM figures JOIN documents USING(pdf_sha256)
119
+ WHERE figures.figure_uid = ? AND documents.removed_at IS NULL
120
+ """,
121
+ (figure_uid,),
122
+ ).fetchone()
123
+ if not row:
124
+ continue
125
+ evidence = str(row["evidence_level"])
126
+ if evidence == "caption_and_mentions":
127
+ score += 4
128
+ if evidence == "visual_only":
129
+ score -= 3
130
+ if row["conflict_reason"]:
131
+ score -= 4
132
+ hits.append(
133
+ SearchHit(
134
+ figure_uid=figure_uid,
135
+ score=round(score, 3),
136
+ why=sorted(why.get(figure_uid, set())),
137
+ evidence_level=evidence,
138
+ is_low_confidence=bool(row["is_low_confidence"]),
139
+ )
140
+ )
141
+ return sorted(hits, key=lambda hit: hit.score, reverse=True)[:top_k]
142
+
143
+
144
+ def _query_from_note_anchor(request: SearchRequest, *, app_home: Path | None) -> str:
145
+ if request.note_path is None:
146
+ return ""
147
+ conn = db.open_database(paths.database_path(app_home or paths.app_home()))
148
+ note_path = str(request.note_path)
149
+ row = conn.execute(
150
+ "SELECT payload_json FROM anchor_cache WHERE note_path = ? AND anchor_id = ? LIMIT 1",
151
+ (note_path, request.anchor_id),
152
+ ).fetchone()
153
+ if not row:
154
+ return ""
155
+ payload = JsonObjectAdapter.validate_python(json.loads(str(row["payload_json"])))
156
+ queries = payload.get("search_queries")
157
+ if isinstance(queries, list) and queries:
158
+ return str(queries[0])
159
+ return str(payload.get("concept") or "")
160
+
161
+
162
+ def _optional_path_text(path: Path | None) -> str:
163
+ return str(path) if path is not None else ""
164
+
165
+
166
+ def _record_receipt(conn: sqlite3.Connection, request: SearchRequest, payload: JsonObject) -> None:
167
+ with conn:
168
+ conn.execute(
169
+ "INSERT INTO search_receipts(receipt_uid, query_text, note_path, anchor_id, provider, status, created_at, payload_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
170
+ (
171
+ str(uuid.uuid4()),
172
+ request.query_text,
173
+ _optional_path_text(request.note_path),
174
+ request.anchor_id,
175
+ request.provider,
176
+ str(_json_field(payload, "status")),
177
+ datetime.now(UTC).isoformat(),
178
+ json.dumps(payload, ensure_ascii=False),
179
+ ),
180
+ )
181
+
182
+
183
+ def _blocked(reason: str, next_action: str) -> JsonObject:
184
+ return {"schema": SCHEMA, "status": "blocked", "phase": "search", "blocked_reason": reason, "next_action": next_action}
185
+
186
+
187
+ def _json_field(source: JsonObject, key: str, default: JsonValue = None) -> JsonValue:
188
+ return source.get(key, default)
@@ -0,0 +1 @@
1
+ """Textual UI for the PDF library."""
@@ -0,0 +1,89 @@
1
+ """Minimal functional Textual review app for the PDF library."""
2
+ from __future__ import annotations
3
+
4
+ from textual.app import App, ComposeResult
5
+ from textual.widgets import Footer, Static
6
+
7
+ from mednotes.domains.wiki.capabilities.pdf.tui import image_backend as image_backend_mod
8
+ from mednotes.domains.wiki.capabilities.pdf.tui.state import PdfLibraryState
9
+
10
+
11
+ class RenderableStatic(Static):
12
+ @property
13
+ def renderable(self) -> str:
14
+ return str(self.content)
15
+
16
+
17
+ class PdfLibraryApp(App[None]):
18
+ BINDINGS = [
19
+ ("i", "ingest", "Ingest"),
20
+ ("s", "search", "Search"),
21
+ ("enter", "select_first", "Select"),
22
+ ("p", "preview", "Preview"),
23
+ ("d", "doctor", "Doctor"),
24
+ ("q", "quit", "Quit"),
25
+ ]
26
+
27
+ CSS = """
28
+ Screen { layout: vertical; }
29
+ #screen-title { height: 3; padding: 1 2; text-style: bold; }
30
+ #body { padding: 1 2; }
31
+ """
32
+
33
+ def __init__(self, *, state: PdfLibraryState | None = None, image_backend: str = "auto") -> None:
34
+ super().__init__()
35
+ self.state = state or PdfLibraryState()
36
+ self.backend = image_backend_mod.detect(preferred=image_backend)
37
+
38
+ def compose(self) -> ComposeResult:
39
+ yield RenderableStatic(self._title(), id="screen-title")
40
+ yield Static(self._body(), id="body")
41
+ yield Footer()
42
+
43
+ def on_mount(self) -> None:
44
+ self._refresh()
45
+
46
+ def action_doctor(self) -> None:
47
+ self.state.active_screen = "doctor"
48
+ self._refresh()
49
+
50
+ def action_ingest(self) -> None:
51
+ self.state.active_screen = "ingest"
52
+ self._refresh()
53
+
54
+ def action_search(self) -> None:
55
+ self.state.active_screen = "search"
56
+ self._refresh()
57
+
58
+ def action_select_first(self) -> None:
59
+ if self.state.search_results:
60
+ self.state.select_figure(self.state.search_results[0].figure_uid)
61
+ self.state.active_screen = "figure_review"
62
+ self._refresh()
63
+
64
+ def action_preview(self) -> None:
65
+ self.state.active_screen = "insert_preview"
66
+ self._refresh()
67
+
68
+ def _refresh(self) -> None:
69
+ self.query_one("#screen-title", Static).update(self._title())
70
+ self.query_one("#body", Static).update(self._body())
71
+
72
+ def _title(self) -> str:
73
+ labels = {
74
+ "doctor": "Doctor / Setup",
75
+ "ingest": "Ingest Queue",
76
+ "search": "Search",
77
+ "figure_review": "Figure Review",
78
+ "insert_preview": "Insert Preview",
79
+ }
80
+ return labels.get(self.state.active_screen, self.state.active_screen)
81
+
82
+ def _body(self) -> str:
83
+ return (
84
+ f"Backend: {self.backend.name}\n"
85
+ f"Note: {self.state.selected_note or '-'}\n"
86
+ f"Queued PDFs: {len(self.state.ingest_queue)}\n"
87
+ f"Results: {len(self.state.search_results)}\n"
88
+ f"Selected figure: {self.state.selected_figure_uid or '-'}"
89
+ )