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,331 @@
1
+ """Plan and apply DB-to-YAML alias projections."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ import sqlite3
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+
10
+ from pydantic import Field, StrictBool, StrictStr
11
+
12
+ from mednotes.domains.wiki.batch_state import file_sha256
13
+ from mednotes.domains.wiki.capabilities.notes.raw_chats import atomic_write_text
14
+ from mednotes.domains.wiki.capabilities.vocabulary.link_terms import normalize_key
15
+ from mednotes.domains.wiki.capabilities.vocabulary.vocabulary_map import ProjectionAlias, VocabularyMap
16
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
17
+
18
+ ALIAS_PROJECTION_PLAN_SCHEMA = "medical-notes-workbench.alias-projection-plan.v1"
19
+ ALIAS_PROJECTION_RECEIPT_SCHEMA = "medical-notes-workbench.alias-projection-receipt.v1"
20
+
21
+
22
+ class _AliasProjectionOperationPayload(ContractModel):
23
+ operation_id: StrictStr = ""
24
+ note_path: StrictStr = Field(min_length=1)
25
+ before_hash: StrictStr = ""
26
+ after_aliases: list[StrictStr] = Field(default_factory=list)
27
+ preserved_contextual_aliases: list[StrictStr] = Field(default_factory=list)
28
+ blocked_aliases: list[StrictStr] = Field(default_factory=list)
29
+ requires_backup: StrictBool = False
30
+ backup: StrictBool = False
31
+ changed: StrictBool = False
32
+
33
+
34
+ class _AliasProjectionReceiptPayload(ContractModel):
35
+ status: StrictStr
36
+ blocked_reason: StrictStr = ""
37
+ note_path: StrictStr = ""
38
+ before_hash: StrictStr = ""
39
+ after_hash: StrictStr = ""
40
+ backup_path: StrictStr = ""
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class AliasProjectionReceipt:
45
+ status: str
46
+ blocked_reason: str = ""
47
+ note_path: str = ""
48
+ before_hash: str = ""
49
+ after_hash: str = ""
50
+ backup_path: str = ""
51
+
52
+ def as_dict(self) -> JsonObject:
53
+ return _AliasProjectionReceiptPayload(
54
+ status=self.status,
55
+ blocked_reason=self.blocked_reason,
56
+ note_path=self.note_path,
57
+ before_hash=self.before_hash,
58
+ after_hash=self.after_hash,
59
+ backup_path=self.backup_path,
60
+ ).to_payload()
61
+
62
+
63
+ @dataclass
64
+ class AliasProjectionPlan:
65
+ note_path: Path
66
+ before_hash: str
67
+ after_aliases: list[str]
68
+ preserved_contextual_aliases: list[str]
69
+ changed: bool
70
+ backup: bool = False
71
+ requires_backup: bool = False
72
+ blocked_aliases: list[str] = field(default_factory=list)
73
+
74
+ def as_operation(self) -> JsonObject:
75
+ return _AliasProjectionOperationPayload(
76
+ operation_id=f"alias_projection.update:{self.note_path}",
77
+ note_path=str(self.note_path),
78
+ before_hash=self.before_hash,
79
+ after_aliases=self.after_aliases,
80
+ preserved_contextual_aliases=self.preserved_contextual_aliases,
81
+ blocked_aliases=self.blocked_aliases,
82
+ requires_backup=self.requires_backup,
83
+ backup=False,
84
+ changed=self.changed,
85
+ ).to_payload()
86
+
87
+ def apply(self) -> AliasProjectionReceipt:
88
+ current_hash = _file_hash(self.note_path)
89
+ if current_hash != self.before_hash:
90
+ return AliasProjectionReceipt(
91
+ status="blocked",
92
+ blocked_reason="alias_projection.stale_note_hash",
93
+ note_path=str(self.note_path),
94
+ before_hash=self.before_hash,
95
+ )
96
+ original = self.note_path.read_text(encoding="utf-8")
97
+ updated = replace_aliases_in_frontmatter(original, self.after_aliases)
98
+ if updated != original:
99
+ atomic_write_text(self.note_path, updated)
100
+ return AliasProjectionReceipt(
101
+ status="applied",
102
+ note_path=str(self.note_path),
103
+ before_hash=self.before_hash,
104
+ after_hash=_file_hash(self.note_path),
105
+ backup_path="",
106
+ )
107
+
108
+
109
+ def plan_alias_projection(vocab: VocabularyMap, *, note_path: Path, backup: bool = True) -> AliasProjectionPlan:
110
+ candidates = vocab.note_aliases.get(str(note_path), [])
111
+ selected = _dedupe_aliases(candidates)
112
+ note_text = note_path.read_text(encoding="utf-8")
113
+ title_key = normalize_key(_h1_title(note_text) or note_path.stem)
114
+ after_aliases = [
115
+ item.text
116
+ for item in selected
117
+ if item.visible_in_yaml and item.link_policy in {"direct", "requires_context"}
118
+ and item.normalized_surface != title_key
119
+ ]
120
+ contextual = [
121
+ item.text
122
+ for item in selected
123
+ if item.link_policy == "requires_context" and item.visible_in_yaml and item.normalized_surface != title_key
124
+ ]
125
+ blocked = [item.text for item in selected if item.visible_in_yaml and item.link_policy in {"blocked", "no_link"}]
126
+ current = _current_aliases(note_text)
127
+ return AliasProjectionPlan(
128
+ note_path=note_path,
129
+ before_hash=_file_hash(note_path),
130
+ after_aliases=after_aliases,
131
+ preserved_contextual_aliases=contextual,
132
+ changed=current != after_aliases,
133
+ backup=False,
134
+ requires_backup=False,
135
+ blocked_aliases=blocked,
136
+ )
137
+
138
+
139
+ def build_alias_projection_plan(vocab: VocabularyMap, *, backup: bool = True) -> JsonObject:
140
+ operations: list[JsonObject] = []
141
+ for raw_path in sorted(vocab.note_aliases):
142
+ note_path = Path(raw_path)
143
+ if not note_path.is_file():
144
+ continue
145
+ operations.append(plan_alias_projection(vocab, note_path=note_path, backup=backup).as_operation())
146
+ changed_count = sum(1 for item in operations if item.get("changed"))
147
+ return {
148
+ "schema": ALIAS_PROJECTION_PLAN_SCHEMA,
149
+ "status": "planned",
150
+ "db_path": str(vocab.db_path) if vocab.db_path else "",
151
+ "operation_count": len(operations),
152
+ "changed_count": changed_count,
153
+ "operations": operations,
154
+ }
155
+
156
+
157
+ def apply_alias_projection_plan(plan: JsonObject) -> JsonObject:
158
+ if plan.get("schema") != ALIAS_PROJECTION_PLAN_SCHEMA:
159
+ return {
160
+ "schema": ALIAS_PROJECTION_RECEIPT_SCHEMA,
161
+ "status": "blocked",
162
+ "blocked_reason": "alias_projection.invalid_plan_schema",
163
+ "applied_count": 0,
164
+ "blocked_count": 0,
165
+ "receipts": [],
166
+ }
167
+ receipts: list[JsonObject] = []
168
+ db_path = Path(str(plan.get("db_path") or ""))
169
+ operations = plan.get("operations", [])
170
+ if not isinstance(operations, list):
171
+ operations = []
172
+ for operation in operations:
173
+ if not isinstance(operation, dict):
174
+ continue
175
+ projection_plan = _plan_from_operation(operation)
176
+ receipt = _AliasProjectionReceiptPayload.model_validate(projection_plan.apply().as_dict())
177
+ if (
178
+ receipt.status == "applied"
179
+ and receipt.before_hash != receipt.after_hash
180
+ and db_path.is_file()
181
+ ):
182
+ _sync_note_hash(db_path, projection_plan.note_path, receipt.after_hash)
183
+ receipts.append(receipt.to_payload())
184
+ typed_receipts = [_AliasProjectionReceiptPayload.model_validate(item) for item in receipts]
185
+ blocked_count = sum(1 for item in typed_receipts if item.status == "blocked")
186
+ applied_count = sum(
187
+ 1
188
+ for item in typed_receipts
189
+ if item.status == "applied" and item.before_hash != item.after_hash
190
+ )
191
+ return {
192
+ "schema": ALIAS_PROJECTION_RECEIPT_SCHEMA,
193
+ "status": "blocked" if blocked_count else "completed",
194
+ "blocked_reason": "alias_projection.blocked_operations" if blocked_count else "",
195
+ "operation_count": len(receipts),
196
+ "applied_count": applied_count,
197
+ "blocked_count": blocked_count,
198
+ "receipts": receipts,
199
+ }
200
+
201
+
202
+ def _plan_from_operation(operation: JsonObject) -> AliasProjectionPlan:
203
+ typed = _AliasProjectionOperationPayload.model_validate(JsonObjectAdapter.validate_python(operation))
204
+ return AliasProjectionPlan(
205
+ note_path=Path(typed.note_path),
206
+ before_hash=typed.before_hash,
207
+ after_aliases=list(typed.after_aliases),
208
+ preserved_contextual_aliases=list(typed.preserved_contextual_aliases),
209
+ changed=typed.changed,
210
+ backup=False,
211
+ requires_backup=False,
212
+ blocked_aliases=list(typed.blocked_aliases),
213
+ )
214
+
215
+
216
+ def _sync_note_hash(db_path: Path, note_path: Path, content_hash: str) -> None:
217
+ if not content_hash:
218
+ return
219
+ with sqlite3.connect(db_path) as conn:
220
+ conn.execute(
221
+ """
222
+ UPDATE notes
223
+ SET content_hash = ?, updated_at = CURRENT_TIMESTAMP
224
+ WHERE path = ? AND status = 'active'
225
+ """,
226
+ (content_hash, str(note_path)),
227
+ )
228
+
229
+
230
+ def replace_aliases_in_frontmatter(text: str, aliases: list[str]) -> str:
231
+ frontmatter, body = _split_frontmatter(text)
232
+ if frontmatter is None:
233
+ alias_block = _format_aliases(aliases)
234
+ return f"---\n{alias_block}---\n{body.lstrip(chr(10))}" if alias_block else body
235
+ lines = frontmatter.splitlines(keepends=True)
236
+ out: list[str] = []
237
+ idx = 0
238
+ replaced = False
239
+ while idx < len(lines):
240
+ key = _key_for(lines[idx])
241
+ if key == "aliases":
242
+ if aliases:
243
+ out.append(_format_aliases(aliases))
244
+ replaced = True
245
+ idx += 1
246
+ while idx < len(lines) and _key_for(lines[idx]) is None:
247
+ idx += 1
248
+ continue
249
+ out.append(lines[idx])
250
+ idx += 1
251
+ if aliases and not replaced:
252
+ out.insert(0, _format_aliases(aliases))
253
+ rendered = "".join(out)
254
+ if not rendered.strip():
255
+ return body.lstrip("\n")
256
+ return f"---\n{rendered}---\n{body.lstrip(chr(10))}"
257
+
258
+
259
+ def _dedupe_aliases(candidates: list[ProjectionAlias]) -> list[ProjectionAlias]:
260
+ best: dict[str, ProjectionAlias] = {}
261
+ for candidate in candidates:
262
+ key = candidate.normalized_surface
263
+ existing = best.get(key)
264
+ if existing is None or _alias_score(candidate) > _alias_score(existing):
265
+ best[key] = candidate
266
+ return sorted(best.values(), key=lambda item: (_is_acronym(item.text), item.order, item.text.casefold()))
267
+
268
+
269
+ def _alias_score(candidate: ProjectionAlias) -> tuple[int, int, int, int]:
270
+ policy_score = {"direct": 3, "requires_context": 2, "no_link": 1, "blocked": 0}.get(candidate.link_policy, 0)
271
+ return (policy_score, *_display_score(candidate.text))
272
+
273
+
274
+ def _display_score(value: str) -> tuple[int, int, int]:
275
+ return (
276
+ int(any(ord(char) > 127 for char in value)),
277
+ int(_is_acronym(value)),
278
+ sum(1 for char in value if char.isupper()),
279
+ )
280
+
281
+
282
+ def _is_acronym(value: str) -> bool:
283
+ return value.isupper() and len(value) <= 8
284
+
285
+
286
+ def _file_hash(path: Path) -> str:
287
+ return "sha256:" + file_sha256(path)
288
+
289
+
290
+ def _h1_title(text: str) -> str:
291
+ match = re.search(r"(?m)^#\s+(.+?)\s*$", text)
292
+ return match.group(1).strip() if match else ""
293
+
294
+
295
+ def _split_frontmatter(text: str) -> tuple[str | None, str]:
296
+ lines = text.splitlines(keepends=True)
297
+ if not lines or lines[0].strip() != "---":
298
+ return None, text
299
+ for idx in range(1, len(lines)):
300
+ if lines[idx].strip() == "---":
301
+ return "".join(lines[1:idx]), "".join(lines[idx + 1 :])
302
+ return None, text
303
+
304
+
305
+ def _key_for(line: str) -> str | None:
306
+ match = re.match(r"^([A-Za-z0-9_-]+)\s*:", line)
307
+ return match.group(1) if match else None
308
+
309
+
310
+ def _format_aliases(aliases: list[str]) -> str:
311
+ if not aliases:
312
+ return ""
313
+ return "aliases:\n" + "".join(f" - {json.dumps(alias, ensure_ascii=False)}\n" for alias in aliases)
314
+
315
+
316
+ def _current_aliases(text: str) -> list[str]:
317
+ frontmatter, _body = _split_frontmatter(text)
318
+ if not frontmatter:
319
+ return []
320
+ inline = re.search(r"(?m)^aliases\s*:\s*\[(.*?)\]\s*$", frontmatter)
321
+ if inline:
322
+ return [item.strip().strip("'\"") for item in inline.group(1).split(",") if item.strip()]
323
+ block = re.search(r"(?m)^aliases\s*:\s*\n((?:\s*-\s*.*(?:\n|$))+)", frontmatter)
324
+ if not block:
325
+ return []
326
+ aliases: list[str] = []
327
+ for line in block.group(1).splitlines():
328
+ match = re.match(r"^\s*-\s*(.*?)\s*$", line)
329
+ if match:
330
+ aliases.append(match.group(1).strip().strip("'\""))
331
+ return aliases
@@ -0,0 +1,151 @@
1
+ """Shared term, alias, and catalog helpers for Wiki graph/linker code."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import re
6
+ import unicodedata
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL | re.MULTILINE)
11
+ CATALOG_CONTAINER_KEYS = ("entities", "entidades", "notes", "notas", "items", "catalog", "catalogo")
12
+ TARGET_KEYS = ("target", "target_file", "arquivo", "file", "filename", "nota", "note", "path", "caminho")
13
+ ALIAS_KEYS = ("aliases", "alias", "sinonimos", "sinônimos", "synonyms", "siglas", "acronyms", "termos", "terms")
14
+ TITLE_KEYS = ("titulo", "title", "nome", "name")
15
+ INDEX_TARGET_KEYS = {"_indice_medicina"}
16
+ INDEX_TAG_KEYS = {"indice"}
17
+
18
+
19
+ def normalize_key(value: str) -> str:
20
+ value = unicodedata.normalize("NFKD", value)
21
+ value = "".join(char for char in value if not unicodedata.combining(char))
22
+ return re.sub(r"\s+", " ", value).strip().casefold()
23
+
24
+
25
+ def obsidian_target_name(value: str) -> str:
26
+ target = str(value or "").strip().replace("\\", "/")
27
+ if not target:
28
+ return ""
29
+ name = target.rsplit("/", 1)[-1].strip()
30
+ return name[:-3] if name.casefold().endswith(".md") else name
31
+
32
+
33
+ def is_index_target(value: str) -> bool:
34
+ return normalize_key(obsidian_target_name(value)) in INDEX_TARGET_KEYS
35
+
36
+
37
+ def extract_tags(content: str) -> list[str]:
38
+ tags: list[str] = []
39
+ match = FRONTMATTER_RE.search(content)
40
+ if not match:
41
+ return tags
42
+ yaml_block = match.group(1)
43
+
44
+ list_match = re.search(r"tags:\s*\[(.*?)\]", yaml_block, re.IGNORECASE)
45
+ if list_match:
46
+ tags.extend(clean_yaml_scalar(item).lstrip("#") for item in list_match.group(1).split(",") if item.strip())
47
+
48
+ multi_line_match = re.search(r"tags:\s*\n((?:\s*-\s*.*(?:\n|$))+)", yaml_block, re.IGNORECASE)
49
+ if multi_line_match:
50
+ for line in multi_line_match.group(1).strip().split("\n"):
51
+ item = re.sub(r"^\s*-\s*", "", line).strip()
52
+ if item:
53
+ tags.append(clean_yaml_scalar(item).lstrip("#"))
54
+
55
+ return [tag for tag in tags if tag]
56
+
57
+
58
+ def is_index_note_content(content: str) -> bool:
59
+ return any(normalize_key(tag) in INDEX_TAG_KEYS for tag in extract_tags(content))
60
+
61
+
62
+ def is_index_note(path: Path, content: str) -> bool:
63
+ return is_index_target(path.stem) or is_index_note_content(content)
64
+
65
+
66
+ def expand_path(value: str | os.PathLike[str]) -> Path:
67
+ return Path(os.path.expandvars(str(value))).expanduser()
68
+
69
+
70
+ def clean_yaml_scalar(value: str) -> str:
71
+ return value.strip().strip("'\"").strip()
72
+
73
+
74
+ def extract_aliases(content: str) -> list[str]:
75
+ aliases: list[str] = []
76
+ match = FRONTMATTER_RE.search(content)
77
+ if not match:
78
+ return aliases
79
+ yaml_block = match.group(1)
80
+
81
+ list_match = re.search(r"aliases:\s*\[(.*?)\]", yaml_block, re.IGNORECASE)
82
+ if list_match:
83
+ aliases.extend(clean_yaml_scalar(item) for item in list_match.group(1).split(",") if item.strip())
84
+
85
+ multi_line_match = re.search(r"aliases:\s*\n((?:\s*-\s*.*(?:\n|$))+)", yaml_block, re.IGNORECASE)
86
+ if multi_line_match:
87
+ for line in multi_line_match.group(1).strip().split("\n"):
88
+ item = re.sub(r"^\s*-\s*", "", line).strip()
89
+ if item:
90
+ aliases.append(clean_yaml_scalar(item))
91
+
92
+ return [alias for alias in aliases if alias]
93
+
94
+
95
+ def as_list(value: Any) -> list[Any]:
96
+ if value is None:
97
+ return []
98
+ if isinstance(value, list):
99
+ return value
100
+ if isinstance(value, tuple):
101
+ return list(value)
102
+ return [value]
103
+
104
+
105
+ def string_values(value: Any) -> list[str]:
106
+ return [item.strip() for item in as_list(value) if isinstance(item, str) and item.strip()]
107
+
108
+
109
+ def catalog_entries(data: Any) -> list[tuple[str, dict[str, Any]]]:
110
+ if isinstance(data, list):
111
+ return [("", item) for item in data if isinstance(item, dict)]
112
+ if not isinstance(data, dict):
113
+ return []
114
+
115
+ for key in CATALOG_CONTAINER_KEYS:
116
+ value = data.get(key)
117
+ if isinstance(value, list):
118
+ return [("", item) for item in value if isinstance(item, dict)]
119
+ if isinstance(value, dict):
120
+ return [(str(k), item) for k, item in value.items() if isinstance(item, dict)]
121
+
122
+ return [(str(key), value) for key, value in data.items() if isinstance(value, dict)]
123
+
124
+
125
+ def target_from_entry(entry: dict[str, Any], fallback_key: str = "") -> str | None:
126
+ for key in TARGET_KEYS:
127
+ value = entry.get(key)
128
+ if isinstance(value, str) and value.strip():
129
+ return obsidian_target_name(value.strip())
130
+ if fallback_key:
131
+ return obsidian_target_name(fallback_key)
132
+ for key in TITLE_KEYS:
133
+ value = entry.get(key)
134
+ if isinstance(value, str) and value.strip():
135
+ return value.strip()
136
+ return None
137
+
138
+
139
+ def aliases_from_entry(entry: dict[str, Any]) -> list[str]:
140
+ aliases: list[str] = []
141
+ for key in ALIAS_KEYS:
142
+ aliases.extend(string_values(entry.get(key)))
143
+ return aliases
144
+
145
+
146
+ def terms_from_entry(entry: dict[str, Any], target: str) -> list[str]:
147
+ terms = [target]
148
+ terms.extend(aliases_from_entry(entry))
149
+ for key in TITLE_KEYS:
150
+ terms.extend(string_values(entry.get(key)))
151
+ return terms
@@ -0,0 +1,182 @@
1
+ """Gemini CLI seam for contextual body-link disambiguation."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import tempfile
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from pydantic import ConfigDict, Field
13
+
14
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
15
+
16
+ CONTEXTUAL_ALIAS_SCHEMA = "medical-notes-workbench.contextual-alias-disambiguation.v1"
17
+ _WINDOWS_PROMPT_FILE_THRESHOLD = 6000
18
+
19
+
20
+ class LinkDisambiguationError(RuntimeError):
21
+ """Raised when contextual alias disambiguation cannot get valid JSON."""
22
+
23
+
24
+ class LinkDisambiguationRequiresOrchestrator(LinkDisambiguationError):
25
+ """Raised when direct Gemini CLI disambiguation is disabled for UX safety."""
26
+
27
+
28
+ class _ContextualAliasResponseFields(ContractModel):
29
+ """Typed response envelope required from contextual alias disambiguation."""
30
+
31
+ model_config = ConfigDict(extra="ignore", populate_by_name=True, validate_assignment=True)
32
+
33
+ schema_id: str = Field(alias="schema")
34
+ decisions: list[JsonObject]
35
+
36
+
37
+ def call_contextual_alias_disambiguator(
38
+ requests: list[dict[str, Any]],
39
+ *,
40
+ model: str,
41
+ timeout_seconds: int,
42
+ ) -> dict[str, Any]:
43
+ if not _direct_gemini_disambiguation_allowed():
44
+ raise LinkDisambiguationRequiresOrchestrator(
45
+ "desambiguação contextual por gemini -p direta está desativada; use orquestração por agente"
46
+ )
47
+ prompt = _build_prompt(requests)
48
+ raw = _call_gemini(prompt, model=model, timeout_seconds=timeout_seconds)
49
+ return _parse_response(raw)
50
+
51
+
52
+ def _direct_gemini_disambiguation_allowed() -> bool:
53
+ return os.getenv("MEDNOTES_ALLOW_INTERNAL_GEMINI_DISAMBIGUATION", "").strip().lower() in {"1", "true", "yes"}
54
+
55
+
56
+ def _build_prompt(requests: list[dict[str, Any]]) -> str:
57
+ payload = {
58
+ "schema": "medical-notes-workbench.contextual-alias-disambiguation-request.v1",
59
+ "instructions": [
60
+ "Você decide links de termos médicos ambíguos no corpo de notas Obsidian.",
61
+ "Escolha somente um dos candidatos fornecidos em cada ocorrência.",
62
+ "Nunca invente alvo, nota, meaning_id ou alias.",
63
+ "Se o contexto for insuficiente, responda action=defer.",
64
+ "Se o termo claramente não deve ser linkado, responda action=no_link.",
65
+ "Use confidence entre 0 e 1.",
66
+ f"Responda apenas JSON com schema {CONTEXTUAL_ALIAS_SCHEMA}.",
67
+ ],
68
+ "requests": requests,
69
+ "response_shape": {
70
+ "schema": CONTEXTUAL_ALIAS_SCHEMA,
71
+ "decisions": [
72
+ {
73
+ "occurrence_id": "copie da requisição",
74
+ "action": "link|no_link|defer",
75
+ "chosen_meaning_id": "meaning id do candidato escolhido, se action=link",
76
+ "chosen_target": "target do candidato escolhido, se action=link",
77
+ "confidence": 0.0,
78
+ "reason_code": "curto_sem_espacos",
79
+ "rationale_summary": "resumo redigido, sem copiar trecho clínico bruto",
80
+ }
81
+ ],
82
+ },
83
+ }
84
+ return json.dumps(payload, ensure_ascii=False, indent=2)
85
+
86
+
87
+ def _call_gemini(prompt: str, *, model: str, timeout_seconds: int) -> str:
88
+ binary = os.getenv("MEDNOTES_GEMINI_BINARY", "gemini")
89
+ cmd = [_resolve_gemini_binary(binary), "--skip-trust", "-m", model]
90
+ if _prompt_needs_file(prompt):
91
+ return _invoke_with_prompt_file(cmd, prompt, timeout_seconds=timeout_seconds)
92
+ return _invoke([*cmd, "-p", prompt], timeout_seconds=timeout_seconds)
93
+
94
+
95
+ def _resolve_gemini_binary(binary: str) -> str:
96
+ expanded = os.path.expandvars(os.path.expanduser(binary))
97
+ if _is_pathish(expanded):
98
+ return expanded
99
+ found = shutil.which(expanded)
100
+ if found:
101
+ return found
102
+ if expanded.lower() in {"gemini", "gemini.cmd"}:
103
+ for candidate in _npm_gemini_candidates():
104
+ if candidate.is_file():
105
+ return str(candidate)
106
+ return expanded
107
+
108
+
109
+ def _is_pathish(value: str) -> bool:
110
+ return "/" in value or "\\" in value or (len(value) >= 2 and value[1] == ":")
111
+
112
+
113
+ def _npm_gemini_candidates() -> list[Path]:
114
+ candidates: list[Path] = []
115
+ appdata = os.environ.get("APPDATA")
116
+ if appdata:
117
+ candidates.append(Path(appdata) / "npm" / "gemini.cmd")
118
+ prefix = os.environ.get("NPM_CONFIG_PREFIX")
119
+ if prefix:
120
+ prefix_path = Path(prefix)
121
+ candidates.extend([prefix_path / "gemini.cmd", prefix_path / "bin" / "gemini"])
122
+ return candidates
123
+
124
+
125
+ def _prompt_needs_file(prompt: str) -> bool:
126
+ return os.name == "nt" and len(prompt) >= _WINDOWS_PROMPT_FILE_THRESHOLD
127
+
128
+
129
+ def _invoke_with_prompt_file(cmd: list[str], prompt: str, *, timeout_seconds: int) -> str:
130
+ with tempfile.TemporaryDirectory(prefix="mednotes-link-disambiguation-") as tmp:
131
+ prompt_path = Path(tmp) / "prompt.md"
132
+ prompt_path.write_text(prompt, encoding="utf-8")
133
+ return _invoke([*cmd, "--include-directories", str(prompt_path.parent), "-p", f"@{prompt_path}"], timeout_seconds=timeout_seconds)
134
+
135
+
136
+ def _invoke(cmd: list[str], *, timeout_seconds: int) -> str:
137
+ try:
138
+ proc = subprocess.run(
139
+ _subprocess_command(cmd),
140
+ capture_output=True,
141
+ text=True,
142
+ check=False,
143
+ timeout=timeout_seconds,
144
+ )
145
+ except subprocess.TimeoutExpired as exc:
146
+ raise LinkDisambiguationError(f"gemini CLI excedeu timeout de {timeout_seconds}s") from exc
147
+ except FileNotFoundError as exc:
148
+ raise LinkDisambiguationError("gemini CLI não encontrado para desambiguação contextual") from exc
149
+ except OSError as exc:
150
+ raise LinkDisambiguationError(f"gemini CLI não pôde ser iniciado: {exc}") from exc
151
+ if proc.returncode != 0:
152
+ raise LinkDisambiguationError(f"gemini CLI falhou (rc={proc.returncode}): {proc.stderr.strip()}")
153
+ return proc.stdout
154
+
155
+
156
+ def _subprocess_command(cmd: list[str]) -> list[str]:
157
+ if not cmd:
158
+ return cmd
159
+ suffix = Path(cmd[0]).suffix.lower()
160
+ if os.name == "nt" and suffix in {".cmd", ".bat"}:
161
+ return [os.environ.get("COMSPEC") or "cmd.exe", "/d", "/s", "/c", *cmd]
162
+ return cmd
163
+
164
+
165
+ def _parse_response(raw: str) -> JsonObject:
166
+ stripped = raw.strip()
167
+ if stripped.startswith("```"):
168
+ lines = stripped.splitlines()
169
+ if lines and lines[0].startswith("```"):
170
+ lines = lines[1:]
171
+ if lines and lines[-1].startswith("```"):
172
+ lines = lines[:-1]
173
+ stripped = "\n".join(lines).strip()
174
+ try:
175
+ payload = json.loads(stripped)
176
+ except json.JSONDecodeError as exc:
177
+ raise LinkDisambiguationError(f"resposta Gemini não é JSON válido: {exc}") from exc
178
+ payload = JsonObjectAdapter.validate_python(payload)
179
+ fields = _ContextualAliasResponseFields.model_validate(payload)
180
+ if fields.schema_id != CONTEXTUAL_ALIAS_SCHEMA:
181
+ raise LinkDisambiguationError(f"resposta Gemini precisa usar schema {CONTEXTUAL_ALIAS_SCHEMA}")
182
+ return payload