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,949 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Literal
7
+
8
+ from pydantic import ValidationError as PydanticValidationError
9
+
10
+ from mednotes.domains.wiki.capabilities.related_notes.related_notes import (
11
+ recover_related_notes_export_operation_result,
12
+ sync_related_notes_operation_result,
13
+ )
14
+ from mednotes.domains.wiki.capabilities.specialist.specialist_receipts import (
15
+ validate_specialist_task_run_receipt_attestation,
16
+ )
17
+ from mednotes.domains.wiki.common import ValidationError
18
+ from mednotes.domains.wiki.contracts.effect_payloads import (
19
+ LinkEffectBlockedOutcome,
20
+ LinkEffectCompletedOutcome,
21
+ LinkEffectFailedOutcome,
22
+ LinkEffectGraphBlockedOutcome,
23
+ LinkEffectLinkerBlockedOutcome,
24
+ LinkSubworkflowEffectPayload,
25
+ LinkWorkflowRunEffectPayload,
26
+ RelatedNotesBlockedOutcome,
27
+ RelatedNotesExportCompletedOutcome,
28
+ RelatedNotesExportEffectPayload,
29
+ RelatedNotesQuotaWaitOutcome,
30
+ RelatedNotesRecoveryEffectPayload,
31
+ RelatedNotesSyncCompletedOutcome,
32
+ RelatedNotesSyncEffectPayload,
33
+ RelatedNotesSyncSectionEffectPayload,
34
+ RelatedNotesSyncWarningOutcome,
35
+ SpecialistModelBlockedOutcome,
36
+ SpecialistModelCapacityWaitOutcome,
37
+ SpecialistModelCompletedOutcome,
38
+ SpecialistModelEffectPayload,
39
+ WaitExternalEffectOutcome,
40
+ WaitExternalEffectPayload,
41
+ )
42
+ from mednotes.domains.wiki.flows.link.linking import run_linker
43
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter, JsonValue
44
+ from mednotes.kernel.effect_executor import WorkflowEffectExecutionContext
45
+ from mednotes.kernel.effects import (
46
+ WorkflowEffect,
47
+ WorkflowEffectKind,
48
+ WorkflowEffectResult,
49
+ WorkflowEffectStatus,
50
+ )
51
+ from mednotes.kernel.workflow import VersionControlSafety
52
+
53
+ RELATED_NOTES_EXTERNAL_RETRY_REASONS = frozenset(
54
+ {
55
+ "related_notes_headless_quota_exhausted",
56
+ "related_notes_headless_time_budget_exhausted",
57
+ }
58
+ )
59
+
60
+
61
+ @dataclass
62
+ class WaitExternalEffectAdapter:
63
+ def run(self, effect: WorkflowEffect, context: WorkflowEffectExecutionContext) -> WorkflowEffectResult:
64
+ del context
65
+ if effect.kind != WorkflowEffectKind.WAIT_EXTERNAL:
66
+ raise ValueError(f"WaitExternalEffectAdapter cannot run effect kind: {effect.kind.value}")
67
+ try:
68
+ typed = WaitExternalEffectPayload.from_effect_payload(effect.payload)
69
+ except PydanticValidationError as exc:
70
+ return _effect_payload_contract_block(
71
+ effect,
72
+ payload_schema="wait_external_effect_payload",
73
+ exc=exc,
74
+ operation_payload=_safe_operation_payload(effect.payload),
75
+ )
76
+ recovery_state = typed.related_notes_recovery_state
77
+ if recovery_state is not None:
78
+ reason = recovery_state.blocked_reason or typed.blocked_reason or effect.target
79
+ next_action = recovery_state.next_action or typed.next_action
80
+ else:
81
+ reason = typed.blocked_reason or typed.wait_target or effect.target or "workflow_external_wait"
82
+ next_action = typed.next_action
83
+ resume_action = effect.resume_action or next_action or "Aguardar a condição externa e retomar pela rota oficial."
84
+ return WorkflowEffectResult(
85
+ effect=effect,
86
+ status=WorkflowEffectStatus.WAITING_EXTERNAL,
87
+ outcome=WaitExternalEffectOutcome(reason_code=reason),
88
+ public_summary="Workflow aguardando condição externa para continuar.",
89
+ developer_summary=f"Workflow effect is waiting for external condition: {reason}.",
90
+ payload=typed.to_payload(),
91
+ next_action=resume_action,
92
+ resume_action=resume_action,
93
+ error_context={
94
+ "phase": effect.origin_state,
95
+ "blocked_reason": reason,
96
+ "root_cause": reason,
97
+ "affected_artifact": effect.target,
98
+ "next_action": resume_action,
99
+ "retry_scope": "wait_external_resume",
100
+ "human_decision_required": False,
101
+ "wait_target": typed.wait_target,
102
+ },
103
+ )
104
+
105
+
106
+ @dataclass
107
+ class RelatedNotesEffectAdapter:
108
+ config: object
109
+ recover_related_notes_export_fn: Callable[..., object] = recover_related_notes_export_operation_result
110
+ sync_related_notes_fn: Callable[..., object] = sync_related_notes_operation_result
111
+
112
+ def run(self, effect: WorkflowEffect, context: WorkflowEffectExecutionContext) -> WorkflowEffectResult:
113
+ del context
114
+ if effect.kind != WorkflowEffectKind.RUN_SUBWORKFLOW:
115
+ raise ValueError(f"RelatedNotesEffectAdapter cannot run effect kind: {effect.kind.value}")
116
+ match effect.target:
117
+ case "related_notes.export" | "related_notes_export":
118
+ return self._recover_export(effect)
119
+ case "related_notes.section":
120
+ return self._sync_section(effect)
121
+ case _:
122
+ raise ValueError(f"RelatedNotesEffectAdapter cannot run effect target: {effect.target}")
123
+
124
+ def _recover_export(self, effect: WorkflowEffect) -> WorkflowEffectResult:
125
+ try:
126
+ intent = RelatedNotesExportEffectPayload.from_effect_payload(effect.payload)
127
+ except PydanticValidationError as exc:
128
+ return _effect_payload_contract_block(
129
+ effect,
130
+ payload_schema="related_notes_export_effect_payload",
131
+ exc=exc,
132
+ operation_payload=_safe_operation_payload(effect.payload),
133
+ )
134
+ result = self.recover_related_notes_export_fn(
135
+ self.config,
136
+ export_path=_optional_path(intent.export_path),
137
+ mode=intent.mode or "auto",
138
+ workflow=effect.workflow,
139
+ run_id=effect.run_id,
140
+ )
141
+ try:
142
+ operation_payload = _json_object(result)
143
+ typed = RelatedNotesRecoveryEffectPayload.from_operation_payload(operation_payload)
144
+ except PydanticValidationError as exc:
145
+ return _effect_payload_contract_block(
146
+ effect,
147
+ payload_schema="related_notes_recovery_effect_payload",
148
+ exc=exc,
149
+ operation_payload=_safe_operation_payload(result),
150
+ )
151
+ recovery_state = typed.related_notes_recovery_state
152
+ reason = str(
153
+ (recovery_state.blocked_reason if recovery_state is not None else "") or typed.blocked_reason or ""
154
+ )
155
+ next_action = str(typed.next_action or (recovery_state.next_action if recovery_state is not None else "") or "")
156
+ if (
157
+ recovery_state is not None
158
+ and recovery_state.status == "waiting_for_retry"
159
+ and reason in RELATED_NOTES_EXTERNAL_RETRY_REASONS
160
+ ):
161
+ return WorkflowEffectResult(
162
+ effect=effect,
163
+ status=WorkflowEffectStatus.WAITING_EXTERNAL,
164
+ outcome=RelatedNotesQuotaWaitOutcome(reason_code=reason),
165
+ public_summary="Notas Relacionadas aguardando cota externa.",
166
+ developer_summary="Related Notes recovery paused with reusable progress.",
167
+ payload=typed.to_payload(),
168
+ next_action=next_action,
169
+ resume_action=next_action or effect.resume_action,
170
+ )
171
+ if typed.status in {"recovered", "completed"}:
172
+ if effect.requires_receipt and typed.receipt is None:
173
+ return _effect_payload_contract_block_from_message(
174
+ effect,
175
+ payload_schema="related_notes_recovery_effect_payload",
176
+ loc="receipt",
177
+ message="completed related notes recovery effect payload requires receipt",
178
+ operation_payload=typed.to_payload(),
179
+ )
180
+ return WorkflowEffectResult(
181
+ effect=effect,
182
+ status=WorkflowEffectStatus.COMPLETED,
183
+ outcome=RelatedNotesExportCompletedOutcome(),
184
+ public_summary="Export de Notas Relacionadas atualizado.",
185
+ developer_summary="Related Notes export recovery completed.",
186
+ payload=typed.to_payload(),
187
+ receipt=typed.receipt if effect.requires_receipt else None,
188
+ )
189
+ return WorkflowEffectResult(
190
+ effect=effect,
191
+ status=WorkflowEffectStatus.BLOCKED,
192
+ outcome=RelatedNotesBlockedOutcome(reason_code=reason or "related_notes_blocked"),
193
+ public_summary="Notas Relacionadas bloqueadas antes de alterar a Wiki.",
194
+ developer_summary="Related Notes recovery returned a blocking status.",
195
+ payload=typed.to_payload(),
196
+ next_action=next_action or "Corrigir o bloqueio de Related Notes pela rota oficial.",
197
+ error_context={"blocked_reason": reason or "related_notes_blocked"},
198
+ )
199
+
200
+ def _sync_section(self, effect: WorkflowEffect) -> WorkflowEffectResult:
201
+ try:
202
+ intent = RelatedNotesSyncSectionEffectPayload.from_effect_payload(effect.payload)
203
+ except PydanticValidationError as exc:
204
+ return _effect_payload_contract_block(
205
+ effect,
206
+ payload_schema="related_notes_sync_section_effect_payload",
207
+ exc=exc,
208
+ operation_payload=_safe_operation_payload(effect.payload),
209
+ )
210
+ result = self.sync_related_notes_fn(
211
+ self.config,
212
+ export_path=_optional_path(intent.export_path),
213
+ apply=intent.apply,
214
+ # Markdown .bak files are retired; vault guard/version control is
215
+ # the recovery mechanism for workflow-driven mutations.
216
+ backup=False,
217
+ receipt_path=_optional_path(intent.receipt_path),
218
+ min_score=float(intent.min_score),
219
+ max_links=intent.max_links,
220
+ max_age_hours=float(intent.max_age_hours),
221
+ )
222
+ try:
223
+ operation_payload = _json_object(result)
224
+ typed = RelatedNotesSyncEffectPayload.from_operation_payload(operation_payload)
225
+ except PydanticValidationError as exc:
226
+ return _effect_payload_contract_block(
227
+ effect,
228
+ payload_schema="related_notes_sync_effect_payload",
229
+ exc=exc,
230
+ operation_payload=_safe_operation_payload(result),
231
+ )
232
+ if typed.status == "completed":
233
+ safety = _version_control_safety_or_block(
234
+ effect,
235
+ typed.to_payload(),
236
+ changed_file_count=typed.applied_note_count,
237
+ outcome=RelatedNotesBlockedOutcome(reason_code="version_control_safety_evidence_missing"),
238
+ )
239
+ if isinstance(safety, WorkflowEffectResult):
240
+ return safety
241
+ return WorkflowEffectResult(
242
+ effect=effect,
243
+ status=WorkflowEffectStatus.COMPLETED,
244
+ outcome=RelatedNotesSyncCompletedOutcome(),
245
+ public_summary="Notas Relacionadas sincronizadas.",
246
+ developer_summary="Related Notes sync completed.",
247
+ payload=typed.to_payload(),
248
+ receipt=typed.receipt,
249
+ )
250
+ if typed.status in {"preview_ready", "completed_with_warnings"}:
251
+ return WorkflowEffectResult(
252
+ effect=effect,
253
+ status=WorkflowEffectStatus.COMPLETED_WITH_WARNINGS,
254
+ outcome=RelatedNotesSyncWarningOutcome(reason_code=str(typed.blocked_reason or typed.status)),
255
+ public_summary="Notas Relacionadas conferidas com aviso.",
256
+ developer_summary="Related Notes sync returned a non-mutating or warning result.",
257
+ payload=typed.to_payload(),
258
+ receipt=typed.receipt,
259
+ next_action=str(typed.next_action or "Revisar a previa de Notas Relacionadas."),
260
+ )
261
+ return WorkflowEffectResult(
262
+ effect=effect,
263
+ status=WorkflowEffectStatus.BLOCKED,
264
+ outcome=RelatedNotesBlockedOutcome(reason_code=str(typed.blocked_reason or "related_notes_blocked")),
265
+ public_summary="Notas Relacionadas nao foram sincronizadas.",
266
+ developer_summary="Related Notes sync returned blocked status.",
267
+ payload=typed.to_payload(),
268
+ next_action=str(typed.next_action or "Atualizar o export do Related Notes e repetir a rota oficial."),
269
+ error_context={"blocked_reason": str(typed.blocked_reason or "related_notes_blocked")},
270
+ )
271
+
272
+
273
+ def _optional_path(value: object) -> Path | None:
274
+ text = _text_or_empty(value).strip()
275
+ return Path(text) if text else None
276
+
277
+
278
+ def _version_control_safety_or_block(
279
+ effect: WorkflowEffect,
280
+ operation_payload: JsonObject,
281
+ *,
282
+ changed_file_count: int,
283
+ outcome: ContractModel,
284
+ ) -> VersionControlSafety | WorkflowEffectResult:
285
+ """Accept only real guard evidence or explicit non-mutation contracts.
286
+
287
+ Adapters must not fabricate safety from `mutates_resources`, apply mode or
288
+ counters. A mutation with changed files needs safety copied from an
289
+ operation payload or receipt produced by the guarded runtime.
290
+ """
291
+
292
+ safety = _version_control_safety_from_payload(operation_payload)
293
+ if safety is None:
294
+ if changed_file_count > 0 or effect.mutates_resources:
295
+ return _version_control_safety_evidence_missing(
296
+ effect,
297
+ operation_payload=operation_payload,
298
+ outcome=outcome,
299
+ )
300
+ return _no_resource_mutation_safety()
301
+ if safety.no_resource_mutation and changed_file_count > 0:
302
+ return _version_control_safety_evidence_missing(
303
+ effect,
304
+ operation_payload=operation_payload,
305
+ outcome=outcome,
306
+ )
307
+ if safety.changed_file_count != changed_file_count:
308
+ return _version_control_safety_evidence_mismatch(
309
+ effect,
310
+ operation_payload=operation_payload,
311
+ outcome=outcome,
312
+ expected_changed_file_count=changed_file_count,
313
+ evidence_changed_file_count=safety.changed_file_count,
314
+ )
315
+ return safety
316
+
317
+
318
+ def _version_control_safety_from_payload(payload: JsonObject) -> VersionControlSafety | None:
319
+ for candidate in (
320
+ _json_field(payload, "version_control_safety"),
321
+ _json_field(_json_object_or_empty(_json_field(payload, "receipt")), "version_control_safety"),
322
+ _json_field(_json_object_or_empty(_json_field(payload, "guard_receipt")), "version_control_safety"),
323
+ ):
324
+ if isinstance(candidate, dict):
325
+ return VersionControlSafety.model_validate(candidate)
326
+ return None
327
+
328
+
329
+ def _version_control_safety_evidence_missing(
330
+ effect: WorkflowEffect,
331
+ *,
332
+ operation_payload: JsonObject,
333
+ outcome: ContractModel,
334
+ ) -> WorkflowEffectResult:
335
+ return WorkflowEffectResult(
336
+ effect=effect,
337
+ status=WorkflowEffectStatus.BLOCKED,
338
+ outcome=outcome,
339
+ public_summary="A alteração foi bloqueada porque faltou comprovante de proteção do recurso.",
340
+ developer_summary="Mutating effect result did not carry typed version_control_safety evidence.",
341
+ payload={
342
+ "schema": "medical-notes-workbench.version-control-safety-error.v1",
343
+ "operation_payload": operation_payload,
344
+ },
345
+ next_action="Reexecutar pela rota oficial com guard/receipt de versionamento ativo.",
346
+ error_context={
347
+ "root_cause": "version_control_safety_evidence_missing",
348
+ "affected_artifact": effect.target,
349
+ "retry_scope": "effect_adapter",
350
+ },
351
+ )
352
+
353
+
354
+ def _version_control_safety_evidence_mismatch(
355
+ effect: WorkflowEffect,
356
+ *,
357
+ operation_payload: JsonObject,
358
+ outcome: ContractModel,
359
+ expected_changed_file_count: int,
360
+ evidence_changed_file_count: int,
361
+ ) -> WorkflowEffectResult:
362
+ return WorkflowEffectResult(
363
+ effect=effect,
364
+ status=WorkflowEffectStatus.BLOCKED,
365
+ outcome=outcome,
366
+ public_summary="A alteração foi bloqueada porque o comprovante de proteção não bate com o resultado.",
367
+ developer_summary="version_control_safety changed_file_count did not match the mutating effect result.",
368
+ payload={
369
+ "schema": "medical-notes-workbench.version-control-safety-error.v1",
370
+ "operation_payload": operation_payload,
371
+ "expected_changed_file_count": expected_changed_file_count,
372
+ "evidence_changed_file_count": evidence_changed_file_count,
373
+ },
374
+ next_action="Reexecutar pela rota oficial; se o mismatch repetir, tratar como bug de contrato do adapter.",
375
+ error_context={
376
+ "root_cause": "version_control_safety_evidence_mismatch",
377
+ "affected_artifact": effect.target,
378
+ "retry_scope": "effect_adapter",
379
+ "expected_changed_file_count": expected_changed_file_count,
380
+ "evidence_changed_file_count": evidence_changed_file_count,
381
+ },
382
+ )
383
+
384
+
385
+ def _no_resource_mutation_safety() -> VersionControlSafety:
386
+ return VersionControlSafety(
387
+ resource_guard_active=False,
388
+ run_start_seen=False,
389
+ run_finish_seen=False,
390
+ restore_point_before="",
391
+ restore_point_after="",
392
+ sync_status="not_checked",
393
+ backup_online="not_checked",
394
+ direct_mutation_forbidden=True,
395
+ mutation_without_guard=False,
396
+ rollback_declared=False,
397
+ no_resource_mutation=True,
398
+ changed_file_count=0,
399
+ )
400
+
401
+
402
+ def _link_changed_file_count(payload: JsonObject) -> int:
403
+ files_changed = _int_field(payload, "files_changed", 0)
404
+ changed_files = _json_field(payload, "changed_files", [])
405
+ if isinstance(changed_files, list):
406
+ return max(files_changed, len(changed_files))
407
+ return files_changed
408
+
409
+
410
+ def _child_effect_version_control_safety(
411
+ parent_safety: VersionControlSafety,
412
+ *,
413
+ changed_file_count: int,
414
+ mutates: bool,
415
+ ) -> VersionControlSafety:
416
+ """Project an already-active parent guard onto one child effect result.
417
+
418
+ The child runtime owns the mutation count; the parent safety owns evidence
419
+ that a rollback route/guard exists. This avoids accepting legacy receipts
420
+ while also avoiding a false mismatch when the pre-link guard was captured
421
+ before the link effect mutated files.
422
+ """
423
+
424
+ mutated = mutates or changed_file_count > 0
425
+ payload = parent_safety.to_payload()
426
+ payload["changed_file_count"] = changed_file_count
427
+ payload["no_resource_mutation"] = not mutated
428
+ payload["mutation_without_guard"] = bool(mutated and not parent_safety.rollback_declared)
429
+ return VersionControlSafety.model_validate(payload)
430
+
431
+
432
+ def _json_object_or_empty(value: JsonValue) -> JsonObject:
433
+ return _json_object(value) if isinstance(value, dict) else {}
434
+
435
+
436
+ @dataclass
437
+ class LinkWorkflowEffectAdapter:
438
+ config: object
439
+ run_linker_fn: Callable[..., object] = run_linker
440
+
441
+ def run(self, effect: WorkflowEffect, context: WorkflowEffectExecutionContext) -> WorkflowEffectResult:
442
+ del context
443
+ if effect.kind != WorkflowEffectKind.RUN_SUBWORKFLOW or effect.target not in {
444
+ "/mednotes:link",
445
+ "/mednotes:link-body",
446
+ }:
447
+ raise ValueError(f"LinkWorkflowEffectAdapter cannot run effect: {effect.kind.value}:{effect.target}")
448
+ try:
449
+ intent = LinkWorkflowRunEffectPayload.from_effect_payload(effect.payload)
450
+ except PydanticValidationError as exc:
451
+ return _effect_payload_contract_block(
452
+ effect,
453
+ payload_schema="link_workflow_run_effect_payload",
454
+ exc=exc,
455
+ operation_payload=_safe_operation_payload(effect.payload),
456
+ )
457
+ if intent.apply and not intent.diagnosis_path.strip():
458
+ return _effect_payload_contract_block_from_message(
459
+ effect,
460
+ payload_schema="link_workflow_run_effect_payload",
461
+ loc="diagnosis_path",
462
+ message="link workflow apply effects require diagnosis_path",
463
+ operation_payload=_safe_operation_payload(effect.payload),
464
+ )
465
+ mode: Literal["apply", "diagnose"] = "apply" if intent.apply else "diagnose"
466
+ include_related_notes = effect.target != "/mednotes:link-body" and not intent.no_related_notes
467
+ parent_safety = intent.version_control_safety
468
+ raw = self.run_linker_fn(
469
+ self.config,
470
+ diagnose=intent.diagnose,
471
+ apply=intent.apply,
472
+ diagnosis_path=_optional_path(intent.diagnosis_path),
473
+ receipt_path=_optional_path(intent.receipt_path),
474
+ trigger_context_path=_optional_path(intent.trigger_context_path),
475
+ include_related_notes=include_related_notes,
476
+ # The FSM effect contract no longer carries adjacent-backup policy.
477
+ backup=False,
478
+ force_diagnose=intent.force_diagnose,
479
+ llm_disambiguation=intent.llm_disambiguation or "auto",
480
+ llm_model=_optional_text(intent.llm_model),
481
+ llm_timeout=intent.llm_timeout,
482
+ version_control_guard_active=parent_safety is not None,
483
+ )
484
+ try:
485
+ operation_payload = _json_object(raw)
486
+ changed_file_count = _link_changed_file_count(operation_payload)
487
+ if parent_safety is not None and _json_field(operation_payload, "version_control_safety") == {}:
488
+ enriched = dict(operation_payload)
489
+ enriched["version_control_safety"] = _child_effect_version_control_safety(
490
+ parent_safety,
491
+ changed_file_count=changed_file_count,
492
+ mutates=effect.mutates_resources,
493
+ ).to_payload()
494
+ operation_payload = JsonObjectAdapter.validate_python(enriched)
495
+ if parent_safety is not None and _version_control_safety_from_payload(operation_payload) is None:
496
+ enriched = dict(operation_payload)
497
+ enriched["version_control_safety"] = _child_effect_version_control_safety(
498
+ parent_safety,
499
+ changed_file_count=changed_file_count,
500
+ mutates=effect.mutates_resources,
501
+ ).to_payload()
502
+ operation_payload = JsonObjectAdapter.validate_python(enriched)
503
+ safety = _version_control_safety_or_block(
504
+ effect,
505
+ operation_payload,
506
+ changed_file_count=changed_file_count,
507
+ outcome=LinkEffectBlockedOutcome(reason_code="version_control_safety_evidence_missing"),
508
+ )
509
+ if isinstance(safety, WorkflowEffectResult):
510
+ return safety
511
+ fsm_payload = _link_fsm_payload_from_raw(
512
+ effect=effect,
513
+ raw=operation_payload,
514
+ mode=mode,
515
+ include_related_notes=include_related_notes,
516
+ version_control_safety=safety,
517
+ )
518
+ fsm_payload = _attach_link_operation_report_details(fsm_payload, operation_payload)
519
+ typed = LinkSubworkflowEffectPayload.model_validate(
520
+ {
521
+ "schema": _json_field(fsm_payload, "schema"),
522
+ "progress_view_model": _json_field(fsm_payload, "progress_view_model"),
523
+ "receipt": _json_field(fsm_payload, "receipt"),
524
+ "reports": _json_field(fsm_payload, "reports"),
525
+ "error_context": _json_field(fsm_payload, "error_context", {}),
526
+ "fsm_payload": fsm_payload,
527
+ }
528
+ )
529
+ except PydanticValidationError as exc:
530
+ return _effect_payload_contract_block(
531
+ effect,
532
+ payload_schema="link_subworkflow_effect_payload",
533
+ exc=exc,
534
+ operation_payload=_safe_operation_payload(raw),
535
+ )
536
+ except ValueError as exc:
537
+ return _effect_payload_contract_block_from_message(
538
+ effect,
539
+ payload_schema="link_subworkflow_effect_payload",
540
+ loc="$",
541
+ message=str(exc),
542
+ operation_payload=_safe_operation_payload(raw),
543
+ )
544
+ status = typed.progress_view_model.status.value
545
+ effect_status = _effect_status_from_progress_status(status)
546
+ reason_code = _link_effect_reason_code(typed.error_context, status=status)
547
+ error_context = dict(typed.error_context)
548
+ if effect_status in {
549
+ WorkflowEffectStatus.BLOCKED,
550
+ WorkflowEffectStatus.FAILED,
551
+ WorkflowEffectStatus.WAITING_EXTERNAL,
552
+ WorkflowEffectStatus.WAITING_HUMAN,
553
+ } and reason_code:
554
+ error_context.setdefault("blocked_reason", reason_code)
555
+ error_context.setdefault("root_cause", reason_code)
556
+ report_summary = _json_field(typed.reports, "summary")
557
+ return WorkflowEffectResult(
558
+ effect=effect,
559
+ status=effect_status,
560
+ outcome=_link_outcome_for_effect_status(effect_status, reason_code=reason_code or status),
561
+ public_summary=_text_or_empty(report_summary),
562
+ developer_summary="Link adapter returned an FSM-first payload.",
563
+ payload=typed.fsm_payload,
564
+ receipt=typed.receipt.to_payload() if effect.requires_receipt else None,
565
+ next_action=_text_or_empty(typed.receipt.next_action),
566
+ resume_action=_text_or_empty(typed.progress_view_model.resume_action),
567
+ error_context=error_context,
568
+ )
569
+
570
+
571
+ @dataclass
572
+ class WikiSubworkflowEffectAdapter:
573
+ """Route generic subworkflow effects to the correct Wiki-domain adapter.
574
+
575
+ `WorkflowEffectKind` stays domain-agnostic in the kernel. Wiki-specific
576
+ work such as Related Notes is selected here from `target` plus the typed
577
+ payload, so the FSM keeps one clean executable effect vocabulary.
578
+ """
579
+
580
+ link_adapter: LinkWorkflowEffectAdapter
581
+ related_notes_adapter: RelatedNotesEffectAdapter
582
+
583
+ def run(self, effect: WorkflowEffect, context: WorkflowEffectExecutionContext) -> WorkflowEffectResult:
584
+ if effect.kind != WorkflowEffectKind.RUN_SUBWORKFLOW:
585
+ raise ValueError(f"WikiSubworkflowEffectAdapter cannot run effect kind: {effect.kind.value}")
586
+ if effect.target in {"/mednotes:link", "/mednotes:link-body"}:
587
+ return self.link_adapter.run(effect, context)
588
+ if effect.target in {"related_notes.export", "related_notes_export", "related_notes.section"}:
589
+ return self.related_notes_adapter.run(effect, context)
590
+ raise ValueError(f"WikiSubworkflowEffectAdapter cannot route effect target: {effect.target}")
591
+
592
+
593
+ def _link_fsm_payload_from_raw(
594
+ *,
595
+ effect: WorkflowEffect,
596
+ raw: JsonObject,
597
+ mode: Literal["apply", "diagnose"],
598
+ include_related_notes: bool,
599
+ version_control_safety: VersionControlSafety,
600
+ ) -> JsonObject:
601
+ from mednotes.domains.wiki.flows.link.link_fsm import build_link_fsm_result
602
+ from mednotes.domains.wiki.flows.link.link_runtime_result import link_fsm_facts_from_linker_result
603
+
604
+ result = build_link_fsm_result(
605
+ link_fsm_facts_from_linker_result(
606
+ raw,
607
+ run_id=f"link-{effect.run_id}",
608
+ mode=mode,
609
+ include_related_notes=include_related_notes,
610
+ version_control_safety=version_control_safety,
611
+ )
612
+ ).to_payload()
613
+ return _json_object(result)
614
+
615
+
616
+ def _attach_link_operation_report_details(fsm_payload: JsonObject, operation_payload: JsonObject) -> JsonObject:
617
+ """Expose compact child-operation facts inside the child FSM report.
618
+
619
+ Parent workflows still consume `link-fsm-result.v1` as the only operational
620
+ artifact. These details are a typed adapter projection from the raw linker
621
+ result, kept under `reports.details` so callers do not need to parse the
622
+ legacy `link-run.v1` receipt as workflow truth.
623
+ """
624
+
625
+ details: dict[str, object] = {}
626
+ for key in (
627
+ "related_notes_sync",
628
+ "body_term_linker",
629
+ "reference_repair",
630
+ "reference_repair_apply",
631
+ "graph_audit_before",
632
+ "graph_audit_after",
633
+ "blockers",
634
+ ):
635
+ value = _json_field(operation_payload, key)
636
+ if isinstance(value, dict):
637
+ details[key] = _json_object(value)
638
+ elif isinstance(value, list):
639
+ details[key] = list(value)
640
+ for key in (
641
+ "status",
642
+ "phase",
643
+ "blocked_reason",
644
+ "next_action",
645
+ "returncode",
646
+ "diagnosis_path",
647
+ "receipt_path",
648
+ "files_changed",
649
+ "changed_file_count",
650
+ "blocker_count",
651
+ "links_planned",
652
+ "links_rewritten",
653
+ ):
654
+ value = _json_field(operation_payload, key)
655
+ if value is not None:
656
+ details[key] = value
657
+ changed_files = _json_field(operation_payload, "changed_files")
658
+ if isinstance(changed_files, list):
659
+ details["changed_files"] = list(changed_files)
660
+
661
+ reports = _json_object_or_empty(_json_field(fsm_payload, "reports"))
662
+ existing_details = _json_object_or_empty(_json_field(reports, "details"))
663
+ reports["details"] = JsonObjectAdapter.validate_python({**existing_details, **details})
664
+ return JsonObjectAdapter.validate_python({**fsm_payload, "reports": reports})
665
+
666
+
667
+ def _effect_status_from_progress_status(status: str) -> WorkflowEffectStatus:
668
+ match status:
669
+ case "completed":
670
+ return WorkflowEffectStatus.COMPLETED
671
+ case "completed_with_warnings":
672
+ return WorkflowEffectStatus.COMPLETED_WITH_WARNINGS
673
+ case "completed_with_link_blockers":
674
+ return WorkflowEffectStatus.BLOCKED
675
+ case "waiting_external":
676
+ return WorkflowEffectStatus.WAITING_EXTERNAL
677
+ case "waiting_human":
678
+ return WorkflowEffectStatus.WAITING_HUMAN
679
+ case "blocked":
680
+ return WorkflowEffectStatus.BLOCKED
681
+ case "failed":
682
+ return WorkflowEffectStatus.FAILED
683
+ case _:
684
+ return WorkflowEffectStatus.FAILED
685
+
686
+
687
+ def _link_effect_reason_code(error_context: JsonObject, *, status: str) -> str:
688
+ """Preserve linker blocker semantics when converting FSM payloads to effects."""
689
+
690
+ for value in (
691
+ _json_field(error_context, "blocked_reason"),
692
+ _json_field(error_context, "root_cause"),
693
+ ):
694
+ text = _text_or_empty(value)
695
+ if text:
696
+ return text
697
+ if status == "completed_with_link_blockers":
698
+ return "link_plan_blocked"
699
+ return ""
700
+
701
+
702
+ def _link_outcome_for_effect_status(status: WorkflowEffectStatus, *, reason_code: str = "") -> ContractModel:
703
+ """Map link FSM status to link-domain outcomes, not generic status buckets."""
704
+
705
+ match status:
706
+ case WorkflowEffectStatus.COMPLETED:
707
+ return LinkEffectCompletedOutcome()
708
+ case WorkflowEffectStatus.COMPLETED_WITH_WARNINGS:
709
+ return LinkEffectLinkerBlockedOutcome(reason_code=reason_code)
710
+ case WorkflowEffectStatus.WAITING_EXTERNAL:
711
+ return LinkEffectBlockedOutcome(reason_code=reason_code)
712
+ case WorkflowEffectStatus.BLOCKED:
713
+ if reason_code in {"graph_blocked", "graph_blockers", "link_plan_blocked"}:
714
+ return LinkEffectGraphBlockedOutcome()
715
+ if reason_code in {"linker_blocked", "body_term_linker", "related_notes_blocked"}:
716
+ return LinkEffectLinkerBlockedOutcome(reason_code=reason_code)
717
+ return LinkEffectBlockedOutcome(reason_code=reason_code)
718
+ case WorkflowEffectStatus.FAILED:
719
+ return LinkEffectFailedOutcome(reason_code=reason_code)
720
+ case WorkflowEffectStatus.SKIPPED:
721
+ return LinkEffectBlockedOutcome(reason_code=reason_code or "skipped")
722
+ case _:
723
+ return LinkEffectFailedOutcome(reason_code=reason_code or status.value)
724
+
725
+
726
+ @dataclass
727
+ class SpecialistModelEffectAdapter:
728
+ runner: Callable[[WorkflowEffect], object] | None = None
729
+
730
+ def run(self, effect: WorkflowEffect, context: WorkflowEffectExecutionContext) -> WorkflowEffectResult:
731
+ del context
732
+ if effect.kind != WorkflowEffectKind.CALL_SPECIALIST_MODEL:
733
+ raise ValueError(f"SpecialistModelEffectAdapter cannot run effect kind: {effect.kind.value}")
734
+ if self.runner is None:
735
+ return WorkflowEffectResult(
736
+ effect=effect,
737
+ status=WorkflowEffectStatus.WAITING_AGENT,
738
+ outcome=SpecialistModelBlockedOutcome(reason_code="specialist_agent_required"),
739
+ public_summary="Aguardando modelo especializado antes de continuar.",
740
+ developer_summary=(
741
+ "No Python runner is available; the agent must execute the specialist effect through "
742
+ "agent_directive.control instead of treating this as external quota."
743
+ ),
744
+ payload={"model_policy": dict(effect.model_policy)},
745
+ next_action="Executar a chamada ao especialista pela rota oficial do agente.",
746
+ )
747
+ raw_result = self.runner(effect)
748
+ if not isinstance(raw_result, dict):
749
+ return _effect_payload_contract_block_from_message(
750
+ effect,
751
+ payload_schema="specialist_model_effect_payload",
752
+ loc="$",
753
+ message=f"specialist runner returned {type(raw_result).__name__}, expected object",
754
+ operation_payload={"raw_result_type": type(raw_result).__name__},
755
+ )
756
+ try:
757
+ operation_payload = _json_object(raw_result)
758
+ result = SpecialistModelEffectPayload.from_operation_payload(operation_payload)
759
+ except PydanticValidationError as exc:
760
+ return _effect_payload_contract_block(
761
+ effect,
762
+ payload_schema="specialist_model_effect_payload",
763
+ exc=exc,
764
+ operation_payload=_safe_operation_payload(raw_result),
765
+ )
766
+ if result.status == "waiting_external":
767
+ reason = str(result.blocked_reason or result.status)
768
+ next_action = str(result.next_action or "Aguardar capacidade do modelo especialista e retomar pela rota oficial.")
769
+ return WorkflowEffectResult(
770
+ effect=effect,
771
+ status=WorkflowEffectStatus.WAITING_EXTERNAL,
772
+ outcome=SpecialistModelCapacityWaitOutcome(reason_code=reason),
773
+ public_summary="Modelo especialista aguardando capacidade externa.",
774
+ developer_summary="Specialist runner returned a resumable external wait.",
775
+ payload=result.to_payload(),
776
+ next_action=next_action,
777
+ resume_action=next_action,
778
+ error_context={
779
+ "blocked_reason": reason,
780
+ "root_cause": reason,
781
+ "required_inputs": list(result.required_inputs),
782
+ },
783
+ )
784
+ if result.status != "completed":
785
+ reason = str(result.blocked_reason or result.status or "blocked")
786
+ return WorkflowEffectResult(
787
+ effect=effect,
788
+ status=WorkflowEffectStatus.BLOCKED,
789
+ outcome=SpecialistModelBlockedOutcome(reason_code=reason),
790
+ public_summary="A chamada ao especialista nao produziu saida aplicavel.",
791
+ developer_summary="Specialist runner returned non-completed status.",
792
+ payload=result.to_payload(),
793
+ next_action=str(result.next_action or "Repetir a chamada ao especialista pela rota oficial."),
794
+ error_context={"status": str(result.status or "blocked"), "blocked_reason": reason},
795
+ )
796
+ receipt = result.receipt
797
+ if receipt is None:
798
+ return _effect_payload_contract_block_from_message(
799
+ effect,
800
+ payload_schema="specialist_model_effect_payload",
801
+ loc="receipt",
802
+ message="completed specialist model effect payload requires typed receipt",
803
+ operation_payload=result.to_payload(),
804
+ )
805
+ try:
806
+ raw_receipt_payload = _json_object(_json_field(result.operation_payload, "receipt"))
807
+ validate_specialist_task_run_receipt_attestation(raw_receipt_payload)
808
+ except PydanticValidationError as exc:
809
+ return _effect_payload_contract_block(
810
+ effect,
811
+ payload_schema="specialist_model_effect_payload",
812
+ exc=exc,
813
+ operation_payload=result.to_payload(),
814
+ )
815
+ except ValidationError as exc:
816
+ return _effect_payload_contract_block_from_message(
817
+ effect,
818
+ payload_schema="specialist_model_effect_payload",
819
+ loc="receipt.receipt_attestation",
820
+ message=str(exc),
821
+ operation_payload=result.to_payload(),
822
+ )
823
+ if receipt.specialist_output_attestation is None:
824
+ return _effect_payload_contract_block_from_message(
825
+ effect,
826
+ payload_schema="specialist_model_effect_payload",
827
+ loc="receipt.specialist_output_attestation",
828
+ message="completed specialist model effect payload requires receipt attestation",
829
+ operation_payload=result.to_payload(),
830
+ )
831
+ receipt_payload = receipt.to_payload()
832
+ attestation_payload = receipt.specialist_output_attestation.to_payload()
833
+ return WorkflowEffectResult(
834
+ effect=effect,
835
+ status=WorkflowEffectStatus.COMPLETED,
836
+ outcome=SpecialistModelCompletedOutcome(),
837
+ public_summary="Saida especializada recebida e validada.",
838
+ developer_summary="Specialist runner returned a typed specialist task receipt and attestation.",
839
+ payload={**result.payload, "specialist_task_run_receipt": receipt_payload},
840
+ receipt=receipt_payload,
841
+ attestation=attestation_payload,
842
+ )
843
+
844
+
845
+ def _effect_payload_contract_block(
846
+ effect: WorkflowEffect,
847
+ *,
848
+ payload_schema: str,
849
+ exc: PydanticValidationError,
850
+ operation_payload: JsonObject,
851
+ outcome: ContractModel | None = None,
852
+ ) -> WorkflowEffectResult:
853
+ first = exc.errors()[0] if exc.errors() else {}
854
+ loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
855
+ msg = str(first.get("msg") or str(exc))
856
+ return WorkflowEffectResult(
857
+ effect=effect,
858
+ status=WorkflowEffectStatus.BLOCKED,
859
+ outcome=outcome or LinkEffectBlockedOutcome(reason_code="effect_payload_contract_invalid"),
860
+ public_summary="O resultado interno do workflow falhou na validação antes de continuar.",
861
+ developer_summary=f"{payload_schema} invalid at {loc}: {msg}",
862
+ payload={
863
+ "schema": "medical-notes-workbench.workflow-effect-payload-contract-error.v1",
864
+ "payload_schema": payload_schema,
865
+ "operation_payload": operation_payload,
866
+ },
867
+ next_action="Corrigir o contrato tipado do efeito e repetir pela rota oficial.",
868
+ error_context={
869
+ "root_cause": "effect_payload_contract_invalid",
870
+ "payload_schema": payload_schema,
871
+ "contract_error": {"loc": loc, "message": msg},
872
+ },
873
+ )
874
+
875
+
876
+ def _effect_payload_contract_block_from_message(
877
+ effect: WorkflowEffect,
878
+ *,
879
+ payload_schema: str,
880
+ loc: str,
881
+ message: str,
882
+ operation_payload: JsonObject,
883
+ outcome: ContractModel | None = None,
884
+ ) -> WorkflowEffectResult:
885
+ return WorkflowEffectResult(
886
+ effect=effect,
887
+ status=WorkflowEffectStatus.BLOCKED,
888
+ outcome=outcome or LinkEffectBlockedOutcome(reason_code="effect_payload_contract_invalid"),
889
+ public_summary="O resultado interno do workflow falhou na validação antes de continuar.",
890
+ developer_summary=f"{payload_schema} invalid at {loc}: {message}",
891
+ payload={
892
+ "schema": "medical-notes-workbench.workflow-effect-payload-contract-error.v1",
893
+ "payload_schema": payload_schema,
894
+ "operation_payload": operation_payload,
895
+ },
896
+ next_action="Corrigir o contrato tipado do efeito e repetir pela rota oficial.",
897
+ error_context={
898
+ "root_cause": "effect_payload_contract_invalid",
899
+ "payload_schema": payload_schema,
900
+ "contract_error": {"loc": loc, "message": message},
901
+ },
902
+ )
903
+
904
+
905
+ def _json_object(payload: object) -> JsonObject:
906
+ return JsonObjectAdapter.validate_python(payload)
907
+
908
+
909
+ def _safe_operation_payload(payload: object) -> JsonObject:
910
+ try:
911
+ return _json_object(payload)
912
+ except PydanticValidationError:
913
+ return {"raw_result_type": type(payload).__name__}
914
+
915
+
916
+ def _json_field(source: JsonObject, key: str, default: JsonValue = None) -> JsonValue:
917
+ return source[key] if key in source else default
918
+
919
+
920
+ def _optional_text(value: JsonValue) -> str | None:
921
+ text = _text_or_empty(value).strip()
922
+ return text or None
923
+
924
+
925
+ def _text_or_empty(value: object) -> str:
926
+ if value is None:
927
+ return ""
928
+ if isinstance(value, str):
929
+ return value
930
+ return f"{value}"
931
+
932
+
933
+ def _float_field(source: JsonObject, key: str, default: float) -> float:
934
+ value = _json_field(source, key, default)
935
+ if isinstance(value, int | float) and not isinstance(value, bool):
936
+ return float(value)
937
+ if isinstance(value, str):
938
+ try:
939
+ return float(value)
940
+ except ValueError:
941
+ return default
942
+ return default
943
+
944
+
945
+ def _int_field(source: JsonObject, key: str, default: int) -> int:
946
+ value = _json_field(source, key, default)
947
+ if isinstance(value, int) and not isinstance(value, bool):
948
+ return value
949
+ return default