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,638 @@
1
+ """Git-derived change context for the Wiki linker.
2
+
3
+ This module never emits raw diffs or note body snippets. It turns Git metadata
4
+ into the same trigger-context shape that workflow-aware callers already use.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import subprocess
10
+ from collections.abc import Mapping
11
+ from hashlib import sha256
12
+ from pathlib import Path
13
+
14
+ from pydantic import ValidationError as PydanticValidationError
15
+
16
+ from mednotes.domains.wiki.batch_state import canonical_json_hash
17
+ from mednotes.domains.wiki.capabilities.notes.raw_chats import atomic_write_text
18
+ from mednotes.domains.wiki.common import _now_iso
19
+ from mednotes.domains.wiki.contracts.link_git import (
20
+ GIT_TRIGGER_SOURCE,
21
+ LINK_GIT_CONTEXT_SCHEMA,
22
+ LINK_STATE_SCHEMA,
23
+ LINK_STATE_SCHEMA_V2,
24
+ LinkGitChangedPath,
25
+ LinkGitChangeEvent,
26
+ LinkGitContext,
27
+ LinkState,
28
+ LinkTriggerContextFromGit,
29
+ )
30
+ from mednotes.kernel.base import JsonObject
31
+ from mednotes.platform.paths import user_state_dir
32
+
33
+ __all__ = [
34
+ "GIT_TRIGGER_SOURCE",
35
+ "LINK_GIT_CONTEXT_SCHEMA",
36
+ "LINK_STATE_SCHEMA",
37
+ "LINK_STATE_SCHEMA_V2",
38
+ "LinkGitChangedPath",
39
+ "LinkGitChangeEvent",
40
+ "LinkGitContext",
41
+ "LinkState",
42
+ "LinkTriggerContextFromGit",
43
+ "collect_git_context",
44
+ "default_link_state_path",
45
+ "load_link_state",
46
+ "trigger_context_from_git",
47
+ "write_link_state",
48
+ ]
49
+
50
+
51
+ def default_link_state_path() -> Path:
52
+ return user_state_dir() / "link-state.json"
53
+
54
+
55
+ def load_link_state(path: Path | None = None) -> LinkState | None:
56
+ """Read persisted linker state and validate it before workflow use."""
57
+
58
+ state_path = path or default_link_state_path()
59
+ try:
60
+ data = json.loads(state_path.read_text(encoding="utf-8"))
61
+ except (FileNotFoundError, json.JSONDecodeError):
62
+ return None
63
+ if not isinstance(data, dict):
64
+ return None
65
+ try:
66
+ return LinkState.model_validate(data)
67
+ except PydanticValidationError:
68
+ return None
69
+
70
+
71
+ def write_link_state(
72
+ *,
73
+ snapshot_hash: str,
74
+ git_context: LinkGitContext,
75
+ receipt_path: Path,
76
+ path: Path | None = None,
77
+ ) -> LinkState:
78
+ """Persist the compact typed state used to detect redundant diagnoses."""
79
+
80
+ previous = load_link_state(path)
81
+ state_payload: JsonObject = {
82
+ "schema": LINK_STATE_SCHEMA_V2,
83
+ "generated_at": _now_iso(),
84
+ "snapshot_hash": snapshot_hash,
85
+ "git_head": git_context.head,
86
+ "git_status_hash": git_context.status_hash,
87
+ "receipt_path": str(receipt_path),
88
+ }
89
+ if previous is not None and previous.last_diagnosis_attempt is not None:
90
+ state_payload["last_diagnosis_attempt"] = previous.last_diagnosis_attempt
91
+ state = LinkState.model_validate(state_payload)
92
+ state_path = path or default_link_state_path()
93
+ atomic_write_text(state_path, json.dumps(state.to_payload(), ensure_ascii=False, indent=2) + "\n")
94
+ return state
95
+
96
+
97
+ def collect_git_context(wiki_dir: Path, *, previous_state: LinkState | None = None) -> LinkGitContext:
98
+ """Collect Git state and normalize it before linker diagnosis can branch."""
99
+
100
+ repo_root = _repo_root(wiki_dir)
101
+ if repo_root is None:
102
+ return _unavailable("git_repository_not_available")
103
+
104
+ head = _git_text(repo_root, ["rev-parse", "--verify", "HEAD"], allow_fail=True)
105
+ branch = _git_text(repo_root, ["rev-parse", "--abbrev-ref", "HEAD"], allow_fail=True)
106
+ previous_head = previous_state.git_head if previous_state is not None else ""
107
+ events: list[LinkGitChangeEvent] = []
108
+ if previous_head and head and previous_head != head:
109
+ events.extend(_diff_events(repo_root, wiki_dir, previous_head, head))
110
+ if head:
111
+ events.extend(_worktree_events(repo_root, wiki_dir, head))
112
+ else:
113
+ events.extend(_untracked_events(repo_root, wiki_dir))
114
+ events = _coalesce_delete_create_renames(_dedupe_events(events))
115
+
116
+ context_payload = {
117
+ "schema": LINK_GIT_CONTEXT_SCHEMA,
118
+ "available": True,
119
+ "repo_root": str(repo_root),
120
+ "branch": branch,
121
+ "head": head,
122
+ "previous_link_head": previous_head,
123
+ "dirty": bool(events),
124
+ "changed_note_count": len(events),
125
+ "changed_notes": events,
126
+ "changed_paths": _changed_paths(events),
127
+ "trigger_context_available": bool(events),
128
+ }
129
+ context_payload["status_hash"] = "sha256:" + canonical_json_hash(
130
+ {
131
+ "repo_root": str(repo_root),
132
+ "branch": branch,
133
+ "head": head,
134
+ "changed_notes": [event.to_payload() for event in events],
135
+ }
136
+ )
137
+ return LinkGitContext.model_validate(context_payload)
138
+
139
+
140
+ def trigger_context_from_git(git_context: LinkGitContext) -> LinkTriggerContextFromGit | None:
141
+ if not git_context.available or not git_context.changed_notes:
142
+ return None
143
+ return LinkTriggerContextFromGit.model_validate({
144
+ "schema": "medical-notes-workbench.link-trigger-context.v1",
145
+ "source_workflow": GIT_TRIGGER_SOURCE,
146
+ "changed_notes": git_context.changed_notes,
147
+ "catalog_changed": False,
148
+ "related_notes_export_changed": False,
149
+ })
150
+
151
+
152
+ def _unavailable(reason: str) -> LinkGitContext:
153
+ return LinkGitContext.model_validate({
154
+ "schema": LINK_GIT_CONTEXT_SCHEMA,
155
+ "available": False,
156
+ "unavailable_reason": reason,
157
+ "repo_root": "",
158
+ "branch": "",
159
+ "head": "",
160
+ "previous_link_head": "",
161
+ "dirty": False,
162
+ "changed_note_count": 0,
163
+ "changed_notes": [],
164
+ "changed_paths": [],
165
+ "trigger_context_available": False,
166
+ "status_hash": "",
167
+ })
168
+
169
+
170
+ def _repo_root(wiki_dir: Path) -> Path | None:
171
+ result = subprocess.run(
172
+ ["git", "-C", str(wiki_dir), "rev-parse", "--show-toplevel"],
173
+ text=True,
174
+ capture_output=True,
175
+ check=False,
176
+ )
177
+ if result.returncode != 0:
178
+ return None
179
+ root = result.stdout.strip()
180
+ return Path(root) if root else None
181
+
182
+
183
+ def _git_text(repo_root: Path, args: list[str], *, allow_fail: bool = False) -> str:
184
+ result = subprocess.run(["git", "-C", str(repo_root), *args], text=True, capture_output=True, check=False)
185
+ if result.returncode != 0:
186
+ if allow_fail:
187
+ return ""
188
+ raise RuntimeError(result.stderr.strip() or f"git {' '.join(args)} failed")
189
+ return result.stdout.strip()
190
+
191
+
192
+ def _git_bytes(
193
+ repo_root: Path,
194
+ args: list[str],
195
+ *,
196
+ allow_fail: bool = False,
197
+ input_bytes: bytes | None = None,
198
+ ) -> bytes:
199
+ result = subprocess.run(
200
+ ["git", "-C", str(repo_root), *args],
201
+ input=input_bytes,
202
+ capture_output=True,
203
+ check=False,
204
+ )
205
+ if result.returncode != 0:
206
+ if allow_fail:
207
+ return b""
208
+ raise RuntimeError(result.stderr.decode("utf-8", errors="replace").strip() or f"git {' '.join(args)} failed")
209
+ return result.stdout
210
+
211
+
212
+ def _wiki_prefix(repo_root: Path, wiki_dir: Path) -> str:
213
+ try:
214
+ rel = wiki_dir.resolve().relative_to(repo_root.resolve()).as_posix()
215
+ except ValueError:
216
+ return ""
217
+ return "." if rel == "." else rel
218
+
219
+
220
+ def _repo_to_wiki_path(repo_root: Path, wiki_dir: Path, repo_path: str) -> str:
221
+ prefix = _wiki_prefix(repo_root, wiki_dir)
222
+ normalized = repo_path.replace("\\", "/").strip("/")
223
+ if prefix and prefix != ".":
224
+ prefix = prefix.strip("/")
225
+ if normalized == prefix:
226
+ return ""
227
+ if not normalized.startswith(prefix + "/"):
228
+ return ""
229
+ normalized = normalized[len(prefix) + 1 :]
230
+ return normalized
231
+
232
+
233
+ def _is_note_relpath(rel_path: str) -> bool:
234
+ if not rel_path or not rel_path.endswith(".md"):
235
+ return False
236
+ parts = Path(rel_path).parts
237
+ if any(part.startswith(".") for part in parts):
238
+ return False
239
+ name = parts[-1] if parts else ""
240
+ return ".bak" not in name and ".rewrite" not in name
241
+
242
+
243
+ def _diff_events(
244
+ repo_root: Path,
245
+ wiki_dir: Path,
246
+ base_ref: str,
247
+ target_ref: str | None = None,
248
+ ) -> list[LinkGitChangeEvent]:
249
+ prefix = _wiki_prefix(repo_root, wiki_dir)
250
+ args = ["diff", "--name-status", "--find-renames=70%", "-z", base_ref]
251
+ if target_ref:
252
+ args.append(target_ref)
253
+ args.extend(["--", prefix])
254
+ output = _git_bytes(repo_root, args, allow_fail=True)
255
+ return _events_from_name_status(repo_root, wiki_dir, output, base_ref=base_ref, target_ref=target_ref)
256
+
257
+
258
+ def _worktree_events(repo_root: Path, wiki_dir: Path, head: str) -> list[LinkGitChangeEvent]:
259
+ return [*_diff_events(repo_root, wiki_dir, head), *_untracked_events(repo_root, wiki_dir)]
260
+
261
+
262
+ def _untracked_events(repo_root: Path, wiki_dir: Path) -> list[LinkGitChangeEvent]:
263
+ prefix = _wiki_prefix(repo_root, wiki_dir)
264
+ output = _git_bytes(repo_root, ["ls-files", "--others", "--exclude-standard", "-z", "--", prefix], allow_fail=True)
265
+ events: list[LinkGitChangeEvent] = []
266
+ for raw_path in _nul_items(output):
267
+ rel = _repo_to_wiki_path(repo_root, wiki_dir, raw_path)
268
+ if not _is_note_relpath(rel):
269
+ continue
270
+ path = wiki_dir / rel
271
+ events.append(
272
+ _clean_event(
273
+ {
274
+ "change_type": "created",
275
+ "content_change": "text",
276
+ "path": rel,
277
+ "title": _title_from_file(path),
278
+ "after_hash": _hash_file(path),
279
+ }
280
+ )
281
+ )
282
+ return events
283
+
284
+
285
+ def _events_from_name_status(
286
+ repo_root: Path,
287
+ wiki_dir: Path,
288
+ output: bytes,
289
+ *,
290
+ base_ref: str,
291
+ target_ref: str | None,
292
+ ) -> list[LinkGitChangeEvent]:
293
+ items = _nul_items(output)
294
+ blob_hashes = _blob_hashes_from_name_status(repo_root, output, base_ref=base_ref)
295
+ events: list[LinkGitChangeEvent] = []
296
+ index = 0
297
+ while index < len(items):
298
+ status = items[index]
299
+ index += 1
300
+ if not status:
301
+ continue
302
+ code = status[0]
303
+ if code in {"R", "C"} and index + 1 < len(items):
304
+ old_repo = items[index]
305
+ new_repo = items[index + 1]
306
+ index += 2
307
+ event = _rename_event(
308
+ repo_root,
309
+ wiki_dir,
310
+ old_repo,
311
+ new_repo,
312
+ base_ref=base_ref,
313
+ target_ref=target_ref,
314
+ blob_hashes=blob_hashes,
315
+ )
316
+ elif index < len(items):
317
+ repo_path = items[index]
318
+ index += 1
319
+ event = _single_path_event(
320
+ repo_root,
321
+ wiki_dir,
322
+ code,
323
+ repo_path,
324
+ base_ref=base_ref,
325
+ target_ref=target_ref,
326
+ blob_hashes=blob_hashes,
327
+ )
328
+ else:
329
+ break
330
+ if event is not None:
331
+ events.append(event)
332
+ return events
333
+
334
+
335
+ def _blob_hashes_from_name_status(repo_root: Path, output: bytes, *, base_ref: str) -> dict[tuple[str, str], str]:
336
+ if not base_ref:
337
+ return {}
338
+ items = _nul_items(output)
339
+ repo_paths: list[str] = []
340
+ seen: set[str] = set()
341
+ index = 0
342
+ while index < len(items):
343
+ status = items[index]
344
+ index += 1
345
+ if not status:
346
+ continue
347
+ code = status[0]
348
+ repo_path = ""
349
+ if code in {"R", "C"} and index + 1 < len(items):
350
+ repo_path = items[index]
351
+ index += 2
352
+ elif index < len(items):
353
+ candidate = items[index]
354
+ index += 1
355
+ if code in {"D", "M", "T"}:
356
+ repo_path = candidate
357
+ else:
358
+ break
359
+ if repo_path and repo_path not in seen:
360
+ repo_paths.append(repo_path)
361
+ seen.add(repo_path)
362
+ return _hash_blobs(repo_root, base_ref, repo_paths)
363
+
364
+
365
+ def _hash_blobs(repo_root: Path, ref: str, repo_paths: list[str]) -> dict[tuple[str, str], str]:
366
+ if not ref or not repo_paths:
367
+ return {}
368
+ input_bytes = "".join(f"{ref}:{repo_path}\n" for repo_path in repo_paths).encode("utf-8")
369
+ output = _git_bytes(repo_root, ["cat-file", "--batch"], allow_fail=True, input_bytes=input_bytes)
370
+ hashes: dict[tuple[str, str], str] = {}
371
+ offset = 0
372
+ for repo_path in repo_paths:
373
+ line_end = output.find(b"\n", offset)
374
+ if line_end < 0:
375
+ break
376
+ header = output[offset:line_end].decode("utf-8", errors="replace")
377
+ offset = line_end + 1
378
+ header_parts = header.rsplit(" ", 2)
379
+ if len(header_parts) != 3 or header_parts[1] != "blob":
380
+ continue
381
+ try:
382
+ size = int(header_parts[2])
383
+ except ValueError:
384
+ continue
385
+ blob = output[offset : offset + size]
386
+ if len(blob) != size:
387
+ break
388
+ hashes[(ref, repo_path)] = _hash_note_bytes(blob)
389
+ offset += size
390
+ if output[offset : offset + 1] == b"\n":
391
+ offset += 1
392
+ return hashes
393
+
394
+
395
+ def _single_path_event(
396
+ repo_root: Path,
397
+ wiki_dir: Path,
398
+ code: str,
399
+ repo_path: str,
400
+ *,
401
+ base_ref: str,
402
+ target_ref: str | None,
403
+ blob_hashes: Mapping[tuple[str, str], str],
404
+ ) -> LinkGitChangeEvent | None:
405
+ rel = _repo_to_wiki_path(repo_root, wiki_dir, repo_path)
406
+ if not _is_note_relpath(rel):
407
+ return None
408
+ path = wiki_dir / rel
409
+ if code == "D":
410
+ return _clean_event(
411
+ {
412
+ "change_type": "deleted",
413
+ "content_change": "structural",
414
+ "old_path": rel,
415
+ "old_title": Path(rel).stem,
416
+ "before_hash": _hash_blob_cached(repo_root, base_ref, repo_path, blob_hashes),
417
+ }
418
+ )
419
+ if code in {"A", "C"}:
420
+ return _clean_event(
421
+ {
422
+ "change_type": "created",
423
+ "content_change": "text",
424
+ "path": rel,
425
+ "title": _title_from_file_or_blob(repo_root, path, target_ref, repo_path),
426
+ "after_hash": _hash_file_or_blob(repo_root, path, target_ref, repo_path),
427
+ }
428
+ )
429
+ if code in {"M", "T"}:
430
+ return _clean_event(
431
+ {
432
+ "change_type": "modified",
433
+ "content_change": "text",
434
+ "path": rel,
435
+ "title": _title_from_file_or_blob(repo_root, path, target_ref, repo_path),
436
+ "before_hash": _hash_blob_cached(repo_root, base_ref, repo_path, blob_hashes),
437
+ "after_hash": _hash_file_or_blob(repo_root, path, target_ref, repo_path),
438
+ }
439
+ )
440
+ return None
441
+
442
+
443
+ def _rename_event(
444
+ repo_root: Path,
445
+ wiki_dir: Path,
446
+ old_repo_path: str,
447
+ new_repo_path: str,
448
+ *,
449
+ base_ref: str,
450
+ target_ref: str | None,
451
+ blob_hashes: Mapping[tuple[str, str], str],
452
+ ) -> LinkGitChangeEvent | None:
453
+ old_rel = _repo_to_wiki_path(repo_root, wiki_dir, old_repo_path)
454
+ new_rel = _repo_to_wiki_path(repo_root, wiki_dir, new_repo_path)
455
+ if not _is_note_relpath(old_rel) or not _is_note_relpath(new_rel):
456
+ return None
457
+ old_stem = Path(old_rel).stem
458
+ new_stem = Path(new_rel).stem
459
+ new_path = wiki_dir / new_rel
460
+ return _clean_event(
461
+ {
462
+ "change_type": "moved" if old_stem == new_stem else "renamed",
463
+ "content_change": "structural",
464
+ "old_path": old_rel,
465
+ "old_title": old_stem,
466
+ "path": new_rel,
467
+ "title": _title_from_file_or_blob(repo_root, new_path, target_ref, new_repo_path),
468
+ "replacement_path": new_rel,
469
+ "replacement_title": new_stem,
470
+ "before_hash": _hash_blob_cached(repo_root, base_ref, old_repo_path, blob_hashes),
471
+ "after_hash": _hash_file_or_blob(repo_root, new_path, target_ref, new_repo_path),
472
+ }
473
+ )
474
+
475
+
476
+ def _coalesce_delete_create_renames(events: list[LinkGitChangeEvent]) -> list[LinkGitChangeEvent]:
477
+ deletes = [item for item in events if item.change_type == "deleted" and item.before_hash]
478
+ creates = [item for item in events if item.change_type == "created" and item.after_hash]
479
+ used_deletes: set[int] = set()
480
+ used_creates: set[int] = set()
481
+ replacements: list[LinkGitChangeEvent] = []
482
+ for delete_index, deleted in enumerate(deletes):
483
+ match_index = next(
484
+ (
485
+ create_index
486
+ for create_index, created in enumerate(creates)
487
+ if create_index not in used_creates and created.after_hash == deleted.before_hash
488
+ ),
489
+ None,
490
+ )
491
+ if match_index is None:
492
+ continue
493
+ created = creates[match_index]
494
+ old_rel = deleted.old_path
495
+ new_rel = created.path
496
+ if not old_rel or not new_rel:
497
+ continue
498
+ old_stem = Path(old_rel).stem
499
+ new_stem = Path(new_rel).stem
500
+ replacements.append(
501
+ _clean_event(
502
+ {
503
+ "change_type": "moved" if old_stem == new_stem else "renamed",
504
+ "content_change": "structural",
505
+ "old_path": old_rel,
506
+ "old_title": old_stem,
507
+ "path": new_rel,
508
+ "title": created.title or new_stem,
509
+ "replacement_path": new_rel,
510
+ "replacement_title": new_stem,
511
+ "before_hash": deleted.before_hash,
512
+ "after_hash": created.after_hash,
513
+ }
514
+ )
515
+ )
516
+ used_deletes.add(delete_index)
517
+ used_creates.add(match_index)
518
+
519
+ skipped_delete_keys = {
520
+ (deletes[index].old_path, deletes[index].before_hash)
521
+ for index in used_deletes
522
+ }
523
+ skipped_create_keys = {
524
+ (creates[index].path, creates[index].after_hash)
525
+ for index in used_creates
526
+ }
527
+ output: list[LinkGitChangeEvent] = []
528
+ for event in events:
529
+ if event.change_type == "deleted" and (event.old_path, event.before_hash) in skipped_delete_keys:
530
+ continue
531
+ if event.change_type == "created" and (event.path, event.after_hash) in skipped_create_keys:
532
+ continue
533
+ output.append(event)
534
+ output.extend(replacements)
535
+ return _dedupe_events(output)
536
+
537
+
538
+ def _dedupe_events(events: list[LinkGitChangeEvent]) -> list[LinkGitChangeEvent]:
539
+ seen: set[tuple[str, str, str]] = set()
540
+ deduped: list[LinkGitChangeEvent] = []
541
+ for event in events:
542
+ key = (event.change_type, event.old_path, event.path)
543
+ if key in seen:
544
+ continue
545
+ seen.add(key)
546
+ deduped.append(event)
547
+ return deduped
548
+
549
+
550
+ def _changed_paths(events: list[LinkGitChangeEvent]) -> list[LinkGitChangedPath]:
551
+ paths: list[LinkGitChangedPath] = []
552
+ for event in events:
553
+ paths.append(
554
+ LinkGitChangedPath.model_validate(
555
+ {
556
+ "change_type": event.change_type,
557
+ "old_path": event.old_path,
558
+ "path": event.path,
559
+ }
560
+ )
561
+ )
562
+ return paths
563
+
564
+
565
+ def _nul_items(output: bytes) -> list[str]:
566
+ return [item.decode("utf-8", errors="replace") for item in output.split(b"\0") if item]
567
+
568
+
569
+ def _title_from_file(path: Path) -> str:
570
+ try:
571
+ return _title_from_text(path.read_text(encoding="utf-8"), fallback=path.stem)
572
+ except OSError:
573
+ return path.stem
574
+
575
+
576
+ def _title_from_file_or_blob(repo_root: Path, path: Path, ref: str | None, repo_path: str) -> str:
577
+ if path.is_file():
578
+ return _title_from_file(path)
579
+ if ref:
580
+ text = _blob_text(repo_root, ref, repo_path)
581
+ if text:
582
+ return _title_from_text(text, fallback=Path(repo_path).stem)
583
+ return path.stem
584
+
585
+
586
+ def _title_from_text(text: str, *, fallback: str) -> str:
587
+ for line in text.splitlines():
588
+ stripped = line.strip()
589
+ if stripped.startswith("# "):
590
+ return stripped[2:].strip() or fallback
591
+ return fallback
592
+
593
+
594
+ def _hash_file(path: Path) -> str:
595
+ if not path.is_file():
596
+ return ""
597
+ return _hash_note_bytes(path.read_bytes())
598
+
599
+
600
+ def _hash_file_or_blob(repo_root: Path, path: Path, ref: str | None, repo_path: str) -> str:
601
+ if path.is_file():
602
+ return _hash_file(path)
603
+ return _hash_blob(repo_root, ref, repo_path) if ref else ""
604
+
605
+
606
+ def _hash_blob_cached(
607
+ repo_root: Path,
608
+ ref: str | None,
609
+ repo_path: str,
610
+ blob_hashes: Mapping[tuple[str, str], str],
611
+ ) -> str:
612
+ if not ref:
613
+ return ""
614
+ key = (ref, repo_path)
615
+ if key in blob_hashes:
616
+ return blob_hashes[key]
617
+ return _hash_blob(repo_root, ref, repo_path)
618
+
619
+
620
+ def _hash_blob(repo_root: Path, ref: str | None, repo_path: str) -> str:
621
+ if not ref:
622
+ return ""
623
+ data = _git_bytes(repo_root, ["show", f"{ref}:{repo_path}"], allow_fail=True)
624
+ return _hash_note_bytes(data) if data else ""
625
+
626
+
627
+ def _hash_note_bytes(data: bytes) -> str:
628
+ normalized = data.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
629
+ return "sha256:" + sha256(normalized).hexdigest()
630
+
631
+
632
+ def _blob_text(repo_root: Path, ref: str, repo_path: str) -> str:
633
+ data = _git_bytes(repo_root, ["show", f"{ref}:{repo_path}"], allow_fail=True)
634
+ return data.decode("utf-8", errors="replace") if data else ""
635
+
636
+
637
+ def _clean_event(event: object) -> LinkGitChangeEvent:
638
+ return LinkGitChangeEvent.model_validate(event)