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,111 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import Field, model_validator
6
+
7
+ from mednotes.kernel.base import ContractModel
8
+ from mednotes.kernel.workflow import HumanDecisionPacket
9
+
10
+
11
+ class PublishManifestNote(ContractModel):
12
+ taxonomy: str = Field(min_length=1)
13
+ title: str = Field(min_length=1)
14
+ content_path: str = Field(min_length=1)
15
+ safe_filename: str | None = None
16
+
17
+
18
+ class PublishManifestBatch(ContractModel):
19
+ raw_file: str = Field(min_length=1)
20
+ coverage_path: str | None = None
21
+ note_plan_path: str | None = None
22
+ raw_files: list[str] = Field(default_factory=list)
23
+ notes: list[PublishManifestNote] = Field(min_length=1)
24
+ batch_id: str | None = None
25
+ run_id: str | None = None
26
+ note_plan_hash: str | None = None
27
+ coverage_hash: str | None = None
28
+ source_artifact_hash: str | None = None
29
+ human_decision_required: bool | None = None
30
+ human_decision_packet: HumanDecisionPacket | None = None
31
+ human_decision_packets: list[HumanDecisionPacket] = Field(default_factory=list)
32
+
33
+ @model_validator(mode="after")
34
+ def pending_human_decision_is_explicit(self) -> PublishManifestBatch:
35
+ if self.human_decision_required is True:
36
+ return self
37
+ if self.human_decision_packet is not None and self.human_decision_packet.status == "pending":
38
+ object.__setattr__(self, "human_decision_required", True)
39
+ if any(packet.status == "pending" for packet in self.human_decision_packets):
40
+ object.__setattr__(self, "human_decision_required", True)
41
+ return self
42
+
43
+
44
+ class PublishManifest(ContractModel):
45
+ schema_id: str | None = Field(default=None, alias="schema")
46
+ human_decision_required: bool | None = None
47
+ human_decision_packet: HumanDecisionPacket | None = None
48
+ human_decision_packets: list[HumanDecisionPacket] = Field(default_factory=list)
49
+ batches: list[PublishManifestBatch] = Field(min_length=1)
50
+
51
+ @model_validator(mode="after")
52
+ def manifest_must_have_notes(self) -> PublishManifest:
53
+ if not self.batches:
54
+ raise ValueError("publish manifest requires canonical batches[]")
55
+ if self.human_decision_packet is not None and self.human_decision_packet.status == "pending":
56
+ object.__setattr__(self, "human_decision_required", True)
57
+ if any(packet.status == "pending" for packet in self.human_decision_packets):
58
+ object.__setattr__(self, "human_decision_required", True)
59
+ return self
60
+
61
+ def require_coverage(self) -> None:
62
+ missing = [batch.raw_file for batch in self.batches if not batch.coverage_path]
63
+ if missing:
64
+ raise ValueError(
65
+ "publish manifest batches require coverage_path when require_coverage=True: "
66
+ + ", ".join(missing)
67
+ )
68
+
69
+ def pending_human_decision(self) -> bool:
70
+ if self.human_decision_required is True:
71
+ return True
72
+ if self.human_decision_packet is not None and self.human_decision_packet.status == "pending":
73
+ return True
74
+ if any(packet.status == "pending" for packet in self.human_decision_packets):
75
+ return True
76
+ return any(batch.human_decision_required is True for batch in self.batches)
77
+
78
+
79
+ class PublishReceiptItem(ContractModel):
80
+ path: str = Field(min_length=1)
81
+ status: Literal["published", "skipped", "blocked"]
82
+ reason: str | None = None
83
+
84
+
85
+ class PublishErrorContext(ContractModel):
86
+ root_cause: str = Field(min_length=1)
87
+ affected_artifact: str = Field(min_length=1)
88
+ error_summary: str = Field(min_length=1)
89
+ suggested_fix: str = Field(min_length=1)
90
+ next_action: str = Field(min_length=1)
91
+ retry_scope: str = Field(min_length=1)
92
+ phase: str | None = None
93
+ blocked_reason: str | None = None
94
+ details: dict[str, object] | None = None
95
+
96
+
97
+ class PublishReceipt(ContractModel):
98
+ schema_id: Literal["medical-notes-workbench.publish-receipt.v1"] = Field(alias="schema")
99
+ status: Literal["ready_to_publish", "published", "completed_with_link_blockers", "blocked"]
100
+ batch_id: str = Field(min_length=1)
101
+ published_count: int = Field(ge=0)
102
+ skipped_count: int = Field(ge=0)
103
+ items: list[PublishReceiptItem]
104
+ next_action: str = ""
105
+ error_context: PublishErrorContext | None = None
106
+
107
+ @model_validator(mode="after")
108
+ def blocked_requires_error_context(self) -> PublishReceipt:
109
+ if self.status == "blocked" and self.error_context is None:
110
+ raise ValueError("blocked publish receipts require error_context")
111
+ return self
@@ -0,0 +1,217 @@
1
+ """Typed raw-coverage contracts for process-chats publish boundaries.
2
+
3
+ The raw coverage file is authored outside the publish adapter, so JSON is
4
+ allowed at the file boundary only. Once loaded, coverage decisions and publish
5
+ counts must read these Pydantic models instead of loose dictionaries.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from collections import Counter
10
+ from collections.abc import Sequence
11
+ from typing import Literal
12
+
13
+ from pydantic import ConfigDict, Field, StrictBool, StrictStr, model_validator
14
+
15
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
16
+
17
+ RAW_COVERAGE_SCHEMA = "medical-notes-workbench.raw-coverage.v1"
18
+ CoverageAction = Literal["planned_meaning", "not_a_note"]
19
+ RawCoverageSourceStatus = Literal["covered", "already_covered", "not_relevant"]
20
+
21
+
22
+ class RawCoverageItem(ContractModel):
23
+ """One exhaustive coverage decision for material in a raw chat."""
24
+
25
+ id: StrictStr = Field(min_length=1)
26
+ action: CoverageAction
27
+ title: StrictStr | None = None
28
+ staged_title: StrictStr | None = None
29
+ meaning_claim: JsonObject | None = None
30
+ reason_code: StrictStr | None = None
31
+ reason: StrictStr | None = None
32
+ status: StrictStr | None = None
33
+ detail: StrictStr | None = None
34
+ coverage: JsonObject | None = None
35
+
36
+ @model_validator(mode="after")
37
+ def validate_coverage_payload(self) -> RawCoverageItem:
38
+ if self.action == "planned_meaning":
39
+ if not (self.title or self.staged_title):
40
+ raise ValueError("planned_meaning coverage items require title or staged_title")
41
+ elif self.action == "not_a_note" and not self.reason:
42
+ raise ValueError("not_a_note coverage items require reason")
43
+ return self
44
+
45
+ @property
46
+ def planned_title(self) -> str:
47
+ return (self.staged_title or self.title or "").strip()
48
+
49
+
50
+ class RawCoverageSource(ContractModel):
51
+ """Per-raw provenance statement for multi-source process-chats coverage."""
52
+
53
+ raw_file: StrictStr = Field(min_length=1)
54
+ status: RawCoverageSourceStatus
55
+ target_title: StrictStr = ""
56
+ target_section: StrictStr = ""
57
+ new_information_summary: StrictStr = ""
58
+ reference_added: StrictStr = ""
59
+ reason: StrictStr = ""
60
+ existing_title: StrictStr = ""
61
+
62
+ @model_validator(mode="after")
63
+ def validate_status_payload(self) -> RawCoverageSource:
64
+ if self.status == "covered":
65
+ missing = [
66
+ field
67
+ for field in (
68
+ "target_title",
69
+ "target_section",
70
+ "new_information_summary",
71
+ "reference_added",
72
+ )
73
+ if not getattr(self, field).strip()
74
+ ]
75
+ if missing:
76
+ raise ValueError(f"covered source requires {', '.join(missing)}")
77
+ elif self.status in {"already_covered", "not_relevant"} and not self.reason.strip():
78
+ raise ValueError(f"{self.status} source requires reason")
79
+ return self
80
+
81
+ def compact_payload(self) -> JsonObject:
82
+ payload = self.model_dump(mode="json", exclude_defaults=True)
83
+ return JsonObjectAdapter.validate_python(payload)
84
+
85
+
86
+ class RawCoverage(ContractModel):
87
+ """Canonical raw-coverage inventory consumed by publish and validation."""
88
+
89
+ schema_id: Literal["medical-notes-workbench.raw-coverage.v1"] = Field(alias="schema")
90
+ raw_file: StrictStr = Field(min_length=1)
91
+ exhaustive: Literal[True]
92
+ items: list[RawCoverageItem] = Field(min_length=1)
93
+ raw_files: list[StrictStr] = Field(default_factory=list)
94
+ sources: list[RawCoverageSource] = Field(default_factory=list)
95
+ batch_id: StrictStr | None = None
96
+ run_id: StrictStr | None = None
97
+ note_plan_hash: StrictStr | None = None
98
+ coverage_hash: StrictStr | None = None
99
+ source_artifact_hash: StrictStr | None = None
100
+
101
+
102
+ class RawCoverageSummary(ContractModel):
103
+ """Typed summary projected from a validated raw coverage inventory."""
104
+
105
+ schema_id: Literal["medical-notes-workbench.raw-coverage.v1"] = Field(
106
+ default=RAW_COVERAGE_SCHEMA,
107
+ alias="schema",
108
+ )
109
+ status: Literal["", "valid"] = ""
110
+ coverage_path: StrictStr = ""
111
+ coverage_hash: StrictStr = ""
112
+ raw_file: StrictStr = ""
113
+ raw_files: list[StrictStr] = Field(default_factory=list)
114
+ multi_source: StrictBool = False
115
+ source_count: int = Field(default=0, ge=0, strict=True)
116
+ exhaustive: StrictBool = False
117
+ item_count: int = Field(default=0, ge=0, strict=True)
118
+ planned_meaning_count: int = Field(default=0, ge=0, strict=True)
119
+ not_a_note_count: int = Field(default=0, ge=0, strict=True)
120
+ staged_note_count: int = Field(default=0, ge=0, strict=True)
121
+ raw_file_count: int = Field(default=0, ge=0, strict=True)
122
+ covered_count: int = Field(default=0, ge=0, strict=True)
123
+ sources: list[RawCoverageSource] = Field(default_factory=list)
124
+ source_status_counts: dict[str, int] = Field(default_factory=dict)
125
+ coverage_hashes: list[StrictStr] = Field(default_factory=list)
126
+ batch_id: StrictStr = ""
127
+ run_id: StrictStr = ""
128
+ note_plan_hash: StrictStr = ""
129
+ source_artifact_hash: StrictStr = ""
130
+ note_plan_source_count: int = Field(default=0, ge=0, strict=True)
131
+ note_plan_hashes: dict[str, str] = Field(default_factory=dict)
132
+ note_plan_item_count: int = Field(default=0, ge=0, strict=True)
133
+ note_plan_planned_meaning_count: int = Field(default=0, ge=0, strict=True)
134
+ note_plan_attach_count: int = Field(default=0, ge=0, strict=True)
135
+ note_plan_not_a_note_count: int = Field(default=0, ge=0, strict=True)
136
+ note_plan_needs_context_count: int = Field(default=0, ge=0, strict=True)
137
+
138
+ def to_payload(self) -> JsonObject:
139
+ payload = self.model_dump(mode="json", by_alias=True)
140
+ if self.sources:
141
+ payload["sources"] = [source.compact_payload() for source in self.sources]
142
+ else:
143
+ payload.pop("sources", None)
144
+ payload = {key: value for key, value in payload.items() if value not in ("", [], {})}
145
+ for key in (
146
+ "multi_source",
147
+ "source_count",
148
+ "exhaustive",
149
+ "item_count",
150
+ "planned_meaning_count",
151
+ "not_a_note_count",
152
+ "staged_note_count",
153
+ "raw_file_count",
154
+ "covered_count",
155
+ ):
156
+ payload[key] = getattr(self, key)
157
+ return JsonObjectAdapter.validate_python(payload)
158
+
159
+
160
+ class RawCoveragePlanBatch(ContractModel):
161
+ """Typed lens over publish's planned-batch projection."""
162
+
163
+ model_config = ConfigDict(extra="ignore", populate_by_name=True, validate_assignment=True)
164
+
165
+ raw_file: StrictStr = ""
166
+ raw_files: list[StrictStr] = Field(default_factory=list)
167
+ coverage: RawCoverageSummary | None = None
168
+
169
+
170
+ def coverage_summary_from_batches(plan: Sequence[RawCoveragePlanBatch]) -> RawCoverageSummary:
171
+ """Aggregate validated planned batches without reading raw dictionaries."""
172
+
173
+ raw_files: list[str] = []
174
+ seen_raw_files: set[str] = set()
175
+ source_status_counts: Counter[str] = Counter()
176
+ coverage_hashes: list[str] = []
177
+ item_count = 0
178
+ planned_meaning_count = 0
179
+ not_a_note_count = 0
180
+ staged_note_count = 0
181
+ source_count = 0
182
+
183
+ for batch in plan:
184
+ batch_raw_files = batch.raw_files or ([batch.raw_file] if batch.raw_file else [])
185
+ for raw_file in batch_raw_files:
186
+ raw_file_text = raw_file.strip()
187
+ if raw_file_text and raw_file_text not in seen_raw_files:
188
+ seen_raw_files.add(raw_file_text)
189
+ raw_files.append(raw_file_text)
190
+ if batch.coverage is None:
191
+ continue
192
+ coverage = batch.coverage
193
+ if coverage.coverage_hash:
194
+ coverage_hashes.append(coverage.coverage_hash)
195
+ item_count += coverage.item_count
196
+ planned_meaning_count += coverage.planned_meaning_count
197
+ not_a_note_count += coverage.not_a_note_count
198
+ staged_note_count += coverage.staged_note_count
199
+ source_count += coverage.source_count
200
+ source_status_counts.update(coverage.source_status_counts)
201
+
202
+ return RawCoverageSummary(
203
+ status="valid" if plan else "",
204
+ raw_files=raw_files,
205
+ multi_source=len(raw_files) > 1,
206
+ source_count=source_count or len(raw_files),
207
+ exhaustive=bool(plan),
208
+ item_count=item_count,
209
+ planned_meaning_count=planned_meaning_count,
210
+ not_a_note_count=not_a_note_count,
211
+ staged_note_count=staged_note_count,
212
+ raw_file_count=len(raw_files),
213
+ covered_count=len(raw_files),
214
+ source_status_counts=dict(source_status_counts),
215
+ coverage_hash=coverage_hashes[0] if len(coverage_hashes) == 1 else "",
216
+ coverage_hashes=coverage_hashes,
217
+ )
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Literal
5
+
6
+ from pydantic import ConfigDict, Field, field_validator
7
+
8
+ from mednotes.kernel.base import ContractModel
9
+
10
+ RelatedNotesEmbeddingProfileId = Literal["clean_v1", "raw_v1", "legacy_v0"]
11
+ RelatedNotesRepresentationHashBasis = Literal["profile_cleaned_markdown", "raw_markdown", "legacy_hybrid_markdown"]
12
+
13
+ _SHA256_PATTERN = r"^sha256:[0-9a-f]{64}$"
14
+
15
+
16
+ def _validate_relative_posix_path(value: str) -> str:
17
+ text = value.strip()
18
+ if (
19
+ not text or text.startswith(("/", "../")) or "\\" in text or "//" in text or any(part in {"", ".", ".."} for part in text.split("/"))
20
+ ):
21
+ raise ValueError("path must be a wiki-relative POSIX path")
22
+ return text
23
+
24
+
25
+ class RelatedNotesExportPlugin(ContractModel):
26
+ name: Literal["related-notes-obsidian"]
27
+ version: str = Field(min_length=1)
28
+
29
+
30
+ class RelatedNotesExportModel(ContractModel):
31
+ provider: str | None = None
32
+ embedding_model: str = Field(min_length=1)
33
+ dimension: int | None = Field(default=None, gt=0)
34
+ embedding_profile_id: RelatedNotesEmbeddingProfileId
35
+ embedding_profile_version: Literal[1]
36
+ representation_hash_basis: RelatedNotesRepresentationHashBasis
37
+
38
+
39
+ class RelatedNotesExportNote(ContractModel):
40
+ path: str = Field(min_length=1)
41
+ title: str = Field(min_length=1)
42
+ content_hash: str = Field(pattern=_SHA256_PATTERN)
43
+
44
+ @field_validator("path")
45
+ @classmethod
46
+ def path_must_be_relative_posix(cls, value: str) -> str:
47
+ return _validate_relative_posix_path(value)
48
+
49
+
50
+ class RelatedNotesExportEdge(ContractModel):
51
+ source_path: str = Field(min_length=1)
52
+ target_path: str = Field(min_length=1)
53
+ score: float = Field(ge=0.0, le=1.0)
54
+ rank: int = Field(ge=1)
55
+ source: Literal["related-notes-obsidian"] = "related-notes-obsidian"
56
+
57
+ @field_validator("source_path", "target_path")
58
+ @classmethod
59
+ def path_must_be_relative_posix(cls, value: str) -> str:
60
+ return _validate_relative_posix_path(value)
61
+
62
+
63
+ class RelatedNotesExport(ContractModel):
64
+ schema_: Literal["medical-notes-workbench.related-notes-export.v1"] = Field(
65
+ alias="schema",
66
+ serialization_alias="schema",
67
+ )
68
+ generated_at: datetime
69
+ vault_root: str = Field(min_length=1)
70
+ plugin: RelatedNotesExportPlugin
71
+ model_info: RelatedNotesExportModel = Field(alias="model", serialization_alias="model")
72
+ score_scale: Literal["0_to_1"]
73
+ notes: list[RelatedNotesExportNote]
74
+ edges: list[RelatedNotesExportEdge]
75
+
76
+ @field_validator("generated_at")
77
+ @classmethod
78
+ def generated_at_must_be_timezone_aware(cls, value: datetime) -> datetime:
79
+ if value.tzinfo is None or value.utcoffset() is None:
80
+ raise ValueError("generated_at must include timezone")
81
+ return value
82
+
83
+
84
+ class RelatedNotesHashMigrationModel(ContractModel):
85
+ """Minimal model slice needed to decide legacy hash migration."""
86
+
87
+ model_config = ConfigDict(extra="ignore", strict=True)
88
+
89
+ embedding_profile_id: RelatedNotesEmbeddingProfileId
90
+
91
+
92
+ class RelatedNotesHashMigrationExport(ContractModel):
93
+ """Typed migration boundary for historical plugin exports.
94
+
95
+ Older plugin exports may lack metadata required by the full public export
96
+ contract. Hash migration only needs the schema, profile and per-note hashes,
97
+ so this closed decision lens avoids raw dict reads without rejecting valid
98
+ legacy migration inputs.
99
+ """
100
+
101
+ model_config = ConfigDict(extra="ignore", strict=True)
102
+
103
+ schema_: Literal["medical-notes-workbench.related-notes-export.v1"] = Field(
104
+ alias="schema",
105
+ serialization_alias="schema",
106
+ )
107
+ model_info: RelatedNotesHashMigrationModel = Field(alias="model", serialization_alias="model")
108
+ notes: list[RelatedNotesExportNote]
109
+
110
+
111
+ class RelatedNotesHeadlessExportSummary(ContractModel):
112
+ schema_: str | None = Field(default=None, alias="schema", serialization_alias="schema")
113
+ status: str = ""
114
+ phase: str = ""
115
+ blocked_reason: str = ""
116
+ detail: str = ""
117
+ export_path: str = ""
118
+ index_path: str = ""
119
+ wiki_dir: str = ""
120
+ note_count: int = Field(default=0, ge=0)
121
+ edge_count: int = Field(default=0, ge=0)
122
+ record_count: int = Field(default=0, ge=0)
123
+ total_note_count: int = Field(default=0, ge=0)
124
+ fresh_record_count: int = Field(default=0, ge=0)
125
+ stale_record_count: int = Field(default=0, ge=0)
126
+ remaining_count: int = Field(default=0, ge=0)
127
+ partial_record_count: int = Field(default=0, ge=0)
128
+ embedded_count: int = Field(default=0, ge=0)
129
+ reused_count: int = Field(default=0, ge=0)
130
+ embedding_model: str = ""
131
+ embedding_profile_id: str = ""
132
+ embedding_request_delay_ms: int = Field(default=0, ge=0)
133
+ embedding_transient_retry_count: int = Field(default=0, ge=0)
134
+ related_notes_limit: int = Field(default=0, ge=0)
135
+ next_retry_after_seconds: int | None = Field(default=None, ge=0)
136
+ resume_supported: bool = False
@@ -0,0 +1,153 @@
1
+ """Typed boundaries for the headless Related Notes adapter.
2
+
3
+ The headless adapter reads Obsidian plugin JSON, a vector index and Gemini HTTP
4
+ responses. Those payloads are external data; workflow decisions must use these
5
+ models before checking status, currentness, vectors, counts or retry details.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Literal
11
+
12
+ from pydantic import ConfigDict, Field, StrictFloat, StrictInt, StrictStr
13
+
14
+ from mednotes.kernel.base import ContractModel, JsonObject
15
+
16
+ RELATED_NOTES_HASH_MIGRATION_CACHE_SCHEMA = "medical-notes-workbench.related-notes-hash-migration-cache.v1"
17
+
18
+
19
+ class RelatedNotesHashMigrationCache(ContractModel):
20
+ """Persisted cache entry for the clean_v1 hash migration shortcut."""
21
+
22
+ schema_: Literal["medical-notes-workbench.related-notes-hash-migration-cache.v1"] = Field(
23
+ alias="schema",
24
+ serialization_alias="schema",
25
+ )
26
+ identity: JsonObject
27
+ status: Literal[
28
+ "no_legacy_clean_v1_hashes",
29
+ "migrated_clean_v1_hashes",
30
+ "legacy_clean_v1_vectors_missing",
31
+ ]
32
+ migrated_note_count: int = Field(default=0, ge=0)
33
+ skipped_note_count: int = Field(default=0, ge=0)
34
+
35
+
36
+ class RelatedNotesHeadlessSettings(ContractModel):
37
+ """Strict subset of plugin settings that drives embedding/relation policy."""
38
+
39
+ model_config = ConfigDict(extra="ignore", strict=True)
40
+
41
+ gemini_api_key: str = Field(default="", alias="geminiApiKey", serialization_alias="geminiApiKey")
42
+ default_embedding_profile: str = Field(
43
+ default="",
44
+ alias="defaultEmbeddingProfile",
45
+ serialization_alias="defaultEmbeddingProfile",
46
+ )
47
+ related_notes_limit: int | None = Field(
48
+ default=None,
49
+ alias="relatedNotesLimit",
50
+ serialization_alias="relatedNotesLimit",
51
+ )
52
+ embedding_request_delay_ms: int | float | None = Field(
53
+ default=None,
54
+ alias="embeddingRequestDelayMs",
55
+ serialization_alias="embeddingRequestDelayMs",
56
+ )
57
+
58
+
59
+ class RelatedNotesVectorRecord(ContractModel):
60
+ """One plugin vector-index record after validation."""
61
+
62
+ model_config = ConfigDict(extra="forbid", strict=True)
63
+
64
+ path: StrictStr = ""
65
+ title: StrictStr = ""
66
+ folder: StrictStr = ""
67
+ preview: StrictStr = ""
68
+ raw_content_hash: StrictStr = Field(default="", alias="rawContentHash", serialization_alias="rawContentHash")
69
+ representation_hash: StrictStr = Field(
70
+ default="",
71
+ alias="representationHash",
72
+ serialization_alias="representationHash",
73
+ )
74
+ content_hash: StrictStr = Field(default="", alias="contentHash", serialization_alias="contentHash")
75
+ mtime: StrictInt = 0
76
+ embedding_model: StrictStr = Field(default="", alias="embeddingModel", serialization_alias="embeddingModel")
77
+ embedding_profile: StrictStr = Field(default="", alias="embeddingProfile", serialization_alias="embeddingProfile")
78
+ embedding_profile_version: StrictInt = Field(
79
+ default=0,
80
+ alias="embeddingProfileVersion",
81
+ serialization_alias="embeddingProfileVersion",
82
+ )
83
+ vector: list[StrictFloat] = Field(default_factory=list, min_length=1)
84
+ updated_at: StrictInt = Field(default=0, alias="updatedAt", serialization_alias="updatedAt")
85
+
86
+
87
+ class RelatedNotesVectorProfile(ContractModel):
88
+ """A profile bucket from the plugin vector index."""
89
+
90
+ model_config = ConfigDict(extra="ignore", strict=True)
91
+
92
+ records: dict[str, RelatedNotesVectorRecord] = Field(default_factory=dict)
93
+
94
+
95
+ class RelatedNotesVectorIndex(ContractModel):
96
+ """Validated vector index with typed records per profile."""
97
+
98
+ model_config = ConfigDict(extra="ignore", strict=True)
99
+
100
+ profiles: dict[str, RelatedNotesVectorProfile] = Field(default_factory=dict)
101
+
102
+ def records_for_profile(self, profile_id: str) -> dict[str, RelatedNotesVectorRecord]:
103
+ profile = self.profiles[profile_id] if profile_id in self.profiles else RelatedNotesVectorProfile()
104
+ return dict(profile.records)
105
+
106
+ def other_profiles_payload(self, profile_id: str) -> JsonObject:
107
+ return {
108
+ key: value.to_payload()
109
+ for key, value in self.profiles.items()
110
+ if key != profile_id
111
+ }
112
+
113
+
114
+ class GeminiEmbedding(ContractModel):
115
+ """Gemini embedding vector payload."""
116
+
117
+ model_config = ConfigDict(extra="ignore")
118
+
119
+ values: list[float]
120
+
121
+
122
+ class GeminiEmbeddingResponse(ContractModel):
123
+ """Single Gemini embedding response."""
124
+
125
+ model_config = ConfigDict(extra="ignore")
126
+
127
+ embedding: GeminiEmbedding
128
+
129
+
130
+ class GeminiBatchEmbeddingResponse(ContractModel):
131
+ """Batch Gemini embedding response."""
132
+
133
+ model_config = ConfigDict(extra="ignore")
134
+
135
+ embeddings: list[GeminiEmbedding]
136
+
137
+
138
+ class GeminiError(ContractModel):
139
+ """Error object returned by Gemini HTTP APIs."""
140
+
141
+ model_config = ConfigDict(extra="ignore")
142
+
143
+ code: int | str = ""
144
+ status: str = ""
145
+ message: str = ""
146
+
147
+
148
+ class GeminiErrorResponse(ContractModel):
149
+ """Typed Gemini error wrapper used only for redacted diagnostics."""
150
+
151
+ model_config = ConfigDict(extra="ignore")
152
+
153
+ error: GeminiError | None = None