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,223 @@
1
+ """Cache SQLite único para o pipeline.
2
+
3
+ Três tabelas:
4
+ - ``anchors``: chave ``sha256(markdown_body)``, JSON da lista de âncoras
5
+ produzida na Etapa 1 (Gemini). Sem TTL — markdown idêntico → mesma saída.
6
+ - ``candidates``: chave ``(source, query, visual_type)``, JSON da lista de
7
+ candidatas devolvida por um adapter na Etapa 2. TTL configurável (default
8
+ 30d) porque APIs de fonte mudam.
9
+ - ``images``: chave ``sha256`` do conteúdo binário. Mapeia para o filename
10
+ local. Permanente (asset baixado é asset baixado).
11
+
12
+ API minimalista; só ``get_*`` / ``put_*`` por tabela. Sem migrations — o
13
+ schema é idempotente via ``CREATE TABLE IF NOT EXISTS``.
14
+
15
+ ``Cache`` aceita ``clock`` injetável para tornar TTL testável sem manipular
16
+ relógio do sistema. Suporta ``":memory:"`` para testes.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import sqlite3
22
+ import time
23
+ from collections.abc import Callable
24
+ from pathlib import Path
25
+
26
+ from mednotes.domains.wiki.capabilities.illustrate.core.config import expand_path
27
+ from mednotes.kernel.base import JsonArrayAdapter, JsonObject, JsonObjectAdapter
28
+
29
+ __all__ = ["Cache"]
30
+
31
+
32
+ _SCHEMA = """
33
+ CREATE TABLE IF NOT EXISTS anchors (
34
+ note_sha TEXT PRIMARY KEY,
35
+ payload TEXT NOT NULL,
36
+ created_at REAL NOT NULL
37
+ );
38
+ CREATE TABLE IF NOT EXISTS candidates (
39
+ source TEXT NOT NULL,
40
+ query TEXT NOT NULL,
41
+ visual_type TEXT NOT NULL,
42
+ payload TEXT NOT NULL,
43
+ created_at REAL NOT NULL,
44
+ PRIMARY KEY (source, query, visual_type)
45
+ );
46
+ CREATE TABLE IF NOT EXISTS images (
47
+ sha TEXT PRIMARY KEY,
48
+ filename TEXT NOT NULL,
49
+ source TEXT NOT NULL,
50
+ source_url TEXT NOT NULL,
51
+ width INTEGER,
52
+ height INTEGER,
53
+ bytes INTEGER,
54
+ created_at REAL NOT NULL
55
+ );
56
+ CREATE TABLE IF NOT EXISTS url_index (
57
+ image_url TEXT PRIMARY KEY,
58
+ sha TEXT NOT NULL,
59
+ created_at REAL NOT NULL
60
+ );
61
+ """
62
+
63
+
64
+ def _json_object_list(payload: str) -> list[JsonObject]:
65
+ """Validate cached JSON arrays before returning them to workflow code."""
66
+
67
+ values = JsonArrayAdapter.validate_python(json.loads(payload))
68
+ return [JsonObjectAdapter.validate_python(item) for item in values]
69
+
70
+
71
+ class Cache:
72
+ def __init__(
73
+ self,
74
+ path: str | Path,
75
+ *,
76
+ clock: Callable[[], float] = time.time,
77
+ ) -> None:
78
+ path_str = str(path)
79
+ if path_str == ":memory:":
80
+ self.path: str | Path = path_str
81
+ else:
82
+ resolved = expand_path(path_str)
83
+ resolved.parent.mkdir(parents=True, exist_ok=True)
84
+ self.path = resolved
85
+ self._conn = sqlite3.connect(str(self.path))
86
+ self._conn.executescript(_SCHEMA)
87
+ self._conn.commit()
88
+ self._clock = clock
89
+
90
+ def close(self) -> None:
91
+ self._conn.close()
92
+
93
+ def __enter__(self) -> Cache:
94
+ return self
95
+
96
+ def __exit__(self, *_exc: object) -> None:
97
+ self.close()
98
+
99
+ # --- anchors -----------------------------------------------------
100
+
101
+ def get_anchors(self, note_sha: str) -> list[JsonObject] | None:
102
+ row = self._conn.execute(
103
+ "SELECT payload FROM anchors WHERE note_sha = ?", (note_sha,)
104
+ ).fetchone()
105
+ return _json_object_list(row[0]) if row else None
106
+
107
+ def put_anchors(self, note_sha: str, anchors: list[JsonObject]) -> None:
108
+ payload = JsonArrayAdapter.validate_python(anchors)
109
+ self._conn.execute(
110
+ "INSERT OR REPLACE INTO anchors(note_sha, payload, created_at) "
111
+ "VALUES (?, ?, ?)",
112
+ (note_sha, json.dumps(payload, ensure_ascii=False), self._clock()),
113
+ )
114
+ self._conn.commit()
115
+
116
+ # --- candidates (TTL) --------------------------------------------
117
+
118
+ def get_candidates(
119
+ self,
120
+ source: str,
121
+ query: str,
122
+ visual_type: str,
123
+ *,
124
+ ttl_days: int,
125
+ ) -> list[JsonObject] | None:
126
+ row = self._conn.execute(
127
+ "SELECT payload, created_at FROM candidates "
128
+ "WHERE source = ? AND query = ? AND visual_type = ?",
129
+ (source, query, visual_type),
130
+ ).fetchone()
131
+ if not row:
132
+ return None
133
+ payload, created_at = row
134
+ age_days = (self._clock() - created_at) / 86400.0
135
+ if age_days > ttl_days:
136
+ return None
137
+ return _json_object_list(payload)
138
+
139
+ def put_candidates(
140
+ self,
141
+ source: str,
142
+ query: str,
143
+ visual_type: str,
144
+ candidates: list[JsonObject],
145
+ ) -> None:
146
+ payload = JsonArrayAdapter.validate_python(candidates)
147
+ self._conn.execute(
148
+ "INSERT OR REPLACE INTO candidates"
149
+ "(source, query, visual_type, payload, created_at) "
150
+ "VALUES (?, ?, ?, ?, ?)",
151
+ (
152
+ source,
153
+ query,
154
+ visual_type,
155
+ json.dumps(payload, ensure_ascii=False),
156
+ self._clock(),
157
+ ),
158
+ )
159
+ self._conn.commit()
160
+
161
+ # --- images (permanente) -----------------------------------------
162
+
163
+ def get_image(self, sha: str) -> JsonObject | None:
164
+ row = self._conn.execute(
165
+ "SELECT filename, source, source_url, width, height, bytes "
166
+ "FROM images WHERE sha = ?",
167
+ (sha,),
168
+ ).fetchone()
169
+ if not row:
170
+ return None
171
+ return JsonObjectAdapter.validate_python({
172
+ "sha": sha,
173
+ "filename": row[0],
174
+ "source": row[1],
175
+ "source_url": row[2],
176
+ "width": row[3],
177
+ "height": row[4],
178
+ "bytes": row[5],
179
+ })
180
+
181
+ def put_image(
182
+ self,
183
+ sha: str,
184
+ *,
185
+ filename: str,
186
+ source: str,
187
+ source_url: str,
188
+ width: int | None = None,
189
+ height: int | None = None,
190
+ size_bytes: int | None = None,
191
+ ) -> None:
192
+ self._conn.execute(
193
+ "INSERT OR REPLACE INTO images"
194
+ "(sha, filename, source, source_url, width, height, bytes, created_at)"
195
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
196
+ (
197
+ sha,
198
+ filename,
199
+ source,
200
+ source_url,
201
+ width,
202
+ height,
203
+ size_bytes,
204
+ self._clock(),
205
+ ),
206
+ )
207
+ self._conn.commit()
208
+
209
+ # --- url → sha lookup (evita re-baixar) -------------------------
210
+
211
+ def get_sha_for_url(self, image_url: str) -> str | None:
212
+ row = self._conn.execute(
213
+ "SELECT sha FROM url_index WHERE image_url = ?", (image_url,)
214
+ ).fetchone()
215
+ return row[0] if row else None
216
+
217
+ def put_url_index(self, image_url: str, sha: str) -> None:
218
+ self._conn.execute(
219
+ "INSERT OR REPLACE INTO url_index(image_url, sha, created_at) "
220
+ "VALUES (?, ?, ?)",
221
+ (image_url, sha, self._clock()),
222
+ )
223
+ self._conn.commit()
@@ -0,0 +1,131 @@
1
+ """Carrega configuração local e persistente do Medical Notes Workbench."""
2
+ from __future__ import annotations
3
+
4
+ import tomllib
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from mednotes.platform.paths import (
9
+ default_config_path as _shared_default_config_path,
10
+ )
11
+ from mednotes.platform.paths import (
12
+ expand_path as _shared_expand_path,
13
+ )
14
+ from mednotes.platform.paths import (
15
+ find_config as _shared_find_config,
16
+ )
17
+ from mednotes.platform.paths import (
18
+ resolve_wiki_dir,
19
+ )
20
+ from mednotes.platform.paths import (
21
+ user_state_dir as _shared_user_state_dir,
22
+ )
23
+
24
+ _DEFAULTS: dict[str, Any] = {
25
+ "vault": {"path": "", "attachments_subdir": "attachments/medicina"},
26
+ "enrichment": {
27
+ "max_anchors_per_note": 5,
28
+ "max_image_dimension": 1600,
29
+ "webp_min_savings_pct": 30,
30
+ # Idioma preferido das figuras retornadas. Afeta:
31
+ # - queries que o gemini gera (pt-br adiciona 1 query em PT)
32
+ # - params do SerpAPI (hl/gl)
33
+ # - regra de desempate no rerank (prefere figuras com texto no idioma)
34
+ # Valores: "pt-br", "en" (default), "any" (sem hl/gl).
35
+ "preferred_language": "en",
36
+ },
37
+ "sources": {
38
+ "enabled": [
39
+ "wikimedia",
40
+ "radiopaedia",
41
+ "nih_open_i",
42
+ "openstax",
43
+ "dermnet",
44
+ "teachmeanatomy",
45
+ "web_search",
46
+ ],
47
+ "top_k_per_source": 6,
48
+ },
49
+ # `[gemini]` é consumido pelo orquestrador (`scripts/enrich_notes.py`),
50
+ # não pelo toolbox em si. O enricher core não invoca LLM.
51
+ "gemini": {
52
+ "binary": "gemini",
53
+ "model_anchors": "gemini-2.5-pro",
54
+ "model_rerank": "gemini-2.5-pro",
55
+ "max_candidates_per_anchor": 12,
56
+ "timeout_seconds": 120,
57
+ },
58
+ "download": {
59
+ # User-Agent pra fetch de bytes em `download.py`.
60
+ # Default: UA browser-like (Chrome/macOS) — destrava osmosis,
61
+ # thehealthy.com, e similares com anti-bot básico. Wikimedia também
62
+ # aceita (qualquer browser legítimo passa). Trocar de volta pra UA
63
+ # identificável (`medical-notes-workbench/0.1 (...)`) é mais
64
+ # respeitoso mas perde fontes; veja config.example.toml.
65
+ "user_agent": (
66
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
67
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
68
+ "Chrome/131.0.0.0 Safari/537.36"
69
+ ),
70
+ },
71
+ "cache": {
72
+ "path": "~/Documents/medical-notes-workbench/cache.db",
73
+ "candidates_ttl_days": 30,
74
+ },
75
+ }
76
+
77
+
78
+ def expand_path(p: str) -> Path:
79
+ return _shared_expand_path(p)
80
+
81
+
82
+ def user_state_dir() -> Path:
83
+ """Diretório persistente para estado editável pelo usuário.
84
+
85
+ A extensão Gemini CLI é auto-updatable e pode recriar
86
+ ``~/.gemini/extensions/medical-notes-workbench``. Configuração, chaves,
87
+ cache e venv não devem depender desse diretório volátil.
88
+ """
89
+ return _shared_user_state_dir()
90
+
91
+
92
+ def default_config_path() -> Path:
93
+ return user_state_dir() / "config.toml"
94
+
95
+
96
+ def default_env_path() -> Path:
97
+ return user_state_dir() / ".env"
98
+
99
+
100
+ def _deep_merge(base: dict[str, Any], over: dict[str, Any]) -> dict[str, Any]:
101
+ out = dict(base)
102
+ for k, v in over.items():
103
+ if isinstance(v, dict) and isinstance(out.get(k), dict):
104
+ out[k] = _deep_merge(out[k], v)
105
+ else:
106
+ out[k] = v
107
+ return out
108
+
109
+
110
+ def find_config(start: Path | None = None) -> Path | None:
111
+ return _shared_find_config(start=start)
112
+
113
+
114
+ def load(path: Path | None = None) -> dict[str, Any]:
115
+ if path is None:
116
+ path = find_config()
117
+ if path is None or not path.exists():
118
+ return dict(_DEFAULTS)
119
+ with path.open("rb") as f:
120
+ data = tomllib.load(f)
121
+ return _deep_merge(_DEFAULTS, data)
122
+
123
+
124
+ def resolve_wiki_root(config_path: Path | None = None, *, start: Path | None = None) -> Path | None:
125
+ """Return canonical wiki root for workflows that can fall back from vault.path."""
126
+ resolution = resolve_wiki_dir(config=config_path, start=start or Path.cwd(), enable_gemini_probe=False)
127
+ return resolution.path if resolution.ok else None
128
+
129
+
130
+ def wiki_memory_path() -> Path:
131
+ return _shared_default_config_path()
@@ -0,0 +1,224 @@
1
+ """Etapa 4 (numeração nova): fetch + validate + resize + dedupe.
2
+
3
+ Contrato:
4
+ - Recebe URL, ``vault_dir`` e parâmetros de redimensionamento/encoding.
5
+ - Baixa via ``httpx``, valida o conteúdo via ``Pillow.Image.open`` (magic
6
+ number — proteção contra Google/Bing servir HTML quando o asset some).
7
+ - Redimensiona se ``max(width, height) > max_dim`` (LANCZOS).
8
+ - Decide encoding final: tenta WebP; mantém WebP se a economia for ≥
9
+ ``webp_min_savings_pct``%, senão preserva o formato original
10
+ (PNG/JPEG; GIF vira PNG single-frame; SVG não é suportado).
11
+ - SHA-256 sobre os **bytes finais** (após resize/recode), pra dedupe correta.
12
+ - Idempotência por dois níveis:
13
+ 1. ``cache.get_sha_for_url(url)``: se conhecemos a URL, recuperamos o SHA
14
+ sem ir à rede.
15
+ 2. ``cache.get_image(sha)`` + arquivo existe: reusa.
16
+
17
+ Erros:
18
+ - :class:`DownloadError` para falhas HTTP, conteúdo não-imagem, formato não
19
+ suportado.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import hashlib
24
+ import io
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ import httpx
29
+ from PIL import Image, UnidentifiedImageError
30
+
31
+ from mednotes.domains.wiki.capabilities.illustrate.core.cache import Cache
32
+
33
+ __all__ = ["DownloadError", "download"]
34
+
35
+
36
+ class DownloadError(RuntimeError):
37
+ """Falha no download/validação/encoding de uma imagem."""
38
+
39
+
40
+ _SUPPORTED_FORMATS = {"PNG", "JPEG", "WEBP", "GIF"}
41
+ _FORMAT_TO_EXT = {"PNG": "png", "JPEG": "jpg", "WEBP": "webp", "GIF": "gif"}
42
+
43
+ # Wikimedia (e outros) rejeitam UAs genéricos como `python-httpx/X.Y` com 403.
44
+ _DEFAULT_USER_AGENT = (
45
+ "medical-notes-workbench/0.1 (personal study; "
46
+ "https://github.com/augustocaruso/medical-notes-workbench)"
47
+ )
48
+
49
+
50
+ def download(
51
+ url: str,
52
+ *,
53
+ vault_dir: Path,
54
+ max_dim: int = 1600,
55
+ webp_min_savings_pct: int = 30,
56
+ cache: Cache | None = None,
57
+ client: httpx.Client | None = None,
58
+ source: str = "unknown",
59
+ source_url: str | None = None,
60
+ user_agent: str | None = None,
61
+ ) -> dict[str, Any]:
62
+ """Baixa, valida, normaliza e indexa uma imagem.
63
+
64
+ Devolve ``{sha, filename, path, width, height, bytes, source, source_url, cached}``.
65
+ ``cached=True`` quando a imagem já estava conhecida (por URL ou SHA) e o
66
+ arquivo no vault existe — nesse caso não houve fetch nem reescrita.
67
+ """
68
+ # 1) URL cache hit → evita HTTP inteiramente.
69
+ if cache is not None:
70
+ sha_known = cache.get_sha_for_url(url)
71
+ if sha_known:
72
+ existing = cache.get_image(sha_known)
73
+ if existing:
74
+ path = vault_dir / existing["filename"]
75
+ if path.exists():
76
+ return _hit_dict(existing, path, source_url=source_url or url)
77
+
78
+ # 2) Fetch.
79
+ own = client is None
80
+ request_headers = _browser_like_headers(
81
+ user_agent=user_agent or _DEFAULT_USER_AGENT,
82
+ referer=source_url,
83
+ )
84
+ if own:
85
+ client = httpx.Client(
86
+ timeout=30.0,
87
+ follow_redirects=True,
88
+ )
89
+ try:
90
+ try:
91
+ resp = client.get(url, headers=request_headers)
92
+ resp.raise_for_status()
93
+ except httpx.HTTPError as e:
94
+ raise DownloadError(f"falha HTTP em {url}: {e}") from e
95
+ raw = resp.content
96
+ finally:
97
+ if own:
98
+ client.close()
99
+
100
+ # 3) Valida (magic number via Pillow).
101
+ try:
102
+ img = Image.open(io.BytesIO(raw))
103
+ img.load()
104
+ except (UnidentifiedImageError, OSError) as e:
105
+ raise DownloadError(f"conteúdo de {url} não é imagem válida: {e}") from e
106
+
107
+ fmt = (img.format or "").upper()
108
+ if fmt not in _SUPPORTED_FORMATS:
109
+ raise DownloadError(f"formato {fmt!r} não suportado ({url})")
110
+
111
+ # 4) Resize.
112
+ if max(img.size) > max_dim:
113
+ img.thumbnail((max_dim, max_dim), Image.Resampling.LANCZOS)
114
+
115
+ # 5) Decide encoding final.
116
+ final_bytes, final_fmt = _encode(
117
+ img, original_fmt=fmt, webp_min_savings_pct=webp_min_savings_pct
118
+ )
119
+ sha = hashlib.sha256(final_bytes).hexdigest()
120
+ ext = _FORMAT_TO_EXT[final_fmt]
121
+ filename = f"{sha[:12]}.{ext}"
122
+ out_path = vault_dir / filename
123
+
124
+ # 6) SHA cache hit → arquivo já existe (ou existia em outro lugar).
125
+ if cache is not None:
126
+ existing = cache.get_image(sha)
127
+ if existing and (vault_dir / existing["filename"]).exists():
128
+ cache.put_url_index(url, sha)
129
+ return _hit_dict(existing, vault_dir / existing["filename"], source_url=source_url or url)
130
+
131
+ # 7) Grava + indexa.
132
+ vault_dir.mkdir(parents=True, exist_ok=True)
133
+ if not out_path.exists():
134
+ out_path.write_bytes(final_bytes)
135
+
136
+ width, height = img.size
137
+ size_bytes = len(final_bytes)
138
+
139
+ if cache is not None:
140
+ cache.put_image(
141
+ sha,
142
+ filename=filename,
143
+ source=source,
144
+ source_url=source_url or url,
145
+ width=width,
146
+ height=height,
147
+ size_bytes=size_bytes,
148
+ )
149
+ cache.put_url_index(url, sha)
150
+
151
+ return {
152
+ "sha": sha,
153
+ "filename": filename,
154
+ "path": str(out_path),
155
+ "width": width,
156
+ "height": height,
157
+ "bytes": size_bytes,
158
+ "source": source,
159
+ "source_url": source_url or url,
160
+ "cached": False,
161
+ }
162
+
163
+
164
+ # --- helpers --------------------------------------------------------
165
+
166
+
167
+ def _hit_dict(existing: dict[str, Any], path: Path, *, source_url: str) -> dict[str, Any]:
168
+ return {
169
+ "sha": existing["sha"],
170
+ "filename": existing["filename"],
171
+ "path": str(path),
172
+ "width": existing.get("width"),
173
+ "height": existing.get("height"),
174
+ "bytes": existing.get("bytes"),
175
+ "source": existing.get("source"),
176
+ "source_url": existing.get("source_url") or source_url,
177
+ "cached": True,
178
+ }
179
+
180
+
181
+ def _browser_like_headers(*, user_agent: str, referer: str | None) -> dict[str, str]:
182
+ headers = {
183
+ "User-Agent": user_agent,
184
+ "Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
185
+ "Accept-Language": "en-US,en;q=0.9,pt-BR;q=0.8,pt;q=0.7",
186
+ }
187
+ if referer:
188
+ headers["Referer"] = referer
189
+ return headers
190
+
191
+
192
+ def _encode(
193
+ img: Image.Image, *, original_fmt: str, webp_min_savings_pct: int
194
+ ) -> tuple[bytes, str]:
195
+ if original_fmt == "WEBP":
196
+ return _encode_as(img, "WEBP"), "WEBP"
197
+
198
+ target_fmt = "PNG" if original_fmt == "GIF" else original_fmt
199
+ orig_bytes = _encode_as(img, target_fmt)
200
+ webp_bytes = _encode_as(img, "WEBP")
201
+
202
+ if not orig_bytes:
203
+ return webp_bytes, "WEBP"
204
+
205
+ savings_pct = (len(orig_bytes) - len(webp_bytes)) / len(orig_bytes) * 100
206
+ if savings_pct >= webp_min_savings_pct:
207
+ return webp_bytes, "WEBP"
208
+ return orig_bytes, target_fmt
209
+
210
+
211
+ def _encode_as(img: Image.Image, fmt: str) -> bytes:
212
+ out = io.BytesIO()
213
+ save_img = img
214
+ if fmt == "JPEG" and img.mode in ("RGBA", "P", "LA"):
215
+ save_img = img.convert("RGB")
216
+ if fmt == "WEBP":
217
+ save_img.save(out, "WEBP", quality=88, method=6)
218
+ elif fmt == "JPEG":
219
+ save_img.save(out, "JPEG", quality=88, optimize=True)
220
+ elif fmt == "PNG":
221
+ save_img.save(out, "PNG", optimize=True)
222
+ else: # pragma: no cover
223
+ raise ValueError(f"formato de saída não suportado: {fmt}")
224
+ return out.getvalue()
@@ -0,0 +1,59 @@
1
+ """Leitura e escrita aditiva do frontmatter YAML de notas Markdown.
2
+
3
+ Contrato:
4
+ - O frontmatter, quando presente, começa em ``---\\n`` na primeira linha e
5
+ termina no próximo ``---\\n``.
6
+ - ``read(text)`` retorna ``(meta_dict, body)``. Sem frontmatter -> ``({}, text)``.
7
+ - ``write(meta, body)`` reemite o frontmatter sempre, com ``meta == {}``
8
+ o frontmatter é omitido.
9
+ - ``update(text, patch)`` aplica patch aditivamente sobre o frontmatter
10
+ existente sem mexer em chaves que não estão no patch e sem reordenar
11
+ agressivamente as chaves originais.
12
+
13
+ Não usa nenhum hack contra os campos de origem do export
14
+ (``chat_id``, ``url``, ``title``, ``exported_at``, ``model``, ``source``,
15
+ ``tags``); o caller é responsável por não passar essas chaves no patch.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Any
20
+
21
+ import yaml
22
+
23
+ _FENCE = "---"
24
+
25
+
26
+ def read(text: str) -> tuple[dict[str, Any], str]:
27
+ if not text.startswith(_FENCE + "\n") and not text.startswith(_FENCE + "\r\n"):
28
+ return {}, text
29
+ after_first = text.split("\n", 1)[1]
30
+ end_idx = after_first.find("\n" + _FENCE + "\n")
31
+ if end_idx == -1:
32
+ end_idx_crlf = after_first.find("\n" + _FENCE + "\r\n")
33
+ if end_idx_crlf == -1:
34
+ return {}, text
35
+ end_idx = end_idx_crlf
36
+ yaml_block = after_first[:end_idx]
37
+ rest = after_first[end_idx + len("\n" + _FENCE + "\n") :]
38
+ meta = yaml.safe_load(yaml_block) or {}
39
+ if not isinstance(meta, dict):
40
+ return {}, text
41
+ return meta, rest
42
+
43
+
44
+ def write(meta: dict[str, Any], body: str) -> str:
45
+ if not meta:
46
+ return body
47
+ yaml_block = yaml.safe_dump(
48
+ meta,
49
+ sort_keys=False,
50
+ allow_unicode=True,
51
+ default_flow_style=False,
52
+ ).rstrip("\n")
53
+ return f"{_FENCE}\n{yaml_block}\n{_FENCE}\n{body}"
54
+
55
+
56
+ def update(text: str, patch: dict[str, Any]) -> str:
57
+ meta, body = read(text)
58
+ merged = {**meta, **patch}
59
+ return write(merged, body)