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,264 @@
1
+ """Deterministic style fixes for Wiki_Medicina notes."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from pathlib import Path
6
+ from types import SimpleNamespace
7
+ from typing import Any
8
+
9
+ from mednotes.domains.wiki.capabilities.notes.note_style.frontmatter import (
10
+ FrontmatterYamlUnavailable,
11
+ normalize_wiki_frontmatter,
12
+ )
13
+ from mednotes.domains.wiki.capabilities.notes.note_style.models import STYLE_REPORT_SCHEMA, WIKI_INDEX_LINK
14
+ from mednotes.domains.wiki.capabilities.notes.note_style.tables import (
15
+ escape_wikilink_alias_pipes_in_tables,
16
+ normalize_markdown_tables,
17
+ )
18
+ from mednotes.domains.wiki.capabilities.notes.note_style.validate import index_style_report, validate_note_style
19
+ from mednotes.domains.wiki.capabilities.notes.provenance import (
20
+ ChatProvenance,
21
+ apply_note_provenance,
22
+ classify_note_provenance,
23
+ )
24
+ from mednotes.domains.wiki.capabilities.vocabulary.link_terms import is_index_note_content, is_index_target
25
+ from mednotes.kernel.base import JsonObject
26
+
27
+ _HEADING_EMOJI_RE = re.compile(r"^[\U0001F300-\U0001FAFF\u2600-\u27BF]")
28
+ _LOCAL_PATH_RE = re.compile(r"(?:[A-Za-z]:\\|/Users/|/home/|/var/|/tmp/)")
29
+ _MALFORMED_ALIAS_RE = re.compile(r"\[\[([^\]\|]+)\]\]([A-ZÁÉÍÓÚÇ]{2,12})\b")
30
+ _CALLOUT_START_RE = re.compile(r"^>\s*\[![A-Za-z]+]")
31
+
32
+ _HEADING_EMOJI_RULES: tuple[tuple[re.Pattern[str], str], ...] = (
33
+ (re.compile(r"quando\s+(pensar|suspeitar|usar)", re.I), "🎯"),
34
+ (re.compile(r"(ideia\s+central|fisiopatologia|mecanismo|etiologia|anatomia|fisiologia)", re.I), "🧠"),
35
+ (re.compile(r"(diagn[oó]stico|exames?|achados?|avalia[cç][aã]o)", re.I), "🔎"),
36
+ (re.compile(r"(conduta|tratamento|manejo|terap[eê]utica)", re.I), "🩺"),
37
+ (re.compile(r"(estratifica[cç][aã]o|classifica[cç][aã]o|risco|escore|componentes)", re.I), "⚖️"),
38
+ (re.compile(r"(pegadinhas?|armadilhas?|pontos?\s+de\s+prova)", re.I), "⚠️"),
39
+ (re.compile(r"fechamento", re.I), "🏁"),
40
+ (re.compile(r"notas?\s+relacionadas?", re.I), "🔗"),
41
+ )
42
+
43
+
44
+ def _optional_text(value: str | None) -> str:
45
+ return value.strip() if isinstance(value, str) else ""
46
+
47
+
48
+ def fix_note_style(
49
+ content: str,
50
+ *,
51
+ title: str,
52
+ raw_meta: dict[str, str] | None = None,
53
+ path: str | None = None,
54
+ ) -> tuple[str, dict[str, Any]]:
55
+ if _is_operational_index_note(content, title=title, path=path):
56
+ return content, index_style_report(content, title=title, path=path)
57
+ fixed = content.replace("\r\n", "\n").replace("\r", "\n")
58
+ fixes: list[str] = []
59
+
60
+ stripped_lines = [line.rstrip() for line in fixed.split("\n")]
61
+ stripped = "\n".join(stripped_lines)
62
+ if stripped != fixed:
63
+ fixed = stripped
64
+ fixes.append("trim_trailing_whitespace")
65
+
66
+ try:
67
+ frontmatter_fixed, frontmatter_fixes = normalize_wiki_frontmatter(fixed, title=title, preserve_keys={"chats"})
68
+ except FrontmatterYamlUnavailable as exc:
69
+ return fixed, _blocked_report(title=title, path=path, exc=exc, fixes=fixes)
70
+ if frontmatter_fixed != fixed:
71
+ fixed = frontmatter_fixed
72
+ fixes.extend(frontmatter_fixes or ["normalize_frontmatter"])
73
+
74
+ heading_fixed = _fix_heading_emojis(fixed)
75
+ if heading_fixed != fixed:
76
+ fixed = heading_fixed
77
+ fixes.append("add_known_heading_emojis")
78
+
79
+ alias_fixed = _fix_malformed_alias_links(fixed)
80
+ if alias_fixed != fixed:
81
+ fixed = alias_fixed
82
+ fixes.append("fix_wikilink_alias_suffixes")
83
+
84
+ table_link_fixed = escape_wikilink_alias_pipes_in_tables(fixed)
85
+ if table_link_fixed != fixed:
86
+ fixed = table_link_fixed
87
+ fixes.append("escape_wikilink_pipes_in_tables")
88
+
89
+ table_fixed = normalize_markdown_tables(fixed)
90
+ if table_fixed != fixed:
91
+ fixed = table_fixed
92
+ fixes.append("normalize_markdown_tables")
93
+
94
+ spacing_fixed = _normalize_blank_lines(fixed)
95
+ if spacing_fixed != fixed:
96
+ fixed = spacing_fixed
97
+ fixes.append("normalize_blank_lines")
98
+
99
+ footer_links_fixed = _remove_trailing_invalid_footer_links(fixed)
100
+ if footer_links_fixed != fixed:
101
+ fixed = footer_links_fixed
102
+ fixes.append("remove_invalid_footer_links")
103
+
104
+ try:
105
+ provenance_fixed = _fix_provenance(fixed, raw_meta or {}, title=title)
106
+ except FrontmatterYamlUnavailable as exc:
107
+ return fixed, _blocked_report(title=title, path=path, exc=exc, fixes=fixes)
108
+ if provenance_fixed != fixed:
109
+ fixed = provenance_fixed
110
+ fixes.append("normalize_provenance")
111
+
112
+ if not fixed.endswith("\n"):
113
+ fixed += "\n"
114
+ fixes.append("ensure_trailing_newline")
115
+
116
+ report = validate_note_style(
117
+ fixed,
118
+ title=title,
119
+ raw_meta=raw_meta,
120
+ path=path,
121
+ fixes_applied=fixes,
122
+ )
123
+ return fixed, report
124
+
125
+
126
+ def _is_operational_index_note(content: str, *, title: str, path: str | None = None) -> bool:
127
+ stem = Path(path).stem if path else title
128
+ return is_index_target(stem) or is_index_note_content(content)
129
+
130
+
131
+ def _blocked_report(
132
+ *,
133
+ title: str,
134
+ path: str | None,
135
+ exc: FrontmatterYamlUnavailable,
136
+ fixes: list[str],
137
+ ) -> JsonObject:
138
+ return {
139
+ "schema": STYLE_REPORT_SCHEMA,
140
+ "path": path,
141
+ "title": title,
142
+ "ok": False,
143
+ "errors": [{"code": exc.blocked_reason, "message": exc.next_action, "severity": "error"}],
144
+ "warnings": [],
145
+ "fixes_applied": fixes,
146
+ "requires_llm_rewrite": False,
147
+ "rewrite_prompt": None,
148
+ "frontmatter_present": False,
149
+ "status": "blocked",
150
+ "blocked_reason": exc.blocked_reason,
151
+ "next_action": exc.next_action,
152
+ }
153
+
154
+
155
+ def _fix_heading_emojis(text: str) -> str:
156
+ fixed_lines: list[str] = []
157
+ for line in text.splitlines():
158
+ match = re.match(r"^(##)\s+(.+?)\s*$", line)
159
+ if not match:
160
+ fixed_lines.append(line)
161
+ continue
162
+ heading = match.group(2).strip()
163
+ if _HEADING_EMOJI_RE.match(heading):
164
+ fixed_lines.append(line)
165
+ continue
166
+ emoji = _emoji_for_heading(heading)
167
+ fixed_lines.append(f"## {emoji} {heading}" if emoji else line)
168
+ return "\n".join(fixed_lines)
169
+
170
+
171
+ def _emoji_for_heading(heading: str) -> str:
172
+ for pattern, emoji in _HEADING_EMOJI_RULES:
173
+ if pattern.search(heading):
174
+ return emoji
175
+ return ""
176
+
177
+
178
+ def _fix_malformed_alias_links(text: str) -> str:
179
+ return _MALFORMED_ALIAS_RE.sub(r"[[\1|\2]]", text)
180
+
181
+
182
+ def _normalize_blank_lines(text: str) -> str:
183
+ text = re.sub(r"\n{3,}", "\n\n", text)
184
+ text = re.sub(r"\n+(## 🔗 Notas Relacionadas)", r"\n\n\1", text)
185
+ return _normalize_callout_spacing(text)
186
+
187
+
188
+ def _normalize_callout_spacing(text: str) -> str:
189
+ normalized: list[str] = []
190
+ for line in text.splitlines():
191
+ stripped = line.strip()
192
+ is_callout_start = bool(_CALLOUT_START_RE.match(stripped))
193
+ is_quote = stripped.startswith(">")
194
+ previous_is_quote = bool(normalized and normalized[-1].lstrip().startswith(">"))
195
+ if is_callout_start and normalized and normalized[-1].strip():
196
+ normalized.append("")
197
+ elif stripped and not is_quote and previous_is_quote:
198
+ normalized.append("")
199
+ normalized.append(line)
200
+ return "\n".join(normalized)
201
+
202
+
203
+ def _fix_provenance(text: str, raw_meta: dict[str, str], *, title: str) -> str:
204
+ state = classify_note_provenance(text)
205
+ chat = _chat_from_raw_meta(raw_meta)
206
+ if state.status == "already_canonical":
207
+ return text
208
+ if state.status != "migratable" and chat is None:
209
+ return text
210
+ chats = [chat] if chat is not None else []
211
+ result = apply_note_provenance(
212
+ text,
213
+ chats=chats,
214
+ chat_lookup=_RawMetaLookup(raw_meta, fallback_title=title),
215
+ )
216
+ return str(result["text"])
217
+
218
+
219
+ def _remove_trailing_invalid_footer_links(text: str) -> str:
220
+ lines = text.rstrip().splitlines()
221
+ changed = True
222
+ while changed and lines:
223
+ changed = False
224
+ tail_start = max(0, len(lines) - 8)
225
+ for idx in range(len(lines) - 1, tail_start - 1, -1):
226
+ stripped = lines[idx].strip()
227
+ if (
228
+ stripped == WIKI_INDEX_LINK
229
+ or "_Índice_Medicina" in stripped
230
+ or "Indice_Medicina" in stripped
231
+ or stripped.startswith("obsidian://")
232
+ or bool(_LOCAL_PATH_RE.search(stripped))
233
+ ):
234
+ lines.pop(idx)
235
+ changed = True
236
+ while lines and not lines[-1].strip():
237
+ lines.pop()
238
+ return "\n".join(lines).rstrip() + "\n"
239
+
240
+
241
+ def _chat_from_raw_meta(raw_meta: dict[str, str]) -> ChatProvenance | None:
242
+ fonte_id = _optional_text(raw_meta.get("fonte_id")).strip("/")
243
+ if not fonte_id:
244
+ return None
245
+ return ChatProvenance(
246
+ fonte_id,
247
+ date_created=_optional_text(raw_meta.get("date_created")),
248
+ date_exported=_optional_text(raw_meta.get("exported_at")) or _optional_text(raw_meta.get("date_exported")),
249
+ )
250
+
251
+
252
+ class _RawMetaLookup:
253
+ def __init__(self, raw_meta: dict[str, str], *, fallback_title: str) -> None:
254
+ self.raw_meta = raw_meta
255
+ self.fallback_title = fallback_title
256
+
257
+ def lookup_chat(self, chat_id: str) -> SimpleNamespace:
258
+ return SimpleNamespace(
259
+ id=chat_id,
260
+ title=str(self.raw_meta.get("titulo_triagem") or self.fallback_title or f"Chat {chat_id[:8]}"),
261
+ url=f"https://gemini.google.com/app/{chat_id}",
262
+ date_created=_optional_text(self.raw_meta.get("date_created")),
263
+ date_exported=_optional_text(self.raw_meta.get("exported_at")) or _optional_text(self.raw_meta.get("date_exported")),
264
+ )
@@ -0,0 +1,435 @@
1
+ """Frontmatter and provenance helpers for Wiki_Medicina notes."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ from copy import deepcopy
7
+ from dataclasses import dataclass
8
+ from functools import lru_cache
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ try: # Keep the CLI usable even in very small extension runtimes.
13
+ import yaml
14
+ except ImportError: # pragma: no cover - exercised only without project deps
15
+ yaml = None # type: ignore[assignment]
16
+
17
+
18
+ _FRONTMATTER_DELIM = "---"
19
+ _KEY_RE = re.compile(r"^([A-Za-z0-9_-]+)\s*:\s*(.*)$")
20
+
21
+ _ALIAS_KEYS = {
22
+ "alias",
23
+ "aliases",
24
+ "sinonimo",
25
+ "sinonimos",
26
+ "sinônimo",
27
+ "sinônimos",
28
+ "sigla",
29
+ "siglas",
30
+ "acronym",
31
+ "acronyms",
32
+ "termo",
33
+ "termos",
34
+ "term",
35
+ "terms",
36
+ }
37
+ _TAG_KEYS = {"tag", "tags"}
38
+ _ENRICHER_KEYS = {
39
+ "images_enriched",
40
+ "images_enriched_at",
41
+ "image_count",
42
+ "image_sources",
43
+ }
44
+ SOURCE_METADATA_KEYS = frozenset(
45
+ {
46
+ "source",
47
+ "sources",
48
+ "source_type",
49
+ "source_kind",
50
+ "source_url",
51
+ "source_urls",
52
+ "source_title",
53
+ "source_author",
54
+ "source_channel",
55
+ "source_platform",
56
+ "source_id",
57
+ "source_published_at",
58
+ "source_accessed_at",
59
+ "source_duration",
60
+ "source_notes",
61
+ }
62
+ )
63
+ OPERATIONAL_WIKI_TAGS = frozenset({"anki", "revisar", "indice", "índice"})
64
+ FRONTMATTER_YAML_BLOCKED_REASON = "frontmatter_yaml_unavailable"
65
+ FRONTMATTER_YAML_NEXT_ACTION = "Rodar /mednotes:setup para preparar o ambiente e repetir o workflow."
66
+
67
+
68
+ class FrontmatterYamlUnavailable(RuntimeError):
69
+ blocked_reason = FRONTMATTER_YAML_BLOCKED_REASON
70
+ next_action = FRONTMATTER_YAML_NEXT_ACTION
71
+
72
+ def __init__(self, message: str = "PyYAML is required to preserve structured Wiki frontmatter.") -> None:
73
+ super().__init__(message)
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class FrontmatterBlock:
78
+ key: str
79
+ lines: tuple[str, ...]
80
+
81
+
82
+ def split_frontmatter(text: str) -> tuple[str | None, str]:
83
+ lines = text.splitlines(keepends=True)
84
+ if not lines or lines[0].strip() != _FRONTMATTER_DELIM:
85
+ return None, text
86
+ for idx in range(1, len(lines)):
87
+ if lines[idx].strip() == _FRONTMATTER_DELIM:
88
+ return "".join(lines[1:idx]), "".join(lines[idx + 1 :])
89
+ return None, text
90
+
91
+
92
+ def parse_frontmatter(text: str) -> dict[str, str]:
93
+ frontmatter, _body = split_frontmatter(text)
94
+ if frontmatter is None:
95
+ return {}
96
+ parsed: dict[str, str] = {}
97
+ for line in frontmatter.splitlines():
98
+ match = _KEY_RE.match(line.strip())
99
+ if match:
100
+ parsed[match.group(1)] = _strip_quotes(match.group(2))
101
+ return parsed
102
+
103
+
104
+ def load_frontmatter_yaml(text: str) -> dict[str, object]:
105
+ frontmatter, _body = split_frontmatter(text)
106
+ if frontmatter is None:
107
+ return {}
108
+ return deepcopy(_load_frontmatter_yaml_cached(frontmatter))
109
+
110
+
111
+ @lru_cache(maxsize=4096)
112
+ def _load_frontmatter_yaml_cached(frontmatter: str) -> dict[str, object]:
113
+ if yaml is None:
114
+ raise FrontmatterYamlUnavailable
115
+ try:
116
+ loaded = yaml.safe_load(frontmatter) or {}
117
+ except Exception as exc:
118
+ raise FrontmatterYamlUnavailable(f"Could not parse Wiki frontmatter as YAML: {exc}") from exc
119
+ if not isinstance(loaded, dict):
120
+ return {}
121
+ return {str(key): value for key, value in loaded.items()}
122
+
123
+
124
+ def dump_frontmatter_yaml(data: dict[str, object]) -> str:
125
+ if yaml is None:
126
+ raise FrontmatterYamlUnavailable
127
+ class _IndentedSafeDumper(yaml.SafeDumper): # type: ignore[misc, valid-type]
128
+ def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:
129
+ super().increase_indent(flow, False)
130
+
131
+ return yaml.dump(
132
+ data,
133
+ Dumper=_IndentedSafeDumper,
134
+ allow_unicode=True,
135
+ default_flow_style=False,
136
+ sort_keys=False,
137
+ )
138
+
139
+
140
+ def _strip_quotes(value: str) -> str:
141
+ value = value.strip()
142
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
143
+ return value[1:-1]
144
+ return value
145
+
146
+
147
+ def raw_meta_from_file(raw_file: Path | None) -> dict[str, str]:
148
+ if raw_file is None:
149
+ return {}
150
+ return parse_frontmatter(raw_file.read_text(encoding="utf-8"))
151
+
152
+
153
+ def infer_title(content: str, path: Path) -> str:
154
+ _frontmatter, body = split_frontmatter(content)
155
+ match = re.search(r"(?m)^#\s+(.+?)\s*$", body)
156
+ return match.group(1).strip() if match else path.stem
157
+
158
+
159
+ def normalize_wiki_frontmatter(
160
+ text: str,
161
+ *,
162
+ title: str | None = None,
163
+ preserve_keys: set[str] | None = None,
164
+ ) -> tuple[str, list[str]]:
165
+ """Return text with the canonical Wiki_Medicina frontmatter shape.
166
+
167
+ Canonical Wiki notes use frontmatter only for exact aliases, Obsidian tags,
168
+ and additive metadata owned by downstream workflows. The enricher owns the
169
+ image metadata keys, so those blocks are preserved verbatim.
170
+ """
171
+
172
+ frontmatter, body = split_frontmatter(text)
173
+ if frontmatter is None:
174
+ return text, []
175
+
176
+ preserve_keys = {_normalize_key(key) for key in (preserve_keys or set())}
177
+ blocks = _frontmatter_blocks(frontmatter)
178
+ try:
179
+ data = load_frontmatter_yaml(text)
180
+ except FrontmatterYamlUnavailable:
181
+ if yaml is None or any(_normalize_key(block.key) in preserve_keys for block in blocks):
182
+ raise
183
+ data = _load_frontmatter_map(frontmatter)
184
+ aliases = _canonical_aliases(_extract_aliases_from_map(data), title=title)
185
+ raw_tags = _extract_tags_from_map(data)
186
+ tags = _canonical_tags(raw_tags)
187
+ preserved = _preserved_frontmatter_items(data, blocks, preserve_keys=preserve_keys)
188
+ canonical = _format_canonical_frontmatter(aliases, tags, preserved)
189
+ normalized_body = body.lstrip("\n")
190
+ normalized = normalized_body if not canonical else f"{_FRONTMATTER_DELIM}\n{canonical}{_FRONTMATTER_DELIM}\n{normalized_body}"
191
+ if normalized == text:
192
+ return text, []
193
+ fixes: list[str] = []
194
+ if aliases:
195
+ fixes.append("normalize_frontmatter_aliases")
196
+ if raw_tags:
197
+ fixes.append("normalize_frontmatter_tags")
198
+ if raw_tags and tags != _canonical_raw_tag_keys(raw_tags):
199
+ fixes.append("remove_nonoperational_frontmatter_tags")
200
+ if preserved:
201
+ fixes.append("preserve_enricher_frontmatter")
202
+ removed_keys = _removed_frontmatter_keys(blocks, preserve_keys=preserve_keys)
203
+ if removed_keys:
204
+ fixes.append("remove_noncanonical_frontmatter_keys")
205
+ if not canonical:
206
+ fixes.append("remove_empty_frontmatter")
207
+ return normalized, fixes
208
+
209
+
210
+ def wiki_frontmatter_aliases(text: str) -> list[str]:
211
+ frontmatter, _body = split_frontmatter(text)
212
+ if frontmatter is None:
213
+ return []
214
+ return _canonical_aliases(_extract_aliases(frontmatter), title=None)
215
+
216
+
217
+ def _frontmatter_blocks(frontmatter: str) -> list[FrontmatterBlock]:
218
+ lines = frontmatter.splitlines(keepends=True)
219
+ blocks: list[FrontmatterBlock] = []
220
+ idx = 0
221
+ while idx < len(lines):
222
+ match = _top_level_key_match(lines[idx])
223
+ if not match:
224
+ idx += 1
225
+ continue
226
+ start = idx
227
+ idx += 1
228
+ while idx < len(lines) and not _top_level_key_match(lines[idx]):
229
+ idx += 1
230
+ blocks.append(FrontmatterBlock(match.group(1), tuple(lines[start:idx])))
231
+ return blocks
232
+
233
+
234
+ def _load_frontmatter_map(frontmatter: str) -> dict[str, Any]:
235
+ if yaml is not None:
236
+ try:
237
+ loaded = yaml.safe_load(frontmatter) or {}
238
+ except Exception:
239
+ loaded = None
240
+ if isinstance(loaded, dict):
241
+ return {str(key): value for key, value in loaded.items()}
242
+
243
+ parsed: dict[str, Any] = {}
244
+ for block in _frontmatter_blocks(frontmatter):
245
+ key, raw = _parse_block_header(block)
246
+ if raw.startswith("[") and raw.endswith("]"):
247
+ parsed[key] = [_strip_quotes(item) for item in raw[1:-1].split(",") if item.strip()]
248
+ elif raw:
249
+ parsed[key] = _strip_quotes(raw)
250
+ else:
251
+ values = []
252
+ for line in block.lines[1:]:
253
+ match = re.match(r"^\s*-\s*(.+?)\s*$", line)
254
+ if match:
255
+ values.append(_strip_quotes(match.group(1)))
256
+ parsed[key] = values
257
+ return parsed
258
+
259
+
260
+ def _extract_aliases(frontmatter: str) -> list[str]:
261
+ data = _load_frontmatter_map(frontmatter)
262
+ return _extract_aliases_from_map(data)
263
+
264
+
265
+ def _extract_aliases_from_map(data: dict[str, Any]) -> list[str]:
266
+ aliases: list[str] = []
267
+ for key, value in data.items():
268
+ if _normalize_key(key) in _ALIAS_KEYS:
269
+ aliases.extend(_coerce_string_list(value))
270
+ return aliases
271
+
272
+
273
+ def _extract_tags(frontmatter: str) -> list[str]:
274
+ data = _load_frontmatter_map(frontmatter)
275
+ return _extract_tags_from_map(data)
276
+
277
+
278
+ def _extract_tags_from_map(data: dict[str, Any]) -> list[str]:
279
+ tags: list[str] = []
280
+ for key, value in data.items():
281
+ if _normalize_key(key) in _TAG_KEYS:
282
+ tags.extend(_coerce_string_list(value))
283
+ return tags
284
+
285
+
286
+ def _coerce_string_list(value: Any) -> list[str]:
287
+ if value is None:
288
+ return []
289
+ if isinstance(value, str):
290
+ return [value]
291
+ if isinstance(value, (list, tuple)):
292
+ return [str(item) for item in value if item is not None]
293
+ return [str(value)]
294
+
295
+
296
+ def _canonical_aliases(values: list[str], *, title: str | None) -> list[str]:
297
+ aliases: list[str] = []
298
+ seen: set[str] = set()
299
+ title_key = _normalize_alias(title or "") if title else ""
300
+ for value in values:
301
+ alias = re.sub(r"\s+", " ", _strip_quotes(str(value))).strip()
302
+ if not alias:
303
+ continue
304
+ key = _normalize_alias(alias)
305
+ if not key or key == title_key or key in seen:
306
+ continue
307
+ seen.add(key)
308
+ aliases.append(alias)
309
+ return aliases
310
+
311
+
312
+ def _canonical_tags(values: list[str]) -> list[str]:
313
+ tags: list[str] = []
314
+ seen: set[str] = set()
315
+ for value in values:
316
+ tag = _canonical_tag_key(value)
317
+ if not tag or tag not in OPERATIONAL_WIKI_TAGS:
318
+ continue
319
+ if tag in seen:
320
+ continue
321
+ seen.add(tag)
322
+ tags.append(tag)
323
+ return tags
324
+
325
+
326
+ def canonical_wiki_tags(values: list[str]) -> list[str]:
327
+ return _canonical_tags(values)
328
+
329
+
330
+ def _canonical_raw_tag_keys(values: list[str]) -> list[str]:
331
+ tags: list[str] = []
332
+ seen: set[str] = set()
333
+ for value in values:
334
+ tag = _canonical_tag_key(value)
335
+ if not tag or tag in seen:
336
+ continue
337
+ seen.add(tag)
338
+ tags.append(tag)
339
+ return tags
340
+
341
+
342
+ def _canonical_tag_key(value: object) -> str:
343
+ return re.sub(r"\s+", " ", _strip_quotes(str(value)).lstrip("#")).strip().casefold()
344
+
345
+
346
+ def _preserved_frontmatter_items(
347
+ data: dict[str, Any],
348
+ blocks: list[FrontmatterBlock],
349
+ *,
350
+ preserve_keys: set[str],
351
+ ) -> list[tuple[str, Any]]:
352
+ items: list[tuple[str, Any]] = []
353
+ seen: set[str] = set()
354
+ allowed = {*_ENRICHER_KEYS, *preserve_keys}
355
+ for block in blocks:
356
+ key = _normalize_key(block.key)
357
+ if key in seen or not _is_preserved_frontmatter_key(key, allowed):
358
+ continue
359
+ for original_key, value in data.items():
360
+ if _normalize_key(str(original_key)) == key:
361
+ items.append((str(original_key), value))
362
+ seen.add(key)
363
+ break
364
+ return items
365
+
366
+
367
+ def _format_canonical_frontmatter(aliases: list[str], tags: list[str], preserved: list[tuple[str, Any]]) -> str:
368
+ lines: list[str] = []
369
+ if aliases:
370
+ lines.append("aliases:\n")
371
+ lines.extend(f" - {_format_yaml_string(alias)}\n" for alias in aliases)
372
+ if tags:
373
+ lines.append("tags:\n")
374
+ lines.extend(f" - {_format_yaml_tag(tag)}\n" for tag in tags)
375
+ for key, value in preserved:
376
+ if _normalize_key(key) in _ALIAS_KEYS or _normalize_key(key) in _TAG_KEYS:
377
+ continue
378
+ lines.extend(_dump_frontmatter_item(key, value))
379
+ return "".join(lines)
380
+
381
+
382
+ def _format_yaml_string(value: str) -> str:
383
+ return json.dumps(value, ensure_ascii=False)
384
+
385
+
386
+ def _format_yaml_tag(value: str) -> str:
387
+ if re.match(r"^[A-Za-z0-9_/-]+$", value):
388
+ return value
389
+ return _format_yaml_string(value)
390
+
391
+
392
+ def _ensure_block_newlines(lines: tuple[str, ...]) -> list[str]:
393
+ fixed: list[str] = []
394
+ for line in lines:
395
+ fixed.append(line if line.endswith("\n") else f"{line}\n")
396
+ return fixed
397
+
398
+
399
+ def _dump_frontmatter_item(key: str, value: Any) -> list[str]:
400
+ dumped = dump_frontmatter_yaml({key: value})
401
+ return _ensure_block_newlines(tuple(dumped.splitlines(keepends=True)))
402
+
403
+
404
+ def _removed_frontmatter_keys(blocks: list[FrontmatterBlock], *, preserve_keys: set[str] | None = None) -> list[str]:
405
+ preserve_keys = preserve_keys or set()
406
+ removed: list[str] = []
407
+ for block in blocks:
408
+ key = _normalize_key(block.key)
409
+ if key not in _ALIAS_KEYS and key not in _TAG_KEYS and not _is_preserved_frontmatter_key(key, preserve_keys):
410
+ removed.append(block.key)
411
+ return removed
412
+
413
+
414
+ def _is_preserved_frontmatter_key(key: str, preserve_keys: set[str]) -> bool:
415
+ return key in _ENRICHER_KEYS or key in SOURCE_METADATA_KEYS or key in preserve_keys or key.startswith("images_")
416
+
417
+
418
+ def _parse_block_header(block: FrontmatterBlock) -> tuple[str, str]:
419
+ first = block.lines[0].strip()
420
+ match = _KEY_RE.match(first)
421
+ if not match:
422
+ return block.key, ""
423
+ return match.group(1), match.group(2).strip()
424
+
425
+
426
+ def _top_level_key_match(line: str) -> re.Match[str] | None:
427
+ return _KEY_RE.match(line.rstrip("\r\n"))
428
+
429
+
430
+ def _normalize_key(value: str) -> str:
431
+ return value.strip().lower()
432
+
433
+
434
+ def _normalize_alias(value: str) -> str:
435
+ return re.sub(r"\s+", " ", value).strip().casefold()