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,485 @@
1
+ """Typed linker runtime boundary for `/mednotes:link` and `/mednotes:link-body`.
2
+
3
+ The linker adapter still returns operational JSON. This boundary validates that
4
+ JSON into facts, but it does not name the final workflow outcome. The only
5
+ machine event emitted here is `LinkRuntimeObservedEvent`; `LinkMachine` guards
6
+ own the operational leaf-state priority.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Literal
12
+
13
+ from pydantic import ConfigDict, Field, StrictStr, model_validator
14
+
15
+ from mednotes.domains.wiki.common import ValidationError
16
+ from mednotes.domains.wiki.contracts.curator import CuratorBatchPlan
17
+ from mednotes.domains.wiki.contracts.related_notes_runtime import LinkRelatedNotesSync, RelatedNotesRecoveryState
18
+ from mednotes.domains.wiki.contracts.vocabulary_ingestion import VocabularyBootstrapPlan
19
+ from mednotes.domains.wiki.flows.link.link_fsm import LINK_BODY_PUBLIC_WORKFLOW, LINK_WORKFLOW, LinkFsmFacts
20
+ from mednotes.domains.wiki.flows.link.link_machine import (
21
+ LinkMode,
22
+ LinkRuntimeObservation,
23
+ LinkRuntimeObservedEvent,
24
+ LinkState,
25
+ )
26
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
27
+ from mednotes.kernel.workflow import VersionControlSafety
28
+
29
+
30
+ class LinkerRunResult(ContractModel):
31
+ """Typed lens for linker operation JSON before it reaches LinkMachine.
32
+
33
+ The lower-level linker emits one contract for diagnosis and another for
34
+ apply. Missing or unknown schemas are contract errors; defaults here may
35
+ preserve optional counters, never fabricate a valid operation from
36
+ non-contract JSON.
37
+ """
38
+
39
+ schema_id: Literal[
40
+ "medical-notes-workbench.link-diagnosis.v1",
41
+ "medical-notes-workbench.link-run.v1",
42
+ "medical-notes-workbench.link-run-receipt.v1",
43
+ ] = Field(alias="schema")
44
+ phase: StrictStr = ""
45
+ status: StrictStr = ""
46
+ blocked_reason: StrictStr = ""
47
+ next_action: StrictStr = ""
48
+ returncode: int = Field(default=0, ge=0, strict=True)
49
+ files_changed: int = Field(default=0, ge=0, strict=True)
50
+ links_planned: int = Field(default=0, ge=0, strict=True)
51
+ links_rewritten: int = Field(default=0, ge=0, strict=True)
52
+ blocker_count: int = Field(default=0, ge=0, strict=True)
53
+ related_notes_applied: bool = Field(default=False, strict=True)
54
+ changed_files: list[str] = Field(default_factory=list)
55
+ required_inputs: list[str] = Field(default_factory=list)
56
+ diagnosis_path: StrictStr = ""
57
+ receipt_path: StrictStr = ""
58
+ error: StrictStr = ""
59
+ parse_error: StrictStr = ""
60
+ stale_reason: StrictStr = ""
61
+ related_notes_sync: LinkRelatedNotesSync | None = None
62
+ related_notes_recovery_state: RelatedNotesRecoveryState = Field(default_factory=RelatedNotesRecoveryState)
63
+ body_term_linker: JsonObject | None = None
64
+ vocabulary_db_path: StrictStr = ""
65
+ vocabulary_bootstrap: VocabularyBootstrapPlan | None = None
66
+ vocabulary_curator_batch_plan: CuratorBatchPlan | None = None
67
+ vocabulary_curator_batch_plan_path: StrictStr = ""
68
+ operation_payload: JsonObject = Field(default_factory=dict)
69
+
70
+ @model_validator(mode="after")
71
+ def _validate_operation_contract(self) -> LinkerRunResult:
72
+ """Require the minimum public fields that keep success from being invented."""
73
+
74
+ if not self.status.strip():
75
+ raise ValueError("status is required for linker runtime contract")
76
+ return self
77
+
78
+ @classmethod
79
+ def from_payload(cls, value: object) -> LinkerRunResult:
80
+ if isinstance(value, cls):
81
+ return value
82
+ raw = _raw_payload(value)
83
+ related = _raw_field(raw, "related_notes_sync")
84
+ bootstrap = _raw_field(raw, "vocabulary_bootstrap")
85
+ curator_batch_plan = _raw_field(raw, "vocabulary_curator_batch_plan")
86
+ return cls.model_validate(
87
+ {
88
+ "schema": _raw_text_field(raw, "schema"),
89
+ "phase": _raw_text_field(raw, "phase"),
90
+ "status": _raw_text_field(raw, "status"),
91
+ "blocked_reason": _raw_text_field(raw, "blocked_reason"),
92
+ "next_action": _raw_text_field(raw, "next_action"),
93
+ "returncode": _raw_field(raw, "returncode", 0),
94
+ "files_changed": _raw_field(raw, "files_changed", 0),
95
+ "links_planned": _raw_field(raw, "links_planned", 0),
96
+ "links_rewritten": _raw_field(raw, "links_rewritten", 0),
97
+ "blocker_count": _raw_field(raw, "blocker_count", 0),
98
+ "related_notes_applied": _raw_field(raw, "related_notes_applied", False),
99
+ "changed_files": [str(item) for item in _raw_list(_raw_field(raw, "changed_files")) if item],
100
+ "required_inputs": [str(item) for item in _raw_list(_raw_field(raw, "required_inputs")) if item],
101
+ "diagnosis_path": _raw_text_field(raw, "diagnosis_path"),
102
+ "receipt_path": _raw_text_field(raw, "receipt_path"),
103
+ "error": _raw_text_field(raw, "error"),
104
+ "parse_error": _raw_text_field(raw, "parse_error"),
105
+ "stale_reason": _raw_text_field(raw, "stale_reason"),
106
+ "related_notes_sync": LinkRelatedNotesSync.from_payload(related) if isinstance(related, dict) else None,
107
+ "related_notes_recovery_state": RelatedNotesRecoveryState.from_payload(
108
+ _raw_field(raw, "related_notes_recovery_state")
109
+ ),
110
+ "body_term_linker": _raw_payload(_raw_field(raw, "body_term_linker")) or None,
111
+ "vocabulary_db_path": _raw_text_field(raw, "vocabulary_db_path"),
112
+ "vocabulary_bootstrap": (
113
+ VocabularyBootstrapPlan.model_validate(bootstrap) if bootstrap is not None else None
114
+ ),
115
+ "vocabulary_curator_batch_plan": (
116
+ CuratorBatchPlan.model_validate(curator_batch_plan) if _has_payload(curator_batch_plan) else None
117
+ ),
118
+ "vocabulary_curator_batch_plan_path": _raw_text_field(raw, "vocabulary_curator_batch_plan_path"),
119
+ "operation_payload": raw,
120
+ }
121
+ )
122
+
123
+
124
+ def link_fsm_facts_from_linker_result(
125
+ result: JsonObject,
126
+ *,
127
+ run_id: str,
128
+ mode: Literal["diagnose", "apply"],
129
+ include_related_notes: bool,
130
+ version_control_safety: VersionControlSafety | dict[str, object],
131
+ ) -> LinkFsmFacts:
132
+ """Translate validated linker facts into the single LinkMachine observation event."""
133
+
134
+ linker_result = LinkerRunResult.from_payload(result)
135
+ link_mode = LinkMode.FULL if include_related_notes else LinkMode.BODY_ONLY
136
+ workflow = LINK_WORKFLOW if link_mode == LinkMode.FULL else LINK_BODY_PUBLIC_WORKFLOW
137
+ observation = _observation_for_linker_result(
138
+ linker_result,
139
+ operation=mode,
140
+ link_mode=link_mode,
141
+ )
142
+ event = LinkRuntimeObservedEvent(
143
+ workflow=workflow,
144
+ run_id=run_id,
145
+ current_state=LinkState.DIAGNOSING_GRAPH.value,
146
+ observation=observation,
147
+ audit_evidence=_audit_evidence(linker_result, mode=mode, include_related_notes=include_related_notes),
148
+ )
149
+ changed_files = list(linker_result.changed_files)
150
+ changed_count = max(linker_result.files_changed, len(changed_files))
151
+ return LinkFsmFacts(
152
+ workflow=workflow,
153
+ mode=link_mode,
154
+ run_id=run_id,
155
+ initial_state=LinkState.DIAGNOSING_GRAPH,
156
+ event=event,
157
+ changed_files=changed_files,
158
+ mutated=mode == "apply" and changed_count > 0,
159
+ artifacts=_artifacts(linker_result),
160
+ version_control_safety=_version_control_safety_with_file_count(
161
+ version_control_safety,
162
+ changed_file_count=changed_count,
163
+ ),
164
+ error_context=_error_context(linker_result),
165
+ )
166
+
167
+
168
+ def _observation_for_linker_result(
169
+ result: LinkerRunResult,
170
+ *,
171
+ operation: Literal["diagnose", "apply"],
172
+ link_mode: LinkMode,
173
+ ) -> LinkRuntimeObservation:
174
+ body_reason = _body_linker_blocked_reason(result)
175
+ related_blocked = link_mode == LinkMode.FULL and _related_notes_blocked(result)
176
+ vocabulary_bootstrap_required = link_mode == LinkMode.FULL and body_reason == "vocabulary_bootstrap_required"
177
+ vocabulary_curator_required = link_mode == LinkMode.FULL and body_reason == "vocabulary_semantic_ingestion_pending"
178
+ vocabulary_db_path = _vocabulary_db_path(result)
179
+ if vocabulary_bootstrap_required and not vocabulary_db_path:
180
+ raise ValueError("vocabulary_db_path is required when vocabulary bootstrap is required")
181
+ return LinkRuntimeObservation(
182
+ mode=link_mode,
183
+ operation=operation,
184
+ failed=_failed(result),
185
+ stale_diagnosis=_stale_diagnosis(result),
186
+ changed_file_count=max(result.files_changed, len(result.changed_files)),
187
+ planned_link_count=result.links_planned,
188
+ rewritten_link_count=result.links_rewritten,
189
+ blocker_count=max(result.blocker_count, 1 if result.status == "blocked" else 0),
190
+ body_linker_blocked=bool(body_reason),
191
+ body_linker_blocked_reason=body_reason,
192
+ related_notes_present=result.related_notes_sync is not None,
193
+ related_notes_blocked=related_blocked,
194
+ related_notes_waiting_external=link_mode == LinkMode.FULL and _related_notes_waiting_external(result),
195
+ related_notes_applied=result.related_notes_applied,
196
+ vocabulary_bootstrap_required=vocabulary_bootstrap_required,
197
+ vocabulary_curator_required=vocabulary_curator_required,
198
+ vocabulary_db_path=vocabulary_db_path,
199
+ vocabulary_curator_batch_plan_path=_vocabulary_curator_batch_plan_path(result),
200
+ vocabulary_curator_work_item_count=_vocabulary_curator_work_item_count(result),
201
+ next_action=_next_action_from_facts(result),
202
+ reason_code=_reason_code_from_facts(result),
203
+ related_notes_export_recovery_required=bool(_related_notes_export_recovery_reason(result)),
204
+ related_notes_export_recovery_reason=_related_notes_export_recovery_reason(result),
205
+ related_notes_recovery_state=result.related_notes_recovery_state.to_payload(),
206
+ )
207
+
208
+
209
+ def _failed(result: LinkerRunResult) -> bool:
210
+ if result.error.strip() or result.parse_error.strip():
211
+ return True
212
+ return result.returncode not in {0, 3}
213
+
214
+
215
+ def _stale_diagnosis(result: LinkerRunResult) -> bool:
216
+ if result.blocked_reason == "stale_diagnosis":
217
+ return True
218
+ return bool(result.stale_reason.strip())
219
+
220
+
221
+ def _related_notes_blocked(result: LinkerRunResult) -> bool:
222
+ related = result.related_notes_sync
223
+ return bool(
224
+ result.blocked_reason == "related_notes_blocked"
225
+ or (related is not None and (related.status == "blocked" or bool(related.blocked_reason)))
226
+ )
227
+
228
+
229
+ RELATED_NOTES_EXPORT_RECOVERY_REASONS = frozenset(
230
+ {
231
+ "related_notes_hash_mismatch",
232
+ "related_notes_export_stale",
233
+ "related_notes_vault_mismatch",
234
+ }
235
+ )
236
+
237
+
238
+ def _related_notes_export_recovery_reason(result: LinkerRunResult) -> str:
239
+ """Expose a recoverable Related Notes export fact without executing recovery."""
240
+
241
+ related = result.related_notes_sync
242
+ reason = related.blocked_reason if related is not None else ""
243
+ return reason if reason in RELATED_NOTES_EXPORT_RECOVERY_REASONS else ""
244
+
245
+
246
+ def _related_notes_waiting_external(result: LinkerRunResult) -> bool:
247
+ recovery = result.related_notes_recovery_state
248
+ return (
249
+ _related_notes_blocked(result)
250
+ and recovery.status == "waiting_for_retry"
251
+ and recovery.blocked_reason
252
+ in {
253
+ "related_notes_headless_quota_exhausted",
254
+ "related_notes_headless_time_budget_exhausted",
255
+ }
256
+ )
257
+
258
+
259
+ class _BodyTermLinkerFields(ContractModel):
260
+ """Typed body-linker blocker fields used to derive link observation facts."""
261
+
262
+ model_config = ConfigDict(extra="ignore")
263
+
264
+ blocked_reason: StrictStr = ""
265
+
266
+
267
+ def _body_linker_blocked_reason(result: LinkerRunResult) -> str:
268
+ return _BodyTermLinkerFields.model_validate(result.body_term_linker or {}).blocked_reason
269
+
270
+
271
+ def _reason_code_from_facts(result: LinkerRunResult) -> str:
272
+ if _stale_diagnosis(result):
273
+ return "stale_diagnosis"
274
+ if _body_linker_blocked_reason(result):
275
+ body_reason = _body_linker_blocked_reason(result)
276
+ if body_reason == "vocabulary_semantic_ingestion_pending":
277
+ return "vocabulary_curator_required"
278
+ return body_reason
279
+ recovery_reason = _related_notes_export_recovery_reason(result)
280
+ if recovery_reason:
281
+ return recovery_reason
282
+ if _related_notes_blocked(result):
283
+ return "related_notes_blocked"
284
+ if result.blocked_reason.strip():
285
+ return result.blocked_reason.strip()
286
+ if result.error.strip():
287
+ return result.error.strip()
288
+ if result.parse_error.strip():
289
+ return result.parse_error.strip()
290
+ return ""
291
+
292
+
293
+ def _next_action_from_facts(result: LinkerRunResult) -> str:
294
+ if result.next_action.strip():
295
+ return result.next_action.strip()
296
+ reason = _reason_code_from_facts(result)
297
+ match reason:
298
+ case "stale_diagnosis":
299
+ return "Repetir a conferencia de links e aplicar o novo diagnostico."
300
+ case "vocabulary_bootstrap_required":
301
+ return "Preparar o vocabulario pela rota oficial e repetir /mednotes:link."
302
+ case "vocabulary_semantic_ingestion_pending" | "vocabulary_curator_required":
303
+ return "Aplicar a curadoria semantica oficial e repetir /mednotes:link."
304
+ case "related_notes_blocked":
305
+ return "Atualizar o export do Related Notes e repetir /mednotes:link."
306
+ case "graph_blockers":
307
+ return "Resolver os bloqueios de grafo pela rota oficial e repetir /mednotes:link."
308
+ case "body_linker_blocked":
309
+ return "Resolver o bloqueio informado pelo linker e repetir /mednotes:link."
310
+ case _:
311
+ return ""
312
+
313
+
314
+ def _audit_evidence(
315
+ result: LinkerRunResult,
316
+ *,
317
+ mode: Literal["diagnose", "apply"],
318
+ include_related_notes: bool,
319
+ ) -> JsonObject:
320
+ recovery = result.related_notes_recovery_state
321
+ operation: JsonObject = {
322
+ "schema": result.schema_id,
323
+ "phase": result.phase,
324
+ "status": result.status,
325
+ "blocked_reason": _reason_code_from_facts(result),
326
+ "next_action": _next_action_from_facts(result),
327
+ "returncode": result.returncode,
328
+ "files_changed": result.files_changed,
329
+ "links_planned": result.links_planned,
330
+ "links_rewritten": result.links_rewritten,
331
+ "blocker_count": result.blocker_count,
332
+ "related_notes_applied": result.related_notes_applied,
333
+ "changed_files": list(result.changed_files),
334
+ "required_inputs": list(result.required_inputs),
335
+ "diagnosis_path": result.diagnosis_path,
336
+ "receipt_path": result.receipt_path,
337
+ }
338
+ if result.related_notes_sync is not None:
339
+ operation["related_notes_sync"] = result.related_notes_sync.to_payload()
340
+ if result.body_term_linker is not None:
341
+ operation["body_term_linker"] = result.body_term_linker
342
+ if result.vocabulary_bootstrap is not None:
343
+ operation["vocabulary_bootstrap"] = result.vocabulary_bootstrap.to_payload()
344
+ if result.vocabulary_curator_batch_plan is not None:
345
+ operation["vocabulary_curator_batch_plan"] = result.vocabulary_curator_batch_plan.to_payload()
346
+ if recovery:
347
+ operation["related_notes_recovery_state"] = recovery.to_payload()
348
+ evidence: JsonObject = {
349
+ "adapter_schema": result.schema_id,
350
+ "adapter_phase": result.phase,
351
+ "adapter_status": result.status,
352
+ "adapter_reason": _reason_code_from_facts(result),
353
+ "operation": operation,
354
+ "mode": mode,
355
+ "include_related_notes": include_related_notes,
356
+ "counts": {
357
+ "files_changed": max(result.files_changed, len(result.changed_files)),
358
+ "links_planned": result.links_planned,
359
+ "links_rewritten": result.links_rewritten,
360
+ "blocker_count": result.blocker_count,
361
+ "related_notes_applied": result.related_notes_applied,
362
+ "fresh_record_count": recovery.fresh_record_count,
363
+ "remaining_count": recovery.remaining_count,
364
+ "total_note_count": recovery.total_note_count,
365
+ "reused_count": recovery.reused_count,
366
+ "embedded_count": recovery.embedded_count,
367
+ },
368
+ }
369
+ if result.required_inputs:
370
+ evidence["required_inputs"] = list(result.required_inputs)
371
+ if recovery:
372
+ evidence["related_notes_recovery_state"] = recovery.to_payload()
373
+ if result.stale_reason:
374
+ evidence["stale_reason"] = result.stale_reason
375
+ for key in (
376
+ "expected_git_status_hash",
377
+ "actual_git_status_hash",
378
+ "expected_git_head",
379
+ "actual_git_head",
380
+ ):
381
+ try:
382
+ value = result.operation_payload[key]
383
+ except KeyError:
384
+ continue
385
+ if isinstance(value, str) and value.strip():
386
+ evidence[key] = value
387
+ return JsonObjectAdapter.validate_python(evidence)
388
+
389
+
390
+ def _error_context(result: LinkerRunResult) -> JsonObject:
391
+ reason = _reason_code_from_facts(result)
392
+ if not (
393
+ _failed(result)
394
+ or _stale_diagnosis(result)
395
+ or result.blocker_count
396
+ or _body_linker_blocked_reason(result)
397
+ or _related_notes_blocked(result)
398
+ ):
399
+ return {}
400
+ summary = result.error or result.parse_error or result.blocked_reason or reason
401
+ return JsonObjectAdapter.validate_python(
402
+ {
403
+ "schema": "medical-notes-workbench.error-context.v1",
404
+ "phase": result.phase or "link",
405
+ "blocked_reason": reason,
406
+ "root_cause": reason,
407
+ "error_summary": summary,
408
+ "suggested_fix": _next_action_from_facts(result),
409
+ "next_action": _next_action_from_facts(result),
410
+ "retry_scope": "link_official_route",
411
+ "human_decision_required": False,
412
+ "missing_inputs": list(result.required_inputs),
413
+ }
414
+ )
415
+
416
+
417
+ def _vocabulary_db_path(result: LinkerRunResult) -> str:
418
+ if result.vocabulary_bootstrap is not None:
419
+ return result.vocabulary_bootstrap.db_path.strip()
420
+ return result.vocabulary_db_path
421
+
422
+
423
+ def _vocabulary_curator_batch_plan_path(result: LinkerRunResult) -> str:
424
+ return result.vocabulary_curator_batch_plan_path
425
+
426
+
427
+ def _vocabulary_curator_work_item_count(result: LinkerRunResult) -> int:
428
+ if result.vocabulary_curator_batch_plan is None:
429
+ return 1
430
+ return max(1, result.vocabulary_curator_batch_plan.item_count)
431
+
432
+
433
+ def _artifacts(result: LinkerRunResult) -> JsonObject:
434
+ artifacts: JsonObject = {}
435
+ if result.diagnosis_path:
436
+ artifacts["diagnosis_path"] = result.diagnosis_path
437
+ if result.receipt_path:
438
+ artifacts["receipt_path"] = result.receipt_path
439
+ return artifacts
440
+
441
+
442
+ def _version_control_safety_with_file_count(
443
+ value: VersionControlSafety | dict[str, object],
444
+ *,
445
+ changed_file_count: int,
446
+ ) -> VersionControlSafety:
447
+ safety = value if isinstance(value, VersionControlSafety) else VersionControlSafety.model_validate(value)
448
+ if safety.changed_file_count == changed_file_count:
449
+ return safety
450
+ raise ValidationError(
451
+ "version_control_safety_evidence_mismatch: guard evidence changed_file_count "
452
+ "does not match linker result"
453
+ )
454
+
455
+
456
+ def _raw_payload(value: object) -> JsonObject:
457
+ if isinstance(value, ContractModel):
458
+ return JsonObjectAdapter.validate_python(value.to_payload())
459
+ if isinstance(value, dict):
460
+ return JsonObjectAdapter.validate_python(dict(value))
461
+ return {}
462
+
463
+
464
+ def _has_payload(value: object) -> bool:
465
+ """Treat empty optional operation objects as absent, not as valid plans."""
466
+
467
+ return isinstance(value, dict) and bool(value)
468
+
469
+
470
+ def _raw_list(value: object) -> list[object]:
471
+ if isinstance(value, list):
472
+ return list(value)
473
+ return []
474
+
475
+
476
+ def _raw_field(raw: JsonObject, key: str, default: object = None) -> object:
477
+ try:
478
+ return raw[key]
479
+ except KeyError:
480
+ return default
481
+
482
+
483
+ def _raw_text_field(raw: JsonObject, key: str) -> object:
484
+ value = _raw_field(raw, key, "")
485
+ return "" if value is None else value
@@ -0,0 +1,183 @@
1
+ """Trigger context helpers for link graph repair."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Literal
7
+
8
+ from pydantic import Field, StrictBool, StrictStr, TypeAdapter, model_validator
9
+ from pydantic import ValidationError as PydanticValidationError
10
+
11
+ from mednotes.domains.wiki.capabilities.notes.raw_chats import atomic_write_text
12
+ from mednotes.domains.wiki.common import ValidationError
13
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
14
+
15
+ LINK_TRIGGER_CONTEXT_SCHEMA = "medical-notes-workbench.link-trigger-context.v1"
16
+
17
+ ChangeType = Literal["created", "modified", "deleted", "renamed", "moved", "merged"]
18
+ ContentChange = Literal["text", "metadata", "structural", "image_only"]
19
+ _TRIGGER_BY_CHANGE = {
20
+ "created": "note_created",
21
+ "modified": "note_modified",
22
+ "deleted": "note_deleted",
23
+ "renamed": "note_renamed",
24
+ "moved": "note_moved",
25
+ "merged": "note_merged",
26
+ }
27
+
28
+
29
+ class LinkTriggerChangedNote(ContractModel):
30
+ """One changed-note record that may force graph or Related Notes repair."""
31
+
32
+ change_type: ChangeType
33
+ content_change: ContentChange = "text"
34
+ path: StrictStr | None = None
35
+ old_path: StrictStr | None = None
36
+ title: StrictStr | None = None
37
+ old_title: StrictStr | None = None
38
+ replacement_path: StrictStr | None = None
39
+ replacement_title: StrictStr | None = None
40
+ before_hash: StrictStr | None = None
41
+ after_hash: StrictStr | None = None
42
+ reason: StrictStr | None = None
43
+ reasons: list[StrictStr] = Field(default_factory=list)
44
+
45
+ @model_validator(mode="after")
46
+ def _structural_changes_name_origin_and_destination(self) -> LinkTriggerChangedNote:
47
+ if self.change_type in {"renamed", "moved", "merged"}:
48
+ has_old = bool(self.old_title or self.old_path)
49
+ has_new = bool(self.title or self.path or self.replacement_title or self.replacement_path)
50
+ if not has_old or not has_new:
51
+ raise ValueError(f"link trigger context {self.change_type} exige origem e destino explícitos.")
52
+ if self.change_type == "deleted" and not (self.old_title or self.old_path):
53
+ raise ValueError("link trigger context deleted exige old_title ou old_path.")
54
+ return self
55
+
56
+ def clean_payload(self) -> JsonObject:
57
+ payload = self.model_dump(mode="json", exclude_none=True)
58
+ if not self.reasons:
59
+ payload.pop("reasons", None)
60
+ return JsonObjectAdapter.validate_python(payload)
61
+
62
+ def affected_path(self) -> str:
63
+ return self.path or self.old_path or ""
64
+
65
+
66
+ class LinkTriggerContext(ContractModel):
67
+ """Closed boundary for workflow-provided linker trigger context."""
68
+
69
+ schema_id: Literal["medical-notes-workbench.link-trigger-context.v1"] = Field(
70
+ default=LINK_TRIGGER_CONTEXT_SCHEMA,
71
+ alias="schema",
72
+ )
73
+ source_workflow: StrictStr = Field(min_length=1)
74
+ changed_notes: list[LinkTriggerChangedNote]
75
+ catalog_changed: StrictBool = False
76
+ related_notes_export_changed: StrictBool = False
77
+ batch_id: StrictStr | None = None
78
+
79
+ def clean_payload(self) -> JsonObject:
80
+ payload = {
81
+ "schema": self.schema_id,
82
+ "source_workflow": self.source_workflow,
83
+ "changed_notes": [note.clean_payload() for note in self.changed_notes],
84
+ "catalog_changed": self.catalog_changed,
85
+ "related_notes_export_changed": self.related_notes_export_changed,
86
+ }
87
+ if self.batch_id:
88
+ payload["batch_id"] = self.batch_id
89
+ return JsonObjectAdapter.validate_python(payload)
90
+
91
+ def image_only(self) -> bool:
92
+ if self.catalog_changed or self.related_notes_export_changed:
93
+ return False
94
+ return bool(self.changed_notes) and all(note.content_change == "image_only" for note in self.changed_notes)
95
+
96
+
97
+ LinkTriggerContextAdapter = TypeAdapter(LinkTriggerContext)
98
+
99
+
100
+ def _trigger_context(payload: object) -> LinkTriggerContext:
101
+ try:
102
+ return LinkTriggerContextAdapter.validate_python(payload)
103
+ except PydanticValidationError as exc:
104
+ first = exc.errors()[0] if exc.errors() else {}
105
+ loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
106
+ message = str(first.get("msg") or "payload inválido")
107
+ raise ValidationError(f"link trigger context inválido: {loc}: {message}") from exc
108
+
109
+
110
+ def _trigger_context_or_none(payload: object | None) -> LinkTriggerContext | None:
111
+ return None if payload is None else _trigger_context(payload)
112
+
113
+
114
+ def validate_trigger_context(payload: object) -> JsonObject:
115
+ return _trigger_context(payload).clean_payload()
116
+
117
+
118
+ def load_trigger_context(path: Path | None) -> JsonObject | None:
119
+ if path is None:
120
+ return None
121
+ try:
122
+ payload = json.loads(path.read_text(encoding="utf-8"))
123
+ except FileNotFoundError as exc:
124
+ raise ValidationError(f"Trigger context não encontrado: {path}") from exc
125
+ except json.JSONDecodeError as exc:
126
+ raise ValidationError(f"Trigger context inválido: {path}: {exc}") from exc
127
+ return validate_trigger_context(payload)
128
+
129
+
130
+ def write_trigger_context(path: Path, payload: object) -> Path:
131
+ clean = validate_trigger_context(payload)
132
+ atomic_write_text(path, json.dumps(clean, ensure_ascii=False, indent=2) + "\n")
133
+ return path
134
+
135
+
136
+ def derive_triggers(payload: object | None) -> list[str]:
137
+ context = _trigger_context_or_none(payload)
138
+ if context is None:
139
+ return ["manual_request"]
140
+ if context.image_only():
141
+ return ["image_only_change"]
142
+ triggers: list[str] = []
143
+ for note in context.changed_notes:
144
+ trigger = _TRIGGER_BY_CHANGE[note.change_type]
145
+ if trigger not in triggers:
146
+ triggers.append(trigger)
147
+ if context.catalog_changed:
148
+ triggers.append("catalog_changed")
149
+ if context.related_notes_export_changed:
150
+ triggers.append("related_notes_export_changed")
151
+ return triggers
152
+
153
+
154
+ def is_image_only_context(payload: object | None) -> bool:
155
+ context = _trigger_context_or_none(payload)
156
+ return False if context is None else context.image_only()
157
+
158
+
159
+ def affected_notes_from_context(payload: object | None) -> list[JsonObject]:
160
+ context = _trigger_context_or_none(payload)
161
+ if context is None:
162
+ return []
163
+ affected: list[JsonObject] = []
164
+ for note in context.changed_notes:
165
+ path = note.affected_path()
166
+ entry: JsonObject = {
167
+ "reason": note.change_type,
168
+ }
169
+ if path:
170
+ entry["path"] = path
171
+ affected.append(JsonObjectAdapter.validate_python(entry))
172
+ return affected
173
+
174
+
175
+ def structural_events_from_context(payload: object | None) -> list[JsonObject]:
176
+ context = _trigger_context_or_none(payload)
177
+ if context is None:
178
+ return []
179
+ events: list[JsonObject] = []
180
+ for note in context.changed_notes:
181
+ if note.change_type in {"deleted", "renamed", "moved", "merged"}:
182
+ events.append(note.clean_payload())
183
+ return events