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,1106 @@
1
+ """Operational StateChart for `/mednotes:link`.
2
+
3
+ This is the target FSM for the link package. It intentionally uses operational
4
+ leaf states for stale diagnosis, vocabulary bootstrap, agent work, external
5
+ quota and human apply confirmation so the public state does not collapse into
6
+ `status + blocked_reason` from the historical linker report.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from enum import StrEnum
12
+ from typing import Annotated, Literal
13
+
14
+ from pydantic import Field, TypeAdapter, field_validator, model_validator
15
+ from statemachine import StateChart
16
+ from statemachine.states import States
17
+
18
+ from mednotes.domains.wiki.contracts.effect_payloads import (
19
+ LinkWorkflowRunEffectPayload,
20
+ LinkWorkflowRunKind,
21
+ RelatedNotesExportEffectPayload,
22
+ RelatedNotesRecoveryStateEffectPayload,
23
+ WaitExternalEffectPayload,
24
+ )
25
+ from mednotes.kernel.base import ContractModel, JsonObject
26
+ from mednotes.kernel.effect_intent import WorkflowEffect, WorkflowEffectKind
27
+ from mednotes.kernel.fsm_model import WorkflowModel
28
+ from mednotes.kernel.fsm_transition_result import WorkflowTransitionResult
29
+ from mednotes.kernel.state_machine import WorkflowStateCategory
30
+ from mednotes.kernel.workflow import (
31
+ DecisionEvidence,
32
+ HumanDecisionOption,
33
+ HumanDecisionPacket,
34
+ RejectedAutomation,
35
+ WorkflowDecision,
36
+ )
37
+
38
+ LINK_WORKFLOW: Literal["/mednotes:link"] = "/mednotes:link"
39
+ LINK_BODY_WORKFLOW: Literal["/mednotes:link-body"] = "/mednotes:link-body"
40
+ LINK_PUBLIC_WORKFLOWS = frozenset({LINK_WORKFLOW, LINK_BODY_WORKFLOW})
41
+ LINK_BODY_FORBIDDEN_EVENT_NAMES = frozenset(
42
+ {
43
+ "vocabulary_bootstrap_required",
44
+ "vocabulary_bootstrap_completed",
45
+ "related_notes_planned",
46
+ "related_notes_apply_requested",
47
+ "vocabulary_semantic_repair_planned",
48
+ "vocabulary_semantic_repair_apply_requested",
49
+ "vocabulary_curator_required",
50
+ "vocabulary_curator_completed",
51
+ "related_notes_export_recovered",
52
+ "related_notes_quota_wait",
53
+ "related_notes_quota_ready",
54
+ "related_notes_applied",
55
+ "vocabulary_semantic_repair_applied",
56
+ }
57
+ )
58
+
59
+
60
+ class LinkMode(StrEnum):
61
+ FULL = "full"
62
+ BODY_ONLY = "body_only"
63
+
64
+
65
+ class LinkState(StrEnum):
66
+ CHECKING_TRIGGER_CONTEXT = "checking_trigger_context"
67
+ DIAGNOSING_GRAPH = "diagnosing_graph"
68
+ VOCABULARY_BOOTSTRAP_REQUIRED = "vocabulary_bootstrap_required"
69
+ PLANNING_BODY_LINKS = "planning_body_links"
70
+ PLANNING_RELATED_NOTES = "planning_related_notes"
71
+ PLANNING_VOCABULARY_SEMANTIC_REPAIR = "planning_vocabulary_semantic_repair"
72
+ WAITING_AGENT_DISAMBIGUATION = "waiting_agent_disambiguation"
73
+ WAITING_AGENT_RELATED_NOTES_EXPORT_RECOVERY = "waiting_agent_related_notes_export_recovery"
74
+ WAITING_AGENT_VOCABULARY_CURATOR = "waiting_agent_vocabulary_curator"
75
+ WAITING_EXTERNAL_RELATED_NOTES_QUOTA = "waiting_external_related_notes_quota"
76
+ WAITING_HUMAN_CONFIRMATION = "waiting_human_confirmation"
77
+ APPLYING_BODY_LINKS = "applying_body_links"
78
+ APPLYING_RELATED_NOTES = "applying_related_notes"
79
+ APPLYING_VOCABULARY_SEMANTIC_REPAIR = "applying_vocabulary_semantic_repair"
80
+ STALE_DIAGNOSIS = "stale_diagnosis"
81
+ APPLY_CANCELLED = "apply_cancelled"
82
+ GRAPH_DIAGNOSIS_BLOCKED = "graph_diagnosis_blocked"
83
+ BODY_LINKS_BLOCKED = "body_links_blocked"
84
+ RELATED_NOTES_BLOCKED = "related_notes_blocked"
85
+ VOCABULARY_SEMANTIC_REPAIR_BLOCKED = "vocabulary_semantic_repair_blocked"
86
+ COMPLETED = "completed"
87
+ COMPLETED_WITH_LINK_BLOCKERS = "completed_with_link_blockers"
88
+ FAILED = "failed"
89
+
90
+
91
+ class LinkEvent(ContractModel):
92
+ """Base event accepted by the link StateChart."""
93
+
94
+ workflow: str = LINK_WORKFLOW
95
+ run_id: str = Field(min_length=1)
96
+ current_state: str = Field(min_length=1)
97
+ audit_evidence: JsonObject = Field(default_factory=dict)
98
+
99
+ @field_validator("workflow")
100
+ @classmethod
101
+ def _workflow_must_be_link(cls, value: str) -> str:
102
+ if value not in LINK_PUBLIC_WORKFLOWS:
103
+ raise ValueError(f"link event workflow must be one of {sorted(LINK_PUBLIC_WORKFLOWS)}")
104
+ return value
105
+
106
+ @model_validator(mode="after")
107
+ def _body_only_rejects_full_package_events(self) -> LinkEvent:
108
+ event_name = _event_name(self)
109
+ if self.workflow == LINK_BODY_WORKFLOW and event_name in LINK_BODY_FORBIDDEN_EVENT_NAMES:
110
+ raise ValueError(f"{event_name} is not valid for {LINK_BODY_WORKFLOW}")
111
+ return self
112
+
113
+
114
+ def _event_name(event: LinkEvent) -> str:
115
+ """Return the concrete Literal discriminator declared by each event class."""
116
+
117
+ name = getattr(event, "name", "")
118
+ if not isinstance(name, str) or not name.strip():
119
+ raise ValueError("link events must declare a name discriminator")
120
+ return name
121
+
122
+
123
+ class TriggerContextReadyEvent(LinkEvent):
124
+ name: Literal["trigger_context_ready"] = "trigger_context_ready"
125
+ trigger_context_path: str = Field(min_length=1)
126
+
127
+
128
+ class StaleDiagnosisEvent(LinkEvent):
129
+ name: Literal["stale_diagnosis"] = "stale_diagnosis"
130
+ reason_code: Literal["stale_diagnosis"]
131
+
132
+
133
+ class DiagnosisRefreshRequestedEvent(LinkEvent):
134
+ name: Literal["diagnosis_refresh_requested"] = "diagnosis_refresh_requested"
135
+ reason_code: Literal["stale_diagnosis"] = "stale_diagnosis"
136
+
137
+
138
+ class DiagnosisCleanEvent(LinkEvent):
139
+ name: Literal["diagnosis_clean"] = "diagnosis_clean"
140
+ changed_file_count: int = Field(default=0, ge=0, strict=True)
141
+
142
+
143
+ class DiagnosisCompletedWithLinkBlockersEvent(LinkEvent):
144
+ name: Literal["diagnosis_completed_with_link_blockers"] = "diagnosis_completed_with_link_blockers"
145
+ blocker_count: int = Field(ge=1, strict=True)
146
+
147
+
148
+ class LinkRuntimeObservation(ContractModel):
149
+ """Adapter facts observed at the boundary; guards decide the leaf state."""
150
+
151
+ mode: LinkMode = LinkMode.FULL
152
+ operation: Literal["diagnose", "apply"]
153
+ failed: bool = False
154
+ stale_diagnosis: bool = False
155
+ changed_file_count: int = Field(default=0, ge=0, strict=True)
156
+ planned_link_count: int = Field(default=0, ge=0, strict=True)
157
+ rewritten_link_count: int = Field(default=0, ge=0, strict=True)
158
+ blocker_count: int = Field(default=0, ge=0, strict=True)
159
+ body_linker_blocked: bool = False
160
+ body_linker_blocked_reason: str = ""
161
+ related_notes_present: bool = False
162
+ related_notes_blocked: bool = False
163
+ related_notes_export_recovery_required: bool = False
164
+ related_notes_export_recovery_reason: str = ""
165
+ related_notes_waiting_external: bool = False
166
+ related_notes_applied: bool = False
167
+ vocabulary_bootstrap_required: bool = False
168
+ vocabulary_curator_required: bool = False
169
+ vocabulary_db_path: str = ""
170
+ vocabulary_curator_batch_plan_path: str = ""
171
+ vocabulary_curator_work_item_count: int = Field(default=1, ge=1, strict=True)
172
+ next_action: str = ""
173
+ reason_code: str = ""
174
+ related_notes_recovery_state: RelatedNotesRecoveryStateEffectPayload = Field(
175
+ default_factory=RelatedNotesRecoveryStateEffectPayload
176
+ )
177
+
178
+
179
+ class LinkRuntimeObservedEvent(LinkEvent):
180
+ """Single runtime observation event; LinkMachine owns outcome priority."""
181
+
182
+ name: Literal["runtime_observed"] = "runtime_observed"
183
+ observation: LinkRuntimeObservation
184
+
185
+
186
+ class VocabularyBootstrapRequiredEvent(LinkEvent):
187
+ name: Literal["vocabulary_bootstrap_required"] = "vocabulary_bootstrap_required"
188
+ db_path: str = Field(min_length=1)
189
+
190
+
191
+ class VocabularyBootstrapCompletedEvent(LinkEvent):
192
+ name: Literal["vocabulary_bootstrap_completed"] = "vocabulary_bootstrap_completed"
193
+ db_path: str = Field(min_length=1)
194
+
195
+
196
+ class BodyLinksPlannedEvent(LinkEvent):
197
+ name: Literal["body_links_planned"] = "body_links_planned"
198
+ planned_link_count: int = Field(ge=0, strict=True)
199
+
200
+
201
+ class BodyLinksReadyForConfirmationEvent(LinkEvent):
202
+ name: Literal["body_links_ready_for_confirmation"] = "body_links_ready_for_confirmation"
203
+ planned_link_count: int = Field(ge=0, strict=True)
204
+
205
+
206
+ class RelatedNotesPlannedEvent(LinkEvent):
207
+ name: Literal["related_notes_planned"] = "related_notes_planned"
208
+ planned_note_count: int = Field(ge=0, strict=True)
209
+
210
+
211
+ class RelatedNotesApplyRequestedEvent(LinkEvent):
212
+ name: Literal["related_notes_apply_requested"] = "related_notes_apply_requested"
213
+ planned_note_count: int = Field(ge=0, strict=True)
214
+
215
+
216
+ class VocabularySemanticRepairPlannedEvent(LinkEvent):
217
+ name: Literal["vocabulary_semantic_repair_planned"] = "vocabulary_semantic_repair_planned"
218
+ planned_repair_count: int = Field(ge=0, strict=True)
219
+
220
+
221
+ class VocabularySemanticRepairApplyRequestedEvent(LinkEvent):
222
+ name: Literal["vocabulary_semantic_repair_apply_requested"] = "vocabulary_semantic_repair_apply_requested"
223
+ planned_repair_count: int = Field(ge=0, strict=True)
224
+
225
+
226
+ class AgentDisambiguationRequiredEvent(LinkEvent):
227
+ name: Literal["agent_disambiguation_required"] = "agent_disambiguation_required"
228
+ ambiguous_alias_count: int = Field(ge=1, strict=True)
229
+
230
+
231
+ class AgentDisambiguationCompletedEvent(LinkEvent):
232
+ name: Literal["agent_disambiguation_completed"] = "agent_disambiguation_completed"
233
+ summary: str = Field(min_length=1)
234
+
235
+
236
+ class VocabularyCuratorRequiredEvent(LinkEvent):
237
+ name: Literal["vocabulary_curator_required"] = "vocabulary_curator_required"
238
+ work_item_count: int = Field(ge=1, strict=True)
239
+ batch_plan_path: str = ""
240
+
241
+
242
+ class VocabularyCuratorCompletedEvent(LinkEvent):
243
+ name: Literal["vocabulary_curator_completed"] = "vocabulary_curator_completed"
244
+ summary: str = Field(min_length=1)
245
+
246
+
247
+ class RelatedNotesQuotaWaitEvent(LinkEvent):
248
+ name: Literal["related_notes_quota_wait"] = "related_notes_quota_wait"
249
+ resume_action: str = Field(min_length=1)
250
+ related_notes_recovery_state: RelatedNotesRecoveryStateEffectPayload = Field(
251
+ default_factory=RelatedNotesRecoveryStateEffectPayload
252
+ )
253
+
254
+ @field_validator("related_notes_recovery_state", mode="before")
255
+ @classmethod
256
+ def _coerce_related_notes_recovery_state(cls, value: object) -> RelatedNotesRecoveryStateEffectPayload:
257
+ return RelatedNotesRecoveryStateEffectPayload.from_payload(value)
258
+
259
+
260
+ class RelatedNotesExportRecoveredEvent(LinkEvent):
261
+ name: Literal["related_notes_export_recovered"] = "related_notes_export_recovered"
262
+ summary: str = Field(min_length=1)
263
+
264
+
265
+ class RelatedNotesQuotaReadyEvent(LinkEvent):
266
+ name: Literal["related_notes_quota_ready"] = "related_notes_quota_ready"
267
+ restored_by: str = Field(min_length=1)
268
+
269
+
270
+ class HumanApplyApprovedEvent(LinkEvent):
271
+ name: Literal["human_apply_approved"] = "human_apply_approved"
272
+ approved_by: str = Field(min_length=1)
273
+
274
+
275
+ class HumanApplyCancelledEvent(LinkEvent):
276
+ name: Literal["human_apply_cancelled"] = "human_apply_cancelled"
277
+ cancelled_by: str = Field(min_length=1)
278
+
279
+
280
+ class BodyLinksAppliedEvent(LinkEvent):
281
+ name: Literal["body_links_applied"] = "body_links_applied"
282
+ changed_file_count: int = Field(ge=0, strict=True)
283
+
284
+
285
+ class RelatedNotesAppliedEvent(LinkEvent):
286
+ name: Literal["related_notes_applied"] = "related_notes_applied"
287
+ changed_file_count: int = Field(ge=0, strict=True)
288
+
289
+
290
+ class VocabularySemanticRepairAppliedEvent(LinkEvent):
291
+ name: Literal["vocabulary_semantic_repair_applied"] = "vocabulary_semantic_repair_applied"
292
+ changed_file_count: int = Field(ge=0, strict=True)
293
+
294
+
295
+ class LinkBlockedEvent(LinkEvent):
296
+ name: Literal["link_blocked"] = "link_blocked"
297
+ reason_code: str = Field(min_length=1)
298
+ next_action: str = Field(min_length=1)
299
+
300
+
301
+ class LinkFailedEvent(LinkEvent):
302
+ name: Literal["link_failed"] = "link_failed"
303
+ reason_code: str = Field(min_length=1)
304
+ next_action: str = Field(min_length=1)
305
+
306
+
307
+ LinkBoundaryEvent = Annotated[
308
+ TriggerContextReadyEvent
309
+ | StaleDiagnosisEvent
310
+ | DiagnosisRefreshRequestedEvent
311
+ | DiagnosisCleanEvent
312
+ | DiagnosisCompletedWithLinkBlockersEvent
313
+ | LinkRuntimeObservedEvent
314
+ | VocabularyBootstrapRequiredEvent
315
+ | VocabularyBootstrapCompletedEvent
316
+ | BodyLinksPlannedEvent
317
+ | BodyLinksReadyForConfirmationEvent
318
+ | RelatedNotesPlannedEvent
319
+ | RelatedNotesApplyRequestedEvent
320
+ | VocabularySemanticRepairPlannedEvent
321
+ | VocabularySemanticRepairApplyRequestedEvent
322
+ | AgentDisambiguationRequiredEvent
323
+ | AgentDisambiguationCompletedEvent
324
+ | VocabularyCuratorRequiredEvent
325
+ | VocabularyCuratorCompletedEvent
326
+ | RelatedNotesExportRecoveredEvent
327
+ | RelatedNotesQuotaWaitEvent
328
+ | RelatedNotesQuotaReadyEvent
329
+ | HumanApplyApprovedEvent
330
+ | HumanApplyCancelledEvent
331
+ | BodyLinksAppliedEvent
332
+ | RelatedNotesAppliedEvent
333
+ | VocabularySemanticRepairAppliedEvent
334
+ | LinkBlockedEvent
335
+ | LinkFailedEvent,
336
+ Field(discriminator="name"),
337
+ ]
338
+ LinkBoundaryEventAdapter = TypeAdapter(LinkBoundaryEvent)
339
+
340
+
341
+ class LinkMachine(StateChart[WorkflowModel]):
342
+ """Pure domain StateChart for link state and effect intents."""
343
+
344
+ allow_event_without_transition = False
345
+ catch_errors_as_events = False
346
+ states = States.from_enum(
347
+ LinkState,
348
+ initial=LinkState.CHECKING_TRIGGER_CONTEXT,
349
+ final={
350
+ LinkState.COMPLETED,
351
+ LinkState.COMPLETED_WITH_LINK_BLOCKERS,
352
+ LinkState.APPLY_CANCELLED,
353
+ LinkState.GRAPH_DIAGNOSIS_BLOCKED,
354
+ LinkState.BODY_LINKS_BLOCKED,
355
+ LinkState.RELATED_NOTES_BLOCKED,
356
+ LinkState.VOCABULARY_SEMANTIC_REPAIR_BLOCKED,
357
+ LinkState.FAILED,
358
+ },
359
+ use_enum_instance=False,
360
+ )
361
+
362
+ trigger_context_ready = states.CHECKING_TRIGGER_CONTEXT.to(states.DIAGNOSING_GRAPH, on="_on_diagnose")
363
+ stale_diagnosis = (
364
+ states.CHECKING_TRIGGER_CONTEXT.to(states.STALE_DIAGNOSIS, on="_on_blocked")
365
+ | states.DIAGNOSING_GRAPH.to(states.STALE_DIAGNOSIS, on="_on_blocked")
366
+ )
367
+ diagnosis_refresh_requested = states.STALE_DIAGNOSIS.to(states.DIAGNOSING_GRAPH, on="_on_diagnose")
368
+ diagnosis_clean = states.DIAGNOSING_GRAPH.to(states.COMPLETED, on="_on_completed")
369
+ diagnosis_completed_with_link_blockers = states.DIAGNOSING_GRAPH.to(
370
+ states.COMPLETED_WITH_LINK_BLOCKERS,
371
+ on="_on_completed",
372
+ )
373
+ runtime_observed = (
374
+ states.DIAGNOSING_GRAPH.to(states.FAILED, cond="_observed_failed", on="_on_runtime_failed")
375
+ | states.DIAGNOSING_GRAPH.to(
376
+ states.STALE_DIAGNOSIS,
377
+ cond="_observed_stale_diagnosis",
378
+ on="_on_runtime_blocked",
379
+ )
380
+ | states.DIAGNOSING_GRAPH.to(
381
+ states.WAITING_EXTERNAL_RELATED_NOTES_QUOTA,
382
+ cond="_observed_related_notes_quota",
383
+ on="_on_runtime_wait_external",
384
+ )
385
+ | states.DIAGNOSING_GRAPH.to(
386
+ states.VOCABULARY_BOOTSTRAP_REQUIRED,
387
+ cond="_observed_vocabulary_bootstrap_required",
388
+ on="_on_runtime_agent_required",
389
+ )
390
+ | states.DIAGNOSING_GRAPH.to(
391
+ states.WAITING_AGENT_VOCABULARY_CURATOR,
392
+ cond="_observed_vocabulary_curator_required",
393
+ on="_on_runtime_agent_required",
394
+ )
395
+ | states.DIAGNOSING_GRAPH.to(
396
+ states.WAITING_AGENT_RELATED_NOTES_EXPORT_RECOVERY,
397
+ cond="_observed_related_notes_export_recovery_required",
398
+ on="_on_runtime_agent_required",
399
+ )
400
+ | states.DIAGNOSING_GRAPH.to(
401
+ states.RELATED_NOTES_BLOCKED,
402
+ cond="_observed_related_notes_blocked",
403
+ on="_on_runtime_blocked",
404
+ )
405
+ | states.DIAGNOSING_GRAPH.to(
406
+ states.BODY_LINKS_BLOCKED,
407
+ cond="_observed_body_linker_blocked",
408
+ on="_on_runtime_blocked",
409
+ )
410
+ | states.DIAGNOSING_GRAPH.to(
411
+ states.COMPLETED_WITH_LINK_BLOCKERS,
412
+ cond="_observed_completed_with_warnings",
413
+ on="_on_completed",
414
+ )
415
+ | states.DIAGNOSING_GRAPH.to(
416
+ states.COMPLETED,
417
+ cond="_observed_completed",
418
+ on="_on_completed",
419
+ )
420
+ )
421
+
422
+ vocabulary_bootstrap_required = states.DIAGNOSING_GRAPH.to(
423
+ states.VOCABULARY_BOOTSTRAP_REQUIRED,
424
+ on="_on_agent_required",
425
+ )
426
+ vocabulary_bootstrap_completed = states.VOCABULARY_BOOTSTRAP_REQUIRED.to(
427
+ states.DIAGNOSING_GRAPH,
428
+ on="_on_transition",
429
+ )
430
+ body_links_planned = states.DIAGNOSING_GRAPH.to(states.PLANNING_BODY_LINKS, on="_on_transition")
431
+ body_links_ready_for_confirmation = states.PLANNING_BODY_LINKS.to(
432
+ states.WAITING_HUMAN_CONFIRMATION,
433
+ on="_on_human_confirmation",
434
+ )
435
+ related_notes_planned = states.DIAGNOSING_GRAPH.to(states.PLANNING_RELATED_NOTES, on="_on_transition")
436
+ related_notes_apply_requested = states.PLANNING_RELATED_NOTES.to(
437
+ states.APPLYING_RELATED_NOTES,
438
+ on="_on_apply_related_notes",
439
+ )
440
+ vocabulary_semantic_repair_planned = states.DIAGNOSING_GRAPH.to(
441
+ states.PLANNING_VOCABULARY_SEMANTIC_REPAIR,
442
+ on="_on_transition",
443
+ )
444
+ vocabulary_semantic_repair_apply_requested = states.PLANNING_VOCABULARY_SEMANTIC_REPAIR.to(
445
+ states.APPLYING_VOCABULARY_SEMANTIC_REPAIR,
446
+ on="_on_apply_vocabulary_semantic_repair",
447
+ )
448
+ agent_disambiguation_required = states.DIAGNOSING_GRAPH.to(
449
+ states.WAITING_AGENT_DISAMBIGUATION,
450
+ on="_on_agent_required",
451
+ )
452
+ agent_disambiguation_completed = states.WAITING_AGENT_DISAMBIGUATION.to(
453
+ states.DIAGNOSING_GRAPH,
454
+ on="_on_transition",
455
+ )
456
+ vocabulary_curator_required = states.DIAGNOSING_GRAPH.to(
457
+ states.WAITING_AGENT_VOCABULARY_CURATOR,
458
+ on="_on_agent_required",
459
+ )
460
+ vocabulary_curator_completed = states.WAITING_AGENT_VOCABULARY_CURATOR.to(
461
+ states.DIAGNOSING_GRAPH,
462
+ on="_on_transition",
463
+ )
464
+ related_notes_export_recovered = states.WAITING_AGENT_RELATED_NOTES_EXPORT_RECOVERY.to(
465
+ states.DIAGNOSING_GRAPH,
466
+ on="_on_transition",
467
+ )
468
+
469
+ related_notes_quota_wait = (
470
+ states.DIAGNOSING_GRAPH.to(states.WAITING_EXTERNAL_RELATED_NOTES_QUOTA, on="_on_wait_external")
471
+ | states.PLANNING_RELATED_NOTES.to(states.WAITING_EXTERNAL_RELATED_NOTES_QUOTA, on="_on_wait_external")
472
+ )
473
+ related_notes_quota_ready = states.WAITING_EXTERNAL_RELATED_NOTES_QUOTA.to(
474
+ states.DIAGNOSING_GRAPH,
475
+ on="_on_transition",
476
+ )
477
+
478
+ human_apply_approved = states.WAITING_HUMAN_CONFIRMATION.to(states.APPLYING_BODY_LINKS, on="_on_apply_body_links")
479
+ human_apply_cancelled = states.WAITING_HUMAN_CONFIRMATION.to(states.APPLY_CANCELLED, on="_on_blocked")
480
+ body_links_applied = states.APPLYING_BODY_LINKS.to(states.COMPLETED, on="_on_completed")
481
+ related_notes_applied = states.APPLYING_RELATED_NOTES.to(states.COMPLETED, on="_on_completed")
482
+ vocabulary_semantic_repair_applied = states.APPLYING_VOCABULARY_SEMANTIC_REPAIR.to(
483
+ states.COMPLETED,
484
+ on="_on_completed",
485
+ )
486
+ link_blocked = (
487
+ states.DIAGNOSING_GRAPH.to(states.GRAPH_DIAGNOSIS_BLOCKED, on="_on_blocked")
488
+ | states.PLANNING_BODY_LINKS.to(states.BODY_LINKS_BLOCKED, on="_on_blocked")
489
+ | states.APPLYING_BODY_LINKS.to(states.BODY_LINKS_BLOCKED, on="_on_blocked")
490
+ | states.PLANNING_RELATED_NOTES.to(states.RELATED_NOTES_BLOCKED, on="_on_blocked")
491
+ | states.APPLYING_RELATED_NOTES.to(states.RELATED_NOTES_BLOCKED, on="_on_blocked")
492
+ | states.PLANNING_VOCABULARY_SEMANTIC_REPAIR.to(
493
+ states.VOCABULARY_SEMANTIC_REPAIR_BLOCKED,
494
+ on="_on_blocked",
495
+ )
496
+ | states.APPLYING_VOCABULARY_SEMANTIC_REPAIR.to(
497
+ states.VOCABULARY_SEMANTIC_REPAIR_BLOCKED,
498
+ on="_on_blocked",
499
+ )
500
+ )
501
+ link_failed = (
502
+ states.DIAGNOSING_GRAPH.to(states.FAILED, on="_on_failed")
503
+ | states.APPLYING_BODY_LINKS.to(states.FAILED, on="_on_failed")
504
+ | states.APPLYING_RELATED_NOTES.to(states.FAILED, on="_on_failed")
505
+ | states.APPLYING_VOCABULARY_SEMANTIC_REPAIR.to(states.FAILED, on="_on_failed")
506
+ )
507
+
508
+ def category_for_state(self, state: str) -> WorkflowStateCategory:
509
+ return category_for_link_state(LinkState(state))
510
+
511
+ def _on_transition(self, workflow_event: LinkEvent, target: object) -> WorkflowTransitionResult:
512
+ return _transition(workflow_event, _target_state(target))
513
+
514
+ def _on_diagnose(self, workflow_event: LinkEvent, target: object) -> WorkflowTransitionResult:
515
+ to_state = _target_state(target)
516
+ return _transition(workflow_event, to_state, effects=[_link_effect(workflow_event, to_state)])
517
+
518
+ def _on_agent_required(self, workflow_event: LinkEvent, target: object) -> WorkflowTransitionResult:
519
+ to_state = _target_state(target)
520
+ return _transition(
521
+ workflow_event,
522
+ to_state,
523
+ reason_code=str(getattr(workflow_event, "reason_code", _event_name(workflow_event))),
524
+ effects=[_agent_required_effect(workflow_event, to_state)],
525
+ resume_action=_resume_action_for_link_state(to_state),
526
+ )
527
+
528
+ def _on_wait_external(self, workflow_event: RelatedNotesQuotaWaitEvent, target: object) -> WorkflowTransitionResult:
529
+ to_state = _target_state(target)
530
+ return _transition(
531
+ workflow_event,
532
+ to_state,
533
+ reason_code="related_notes_quota_wait",
534
+ effects=[_wait_external_effect(workflow_event, to_state)],
535
+ resume_action=workflow_event.resume_action,
536
+ )
537
+
538
+ def _on_human_confirmation(self, workflow_event: LinkEvent, target: object) -> WorkflowTransitionResult:
539
+ to_state = _target_state(target)
540
+ return _human_transition(workflow_event, to_state, reason_code="link_apply_confirmation_required")
541
+
542
+ def _on_apply_body_links(self, workflow_event: LinkEvent, target: object) -> WorkflowTransitionResult:
543
+ to_state = _target_state(target)
544
+ return _transition(
545
+ workflow_event,
546
+ to_state,
547
+ effects=[_link_effect(workflow_event, to_state)],
548
+ resume_action=_resume_action_for_link_state(to_state),
549
+ )
550
+
551
+ def _on_apply_related_notes(self, workflow_event: LinkEvent, target: object) -> WorkflowTransitionResult:
552
+ to_state = _target_state(target)
553
+ return _transition(
554
+ workflow_event,
555
+ to_state,
556
+ effects=[_link_effect(workflow_event, to_state)],
557
+ resume_action=_resume_action_for_link_state(to_state),
558
+ )
559
+
560
+ def _on_apply_vocabulary_semantic_repair(
561
+ self,
562
+ workflow_event: LinkEvent,
563
+ target: object,
564
+ ) -> WorkflowTransitionResult:
565
+ to_state = _target_state(target)
566
+ return _transition(
567
+ workflow_event,
568
+ to_state,
569
+ effects=[_link_effect(workflow_event, to_state)],
570
+ resume_action=_resume_action_for_link_state(to_state),
571
+ )
572
+
573
+ def _on_blocked(self, workflow_event: LinkEvent, target: object) -> WorkflowTransitionResult:
574
+ to_state = _target_state(target)
575
+ reason_code = str(getattr(workflow_event, "reason_code", to_state.value))
576
+ next_action = str(getattr(workflow_event, "next_action", "")) or _resume_action_for_link_state(to_state)
577
+ return _transition(
578
+ workflow_event,
579
+ to_state,
580
+ reason_code=reason_code,
581
+ decision=_decision(kind="hard_block", phase=to_state.value, reason_code=reason_code, next_action=next_action),
582
+ resume_action=next_action,
583
+ )
584
+
585
+ def _on_failed(self, workflow_event: LinkEvent, target: object) -> WorkflowTransitionResult:
586
+ to_state = _target_state(target)
587
+ reason_code = str(getattr(workflow_event, "reason_code", to_state.value))
588
+ next_action = str(getattr(workflow_event, "next_action", "")) or "Retomar pelo diagnostico oficial de links."
589
+ return _transition(
590
+ workflow_event,
591
+ to_state,
592
+ reason_code=reason_code,
593
+ decision=_decision(kind="failed", phase=to_state.value, reason_code=reason_code, next_action=next_action),
594
+ )
595
+
596
+ def _on_completed(self, workflow_event: LinkEvent, target: object) -> WorkflowTransitionResult:
597
+ return _transition(workflow_event, _target_state(target))
598
+
599
+ def _on_runtime_agent_required(
600
+ self,
601
+ workflow_event: LinkRuntimeObservedEvent,
602
+ target: object,
603
+ ) -> WorkflowTransitionResult:
604
+ to_state = _target_state(target)
605
+ return _transition(
606
+ workflow_event,
607
+ to_state,
608
+ reason_code=_runtime_reason_code(workflow_event, to_state),
609
+ effects=[_agent_required_effect(workflow_event, to_state)],
610
+ resume_action=_resume_action_for_link_state(to_state),
611
+ )
612
+
613
+ def _on_runtime_wait_external(
614
+ self,
615
+ workflow_event: LinkRuntimeObservedEvent,
616
+ target: object,
617
+ ) -> WorkflowTransitionResult:
618
+ to_state = _target_state(target)
619
+ next_action = workflow_event.observation.next_action or _resume_action_for_link_state(to_state)
620
+ return _transition(
621
+ workflow_event,
622
+ to_state,
623
+ reason_code="related_notes_quota_wait",
624
+ effects=[_wait_external_effect(workflow_event, to_state)],
625
+ resume_action=next_action,
626
+ )
627
+
628
+ def _on_runtime_blocked(
629
+ self,
630
+ workflow_event: LinkRuntimeObservedEvent,
631
+ target: object,
632
+ ) -> WorkflowTransitionResult:
633
+ to_state = _target_state(target)
634
+ reason_code = _runtime_reason_code(workflow_event, to_state)
635
+ next_action = workflow_event.observation.next_action or _resume_action_for_link_state(to_state)
636
+ return _transition(
637
+ workflow_event,
638
+ to_state,
639
+ reason_code=reason_code,
640
+ decision=_decision(kind="hard_block", phase=to_state.value, reason_code=reason_code, next_action=next_action),
641
+ resume_action=next_action,
642
+ )
643
+
644
+ def _on_runtime_failed(
645
+ self,
646
+ workflow_event: LinkRuntimeObservedEvent,
647
+ target: object,
648
+ ) -> WorkflowTransitionResult:
649
+ to_state = _target_state(target)
650
+ reason_code = _runtime_reason_code(workflow_event, to_state)
651
+ next_action = workflow_event.observation.next_action or "Retomar pelo diagnostico oficial de links."
652
+ return _transition(
653
+ workflow_event,
654
+ to_state,
655
+ reason_code=reason_code,
656
+ decision=_decision(kind="failed", phase=to_state.value, reason_code=reason_code, next_action=next_action),
657
+ )
658
+
659
+ def _observed_failed(self, workflow_event: LinkRuntimeObservedEvent) -> bool:
660
+ observation = workflow_event.observation
661
+ return observation.failed or (
662
+ observation.mode == LinkMode.BODY_ONLY
663
+ and (
664
+ observation.related_notes_waiting_external
665
+ or observation.related_notes_blocked
666
+ or observation.related_notes_export_recovery_required
667
+ or observation.related_notes_applied
668
+ )
669
+ )
670
+
671
+ def _observed_stale_diagnosis(self, workflow_event: LinkRuntimeObservedEvent) -> bool:
672
+ return workflow_event.observation.stale_diagnosis
673
+
674
+ def _observed_related_notes_quota(self, workflow_event: LinkRuntimeObservedEvent) -> bool:
675
+ observation = workflow_event.observation
676
+ return observation.mode == LinkMode.FULL and observation.related_notes_waiting_external
677
+
678
+ def _observed_related_notes_export_recovery_required(self, workflow_event: LinkRuntimeObservedEvent) -> bool:
679
+ observation = workflow_event.observation
680
+ return observation.mode == LinkMode.FULL and observation.related_notes_export_recovery_required
681
+
682
+ def _observed_vocabulary_bootstrap_required(self, workflow_event: LinkRuntimeObservedEvent) -> bool:
683
+ return workflow_event.observation.vocabulary_bootstrap_required
684
+
685
+ def _observed_vocabulary_curator_required(self, workflow_event: LinkRuntimeObservedEvent) -> bool:
686
+ return workflow_event.observation.vocabulary_curator_required
687
+
688
+ def _observed_related_notes_blocked(self, workflow_event: LinkRuntimeObservedEvent) -> bool:
689
+ observation = workflow_event.observation
690
+ return observation.mode == LinkMode.FULL and observation.related_notes_blocked
691
+
692
+ def _observed_body_linker_blocked(self, workflow_event: LinkRuntimeObservedEvent) -> bool:
693
+ observation = workflow_event.observation
694
+ return observation.body_linker_blocked or observation.blocker_count > 0
695
+
696
+ def _observed_completed_with_warnings(self, workflow_event: LinkRuntimeObservedEvent) -> bool:
697
+ observation = workflow_event.observation
698
+ return observation.blocker_count > 0 and not observation.rewritten_link_count
699
+
700
+ def _observed_completed(self, workflow_event: LinkRuntimeObservedEvent) -> bool:
701
+ return not self._observed_failed(workflow_event)
702
+
703
+
704
+ def category_for_link_state(state: LinkState) -> WorkflowStateCategory:
705
+ """Map each link leaf state to the public workflow category."""
706
+
707
+ match state:
708
+ case (
709
+ LinkState.CHECKING_TRIGGER_CONTEXT
710
+ | LinkState.DIAGNOSING_GRAPH
711
+ | LinkState.PLANNING_BODY_LINKS
712
+ | LinkState.PLANNING_RELATED_NOTES
713
+ | LinkState.PLANNING_VOCABULARY_SEMANTIC_REPAIR
714
+ | LinkState.APPLYING_BODY_LINKS
715
+ | LinkState.APPLYING_RELATED_NOTES
716
+ | LinkState.APPLYING_VOCABULARY_SEMANTIC_REPAIR
717
+ ):
718
+ return WorkflowStateCategory.RUNNING
719
+ case (
720
+ LinkState.VOCABULARY_BOOTSTRAP_REQUIRED
721
+ | LinkState.WAITING_AGENT_DISAMBIGUATION
722
+ | LinkState.WAITING_AGENT_RELATED_NOTES_EXPORT_RECOVERY
723
+ | LinkState.WAITING_AGENT_VOCABULARY_CURATOR
724
+ ):
725
+ return WorkflowStateCategory.WAITING_AGENT
726
+ case LinkState.WAITING_EXTERNAL_RELATED_NOTES_QUOTA:
727
+ return WorkflowStateCategory.WAITING_EXTERNAL
728
+ case LinkState.WAITING_HUMAN_CONFIRMATION:
729
+ return WorkflowStateCategory.WAITING_HUMAN
730
+ case (
731
+ LinkState.STALE_DIAGNOSIS
732
+ | LinkState.APPLY_CANCELLED
733
+ | LinkState.GRAPH_DIAGNOSIS_BLOCKED
734
+ | LinkState.BODY_LINKS_BLOCKED
735
+ | LinkState.RELATED_NOTES_BLOCKED
736
+ | LinkState.VOCABULARY_SEMANTIC_REPAIR_BLOCKED
737
+ ):
738
+ return WorkflowStateCategory.BLOCKED
739
+ case LinkState.FAILED:
740
+ return WorkflowStateCategory.FAILED
741
+ case LinkState.COMPLETED:
742
+ return WorkflowStateCategory.COMPLETED
743
+ case LinkState.COMPLETED_WITH_LINK_BLOCKERS:
744
+ return WorkflowStateCategory.COMPLETED_WITH_WARNINGS
745
+
746
+
747
+ def _transition(
748
+ workflow_event: LinkEvent,
749
+ to_state: LinkState,
750
+ *,
751
+ reason_code: str | None = None,
752
+ effects: list[WorkflowEffect] | None = None,
753
+ decision: WorkflowDecision | None = None,
754
+ human_decision_packet: HumanDecisionPacket | None = None,
755
+ resume_action: str = "",
756
+ ) -> WorkflowTransitionResult:
757
+ return WorkflowTransitionResult(
758
+ workflow=workflow_event.workflow,
759
+ run_id=workflow_event.run_id,
760
+ from_state=workflow_event.current_state,
761
+ to_state=to_state.value,
762
+ trigger=_event_name(workflow_event),
763
+ reason_code=reason_code or str(getattr(workflow_event, "reason_code", _event_name(workflow_event))),
764
+ effects=list(effects or []),
765
+ decision=decision,
766
+ human_decision_packet=human_decision_packet,
767
+ resume_action=resume_action,
768
+ )
769
+
770
+
771
+ def _target_state(target: object) -> LinkState:
772
+ """Read the python-statemachine transition target without touching IO."""
773
+
774
+ value = getattr(target, "value", target)
775
+ return LinkState(str(value))
776
+
777
+
778
+ def _agent_required_effect(workflow_event: LinkEvent, origin_state: LinkState) -> WorkflowEffect:
779
+ """Emit the effect selected by the LinkMachine waiting-agent state."""
780
+
781
+ if origin_state == LinkState.WAITING_AGENT_RELATED_NOTES_EXPORT_RECOVERY:
782
+ return _related_notes_export_recovery_effect(workflow_event, origin_state)
783
+ return _link_effect(workflow_event, origin_state)
784
+
785
+
786
+ def _related_notes_export_recovery_effect(workflow_event: LinkEvent, origin_state: LinkState) -> WorkflowEffect:
787
+ reason_code = str(getattr(workflow_event, "reason_code", "")) or origin_state.value
788
+ if isinstance(workflow_event, LinkRuntimeObservedEvent):
789
+ reason_code = workflow_event.observation.related_notes_export_recovery_reason or _runtime_reason_code(
790
+ workflow_event,
791
+ origin_state,
792
+ )
793
+ payload = RelatedNotesExportEffectPayload(mode="auto", reason_code=reason_code).to_payload()
794
+ return WorkflowEffect(
795
+ workflow=workflow_event.workflow,
796
+ run_id=workflow_event.run_id,
797
+ effect_id="link-related-notes-export-recovery",
798
+ origin_state=origin_state.value,
799
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
800
+ target="related_notes.export",
801
+ payload=payload,
802
+ requires_receipt=False,
803
+ no_resource_mutation=True,
804
+ resume_action=_resume_action_for_link_state(origin_state),
805
+ )
806
+
807
+
808
+ def _link_effect(workflow_event: LinkEvent, origin_state: LinkState) -> WorkflowEffect:
809
+ target, payload_kind = _effect_contract_for_state(origin_state)
810
+ if workflow_event.workflow == LINK_BODY_WORKFLOW:
811
+ if origin_state not in {LinkState.DIAGNOSING_GRAPH, LinkState.APPLYING_BODY_LINKS}:
812
+ raise ValueError(f"{origin_state.value} is not executable for {LINK_BODY_WORKFLOW}")
813
+ target = LINK_BODY_WORKFLOW
814
+ return WorkflowEffect(
815
+ workflow=workflow_event.workflow,
816
+ run_id=workflow_event.run_id,
817
+ effect_id=f"link-{origin_state.value.replace('_', '-')}",
818
+ origin_state=origin_state.value,
819
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
820
+ target=target,
821
+ payload=_link_effect_payload(workflow_event, payload_kind=payload_kind),
822
+ mutates_resources=origin_state
823
+ in {
824
+ LinkState.APPLYING_BODY_LINKS,
825
+ LinkState.APPLYING_RELATED_NOTES,
826
+ LinkState.APPLYING_VOCABULARY_SEMANTIC_REPAIR,
827
+ },
828
+ rollback_declared=origin_state
829
+ in {
830
+ LinkState.APPLYING_BODY_LINKS,
831
+ LinkState.APPLYING_RELATED_NOTES,
832
+ LinkState.APPLYING_VOCABULARY_SEMANTIC_REPAIR,
833
+ },
834
+ requires_receipt=False,
835
+ no_resource_mutation=origin_state
836
+ not in {
837
+ LinkState.APPLYING_BODY_LINKS,
838
+ LinkState.APPLYING_RELATED_NOTES,
839
+ LinkState.APPLYING_VOCABULARY_SEMANTIC_REPAIR,
840
+ },
841
+ )
842
+
843
+
844
+ def _link_effect_payload(workflow_event: LinkEvent, *, payload_kind: LinkWorkflowRunKind) -> JsonObject:
845
+ """Project typed event fields that the adapter/agent needs to execute the effect."""
846
+
847
+ apply_requested = payload_kind.startswith("apply_")
848
+ payload = LinkWorkflowRunEffectPayload(
849
+ kind=payload_kind,
850
+ diagnose=not apply_requested,
851
+ apply=apply_requested,
852
+ diagnosis_path=str(getattr(workflow_event, "diagnosis_path", "")),
853
+ receipt_path=str(getattr(workflow_event, "receipt_path", "")),
854
+ no_related_notes=workflow_event.workflow == LINK_BODY_WORKFLOW,
855
+ ).to_payload()
856
+ if isinstance(workflow_event, VocabularyBootstrapRequiredEvent):
857
+ payload["db_path"] = workflow_event.db_path
858
+ if isinstance(workflow_event, VocabularyCuratorRequiredEvent):
859
+ payload["work_item_count"] = workflow_event.work_item_count
860
+ if workflow_event.batch_plan_path:
861
+ payload["batch_plan_path"] = workflow_event.batch_plan_path
862
+ if isinstance(workflow_event, LinkRuntimeObservedEvent):
863
+ observation = workflow_event.observation
864
+ if payload_kind == "vocabulary_bootstrap":
865
+ if not observation.vocabulary_db_path.strip():
866
+ raise ValueError("vocabulary_bootstrap effect requires vocabulary_db_path")
867
+ payload["db_path"] = observation.vocabulary_db_path
868
+ if payload_kind == "vocabulary_curator":
869
+ payload["work_item_count"] = observation.vocabulary_curator_work_item_count
870
+ if observation.vocabulary_curator_batch_plan_path:
871
+ payload["batch_plan_path"] = observation.vocabulary_curator_batch_plan_path
872
+ return payload
873
+
874
+
875
+ def _wait_external_effect(workflow_event: RelatedNotesQuotaWaitEvent | LinkRuntimeObservedEvent, origin_state: LinkState) -> WorkflowEffect:
876
+ recovery_state = (
877
+ workflow_event.observation.related_notes_recovery_state
878
+ if isinstance(workflow_event, LinkRuntimeObservedEvent)
879
+ else workflow_event.related_notes_recovery_state
880
+ )
881
+ resume_action = (
882
+ workflow_event.observation.next_action
883
+ if isinstance(workflow_event, LinkRuntimeObservedEvent)
884
+ else workflow_event.resume_action
885
+ )
886
+ if not recovery_state.status:
887
+ recovery_state = RelatedNotesRecoveryStateEffectPayload.from_payload(
888
+ {
889
+ "schema": "medical-notes-workbench.related-notes-recovery-state.v1",
890
+ "status": "waiting_for_retry",
891
+ "blocked_reason": "related_notes_headless_quota_exhausted",
892
+ "next_action": resume_action,
893
+ "resume_supported": True,
894
+ }
895
+ )
896
+ payload = WaitExternalEffectPayload.model_validate(
897
+ {
898
+ "related_notes_recovery_state": recovery_state,
899
+ "next_action": resume_action,
900
+ }
901
+ ).to_payload()
902
+ return WorkflowEffect(
903
+ workflow=workflow_event.workflow,
904
+ run_id=workflow_event.run_id,
905
+ effect_id="link-related-notes-quota-wait",
906
+ origin_state=origin_state.value,
907
+ kind=WorkflowEffectKind.WAIT_EXTERNAL,
908
+ target="related_notes.quota",
909
+ payload=payload,
910
+ requires_receipt=False,
911
+ no_resource_mutation=True,
912
+ resume_action=resume_action,
913
+ )
914
+
915
+
916
+ def _human_transition(
917
+ workflow_event: LinkEvent,
918
+ to_state: LinkState,
919
+ *,
920
+ reason_code: str,
921
+ ) -> WorkflowTransitionResult:
922
+ decision = _decision(
923
+ kind="ask_human",
924
+ phase=to_state.value,
925
+ reason_code=reason_code,
926
+ next_action="link:confirm-apply",
927
+ )
928
+ packet = HumanDecisionPacket.model_validate(decision.to_human_decision_packet())
929
+ effect = WorkflowEffect(
930
+ workflow=workflow_event.workflow,
931
+ run_id=workflow_event.run_id,
932
+ effect_id="link-human-apply-confirmation",
933
+ origin_state=to_state.value,
934
+ kind=WorkflowEffectKind.ASK_HUMAN,
935
+ target="human.link_apply_confirmation",
936
+ payload={"kind": "human_decision", "reason_code": reason_code},
937
+ requires_receipt=False,
938
+ no_resource_mutation=True,
939
+ )
940
+ return _transition(
941
+ workflow_event,
942
+ to_state,
943
+ reason_code=reason_code,
944
+ effects=[effect],
945
+ decision=decision,
946
+ human_decision_packet=packet,
947
+ resume_action=decision.resume_action,
948
+ )
949
+
950
+
951
+ def _effect_contract_for_state(state: LinkState) -> tuple[str, LinkWorkflowRunKind]:
952
+ match state:
953
+ case LinkState.DIAGNOSING_GRAPH:
954
+ return LINK_WORKFLOW, "diagnose"
955
+ case LinkState.VOCABULARY_BOOTSTRAP_REQUIRED:
956
+ return LINK_WORKFLOW, "vocabulary_bootstrap"
957
+ case LinkState.WAITING_AGENT_DISAMBIGUATION:
958
+ return LINK_WORKFLOW, "agent_disambiguation"
959
+ case LinkState.WAITING_AGENT_VOCABULARY_CURATOR:
960
+ return LINK_WORKFLOW, "vocabulary_curator"
961
+ case LinkState.APPLYING_BODY_LINKS:
962
+ return LINK_WORKFLOW, "apply_body_links"
963
+ case LinkState.APPLYING_RELATED_NOTES:
964
+ return LINK_WORKFLOW, "apply_related_notes"
965
+ case LinkState.APPLYING_VOCABULARY_SEMANTIC_REPAIR:
966
+ return LINK_WORKFLOW, "apply_vocabulary_semantic_repair"
967
+ case _:
968
+ raise AssertionError(f"state does not emit link effect: {state.value}")
969
+
970
+
971
+ def _runtime_reason_code(workflow_event: LinkRuntimeObservedEvent, state: LinkState) -> str:
972
+ observation = workflow_event.observation
973
+ if observation.mode == LinkMode.BODY_ONLY and (
974
+ observation.related_notes_waiting_external
975
+ or observation.related_notes_blocked
976
+ or observation.related_notes_applied
977
+ ):
978
+ return "link_body_mode_contract_violation"
979
+ if observation.reason_code.strip():
980
+ return observation.reason_code.strip()
981
+ if state == LinkState.STALE_DIAGNOSIS:
982
+ return "stale_diagnosis"
983
+ if state == LinkState.WAITING_EXTERNAL_RELATED_NOTES_QUOTA:
984
+ return "related_notes_quota_wait"
985
+ if state == LinkState.WAITING_AGENT_RELATED_NOTES_EXPORT_RECOVERY:
986
+ return observation.related_notes_export_recovery_reason or "related_notes_export_recovery_required"
987
+ if state == LinkState.VOCABULARY_BOOTSTRAP_REQUIRED:
988
+ return "vocabulary_bootstrap_required"
989
+ if state == LinkState.WAITING_AGENT_VOCABULARY_CURATOR:
990
+ return "vocabulary_semantic_ingestion_pending"
991
+ if state == LinkState.RELATED_NOTES_BLOCKED:
992
+ return "related_notes_blocked"
993
+ if state == LinkState.BODY_LINKS_BLOCKED:
994
+ return observation.body_linker_blocked_reason or "body_linker_blocked"
995
+ if state == LinkState.FAILED:
996
+ return "link_failed"
997
+ return state.value
998
+
999
+
1000
+ def _resume_action_for_link_state(state: LinkState) -> str:
1001
+ match state:
1002
+ case LinkState.STALE_DIAGNOSIS:
1003
+ return "link:diagnose"
1004
+ case LinkState.GRAPH_DIAGNOSIS_BLOCKED:
1005
+ return "link:diagnose"
1006
+ case LinkState.BODY_LINKS_BLOCKED:
1007
+ return "link:repair-body-links"
1008
+ case LinkState.RELATED_NOTES_BLOCKED:
1009
+ return "link:repair-related-notes"
1010
+ case LinkState.WAITING_AGENT_RELATED_NOTES_EXPORT_RECOVERY:
1011
+ return "link:recover-related-notes-export"
1012
+ case LinkState.VOCABULARY_SEMANTIC_REPAIR_BLOCKED:
1013
+ return "link:repair-vocabulary-semantics"
1014
+ case LinkState.VOCABULARY_BOOTSTRAP_REQUIRED:
1015
+ return "link:bootstrap-vocabulary"
1016
+ case LinkState.WAITING_AGENT_DISAMBIGUATION:
1017
+ return "link:run-agent-disambiguation"
1018
+ case LinkState.WAITING_AGENT_VOCABULARY_CURATOR:
1019
+ return "link:run-vocabulary-curator"
1020
+ case LinkState.WAITING_HUMAN_CONFIRMATION:
1021
+ return "link:confirm-apply"
1022
+ case LinkState.APPLYING_BODY_LINKS:
1023
+ return "link:apply-body-links"
1024
+ case LinkState.APPLYING_RELATED_NOTES:
1025
+ return "link:apply-related-notes"
1026
+ case LinkState.APPLYING_VOCABULARY_SEMANTIC_REPAIR:
1027
+ return "link:apply-vocabulary-semantic-repair"
1028
+ case _:
1029
+ return "link:diagnose"
1030
+
1031
+
1032
+ def _decision(
1033
+ *,
1034
+ kind: Literal["hard_block", "failed", "ask_human"],
1035
+ phase: str,
1036
+ reason_code: str,
1037
+ next_action: str,
1038
+ ) -> WorkflowDecision:
1039
+ evidence = [
1040
+ DecisionEvidence(
1041
+ summary=f"link StateChart reached {phase}.",
1042
+ technical_code=reason_code,
1043
+ source="link_machine",
1044
+ )
1045
+ ]
1046
+ base: JsonObject = {
1047
+ "kind": kind,
1048
+ "phase": phase,
1049
+ "reason_code": reason_code,
1050
+ "public_summary": "O workflow de links precisa parar nesta etapa.",
1051
+ "developer_summary": f"StateChart transition stopped at {phase}:{reason_code}.",
1052
+ "evidence": evidence,
1053
+ "next_action": next_action,
1054
+ "required_inputs": _required_inputs_for_reason(reason_code),
1055
+ "resume_action": next_action,
1056
+ }
1057
+ if kind == "ask_human":
1058
+ base.update(
1059
+ {
1060
+ "public_summary": "Preciso da sua confirmacao antes de aplicar mudancas de links.",
1061
+ "human_decision_kind": reason_code,
1062
+ "recommended_option_id": "apply",
1063
+ "options": [
1064
+ HumanDecisionOption(
1065
+ id="apply",
1066
+ label="Aplicar",
1067
+ description="Aplica o plano de links com protecao do vault.",
1068
+ ),
1069
+ HumanDecisionOption(
1070
+ id="cancel",
1071
+ label="Cancelar",
1072
+ description="Mantem a Wiki sem alteracoes deste plano.",
1073
+ ),
1074
+ ],
1075
+ "rejected_automations": _rejected_automations(reason_code),
1076
+ }
1077
+ )
1078
+ return WorkflowDecision(**base)
1079
+
1080
+
1081
+ def _required_inputs_for_reason(reason_code: str) -> list[str]:
1082
+ """Return missing operator inputs owned by LinkMachine blocker decisions."""
1083
+
1084
+ match reason_code:
1085
+ case "linker_mode_required":
1086
+ return ["diagnose_or_apply"]
1087
+ case "trigger_context_apply_not_allowed":
1088
+ return ["diagnosis"]
1089
+ case _:
1090
+ return []
1091
+
1092
+
1093
+ def _rejected_automations(reason_code: str) -> list[RejectedAutomation]:
1094
+ return [
1095
+ RejectedAutomation(kind="auto_fix", reason_code=reason_code, reason="Aplicar links muta a Wiki."),
1096
+ RejectedAutomation(
1097
+ kind="auto_defer",
1098
+ reason_code=reason_code,
1099
+ reason="Adiar sem decisao deixa o plano de links pendente.",
1100
+ ),
1101
+ RejectedAutomation(
1102
+ kind="auto_plan",
1103
+ reason_code=reason_code,
1104
+ reason="Planejar novamente nao substitui a confirmacao de apply.",
1105
+ ),
1106
+ ]