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,1260 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated, Literal, NamedTuple, cast
4
+
5
+ from pydantic import ConfigDict, Field, TypeAdapter, field_validator, model_validator
6
+
7
+ from mednotes.domains.wiki.contracts.effect_payloads import (
8
+ RelatedNotesRecoveryEffectPayload,
9
+ RelatedNotesRecoveryStateEffectPayload,
10
+ WaitExternalEffectPayload,
11
+ )
12
+ from mednotes.domains.wiki.contracts.workflow_outcomes import WorkflowDecision, decision_from_payload
13
+ from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_fsm import (
14
+ FIX_WIKI_WORKFLOW,
15
+ FixWikiDiagnosisLane,
16
+ FixWikiFsmFacts,
17
+ FixWikiState,
18
+ )
19
+ from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_machine import (
20
+ AtomicitySplitAppliedEvent,
21
+ AtomicitySplitBlockedEvent,
22
+ DeterministicRepairsAppliedEvent,
23
+ DeterministicRepairsBlockedEvent,
24
+ FinalValidationFailedEvent,
25
+ FinalValidationFoundMoreWorkEvent,
26
+ FinalValidationPassedEvent,
27
+ FinalValidationWarningsEvent,
28
+ FixWikiBoundaryEventAdapter,
29
+ FixWikiEvent,
30
+ FixWikiMachine,
31
+ LinkCompletedEvent,
32
+ LinkerBlockedEvent,
33
+ LinkGraphBlockedEvent,
34
+ MergeAppliedEvent,
35
+ MergeBlockedEvent,
36
+ RelatedNotesBlockedEvent,
37
+ RelatedNotesExportCompletedEvent,
38
+ RelatedNotesObsidianNotReadyEvent,
39
+ RelatedNotesQuotaWaitEvent,
40
+ RollbackCompletedEvent,
41
+ RollbackFailedEvent,
42
+ SetupBootstrapBlockedEvent,
43
+ SetupBootstrapReadyEvent,
44
+ StyleRewriteAppliedEvent,
45
+ StyleRewriteBlockedEvent,
46
+ StyleRewriteCapacityWaitEvent,
47
+ StyleRewriteReviewRequiredEvent,
48
+ StyleRewriteSpecialistCompletedEvent,
49
+ TaxonomyAppliedEvent,
50
+ TaxonomyBlockedEvent,
51
+ TaxonomyDecisionRequiredEvent,
52
+ VaultGuardBlockedEvent,
53
+ VaultGuardReadyEvent,
54
+ VocabularyAppliedEvent,
55
+ VocabularyCuratorCompletedEvent,
56
+ VocabularyEvalNeedsReviewEvent,
57
+ VocabularyIntegrityFailedEvent,
58
+ )
59
+ from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_states import FIX_WIKI_DIAGNOSIS_PRIORITY
60
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
61
+ from mednotes.kernel.effects import WorkflowEffect, WorkflowEffectKind, WorkflowEffectResult, WorkflowEffectStatus
62
+ from mednotes.kernel.fsm_model import WorkflowModel
63
+ from mednotes.kernel.state_machine import send_workflow_event
64
+ from mednotes.kernel.workflow import HumanDecisionPacket
65
+
66
+ RELATED_NOTES_EXTERNAL_RETRY_REASONS = frozenset(
67
+ {
68
+ "related_notes_headless_quota_exhausted",
69
+ "related_notes_headless_time_budget_exhausted",
70
+ }
71
+ )
72
+
73
+
74
+ class _FixWikiEffectErrorContextFields(ContractModel):
75
+ """Typed lens for effect error fields that influence recovery state."""
76
+
77
+ model_config = ConfigDict(extra="ignore")
78
+
79
+ blocked_reason: str = ""
80
+ root_cause: str = ""
81
+
82
+
83
+ class _FixWikiLinkChildDiagnosticFields(ContractModel):
84
+ """Typed diagnostic fields exposed by the child `/mednotes:link` FSM result."""
85
+
86
+ model_config = ConfigDict(extra="ignore")
87
+
88
+ related_notes_recovery_state: RelatedNotesRecoveryStateEffectPayload | None = None
89
+
90
+ @field_validator("related_notes_recovery_state", mode="before")
91
+ @classmethod
92
+ def _coerce_recovery_state(cls, value: object) -> RelatedNotesRecoveryStateEffectPayload | None:
93
+ if value in (None, {}):
94
+ return None
95
+ if isinstance(value, RelatedNotesRecoveryStateEffectPayload):
96
+ return value
97
+ return RelatedNotesRecoveryStateEffectPayload.from_payload(value)
98
+
99
+
100
+ class _FixWikiLinkChildPayloadFields(ContractModel):
101
+ """Typed boundary for the child link FSM payload consumed by fix-wiki."""
102
+
103
+ model_config = ConfigDict(extra="ignore")
104
+
105
+ schema_id: str = Field(default="", alias="schema")
106
+ diagnostic_context: _FixWikiLinkChildDiagnosticFields = Field(default_factory=_FixWikiLinkChildDiagnosticFields)
107
+
108
+
109
+ class FixWikiEffectRuntimeUpdate(ContractModel):
110
+ """Typed effect-result update before rebuilding canonical fix-wiki facts."""
111
+
112
+ model_config = ConfigDict(extra="forbid")
113
+
114
+ next_action: str = ""
115
+ diagnostic_context: JsonObject = Field(default_factory=dict)
116
+ error_context: JsonObject = Field(default_factory=dict)
117
+ external_wait_reason_code: str = ""
118
+ external_wait_resume_action: str = ""
119
+ external_wait_payload: JsonObject = Field(default_factory=dict)
120
+ related_notes_blocked: bool = Field(default=False, strict=True)
121
+ related_notes_recovery_state: JsonObject = Field(default_factory=dict)
122
+ human_decision_required: bool = Field(default=False, strict=True)
123
+ decision: WorkflowDecision | None = None
124
+ human_decision_packet: HumanDecisionPacket | None = None
125
+ pending_effects: list[WorkflowEffect] | None = None
126
+ linker_blocked: bool = Field(default=False, strict=True)
127
+ failed: bool = Field(default=False, strict=True)
128
+ failed_reason_code: str = ""
129
+
130
+ def to_runtime_update(self) -> dict[str, object]:
131
+ return cast(
132
+ dict[str, object],
133
+ self.model_dump(mode="python", exclude_defaults=True, exclude_none=True),
134
+ )
135
+
136
+
137
+ def _style_rewrite_payload_text(payload: JsonObject, key: str) -> str:
138
+ """Read required work-item identity without letting raw JSON drive policy."""
139
+
140
+ if key not in payload:
141
+ return ""
142
+ value = payload[key]
143
+ if value is None:
144
+ return ""
145
+ return str(value).strip()
146
+
147
+
148
+ class FixWikiStyleRewriteSpecialistEffectRequest(ContractModel):
149
+ """Typed request for the executable specialist effect emitted by fix-wiki.
150
+
151
+ The effect authorizes parallel specialist authoring, but it never authorizes
152
+ parallel vault mutation. Each item is an independent temp-output proposal;
153
+ the CLI remains the only serial apply boundary.
154
+ """
155
+
156
+ model_config = ConfigDict(extra="forbid")
157
+
158
+ run_id: str
159
+ work_id: str = Field(min_length=1)
160
+ target_path: str = Field(min_length=1)
161
+ agent_name: str = Field(default="med-knowledge-architect", min_length=1)
162
+ title: str = ""
163
+ plan_path: str = ""
164
+ manifest_path: str = ""
165
+ current_batch_items: list[JsonObject] = Field(min_length=1)
166
+ authoring_max_concurrency: int = Field(default=1, ge=1)
167
+
168
+ @model_validator(mode="after")
169
+ def batch_items_have_unique_operational_owners(self) -> FixWikiStyleRewriteSpecialistEffectRequest:
170
+ work_ids = [_style_rewrite_payload_text(item, "work_id") for item in self.current_batch_items]
171
+ target_paths = [_style_rewrite_payload_text(item, "target_path") for item in self.current_batch_items]
172
+ temp_outputs = [_style_rewrite_payload_text(item, "temp_output") for item in self.current_batch_items]
173
+ if any(not value for value in work_ids):
174
+ raise ValueError("style rewrite batch items require work_id")
175
+ if any(not value for value in target_paths):
176
+ raise ValueError("style rewrite batch items require target_path")
177
+ if any(not value for value in temp_outputs):
178
+ raise ValueError("style rewrite batch items require temp_output")
179
+ if len(work_ids) != len(set(work_ids)):
180
+ raise ValueError("style rewrite batch work_id values must be unique")
181
+ if len(target_paths) != len(set(target_paths)):
182
+ raise ValueError("style rewrite batch target_path values must be unique")
183
+ if len(temp_outputs) != len(set(temp_outputs)):
184
+ raise ValueError("style rewrite batch temp_output values must be unique")
185
+ if work_ids[0] != self.work_id:
186
+ raise ValueError("style rewrite effect work_id must match the first batch item")
187
+ if target_paths[0] != self.target_path:
188
+ raise ValueError("style rewrite effect target_path must match the first batch item")
189
+ return self
190
+
191
+
192
+ class _StyleRewriteWorkItemForEffect(ContractModel):
193
+ """Minimal style-rewrite work item shape needed to launch a specialist."""
194
+
195
+ model_config = ConfigDict(extra="ignore")
196
+
197
+ work_id: str = ""
198
+ target_path: str = ""
199
+ agent: str = ""
200
+ title: str = ""
201
+
202
+
203
+ class _StyleRewritePlanForEffect(ContractModel):
204
+ """Typed plan slice for deriving specialist effects inside the effects domain."""
205
+
206
+ model_config = ConfigDict(extra="ignore")
207
+
208
+ status: str = ""
209
+ agent: str = ""
210
+ max_concurrency: int = Field(default=1, ge=0)
211
+ work_items: list[_StyleRewriteWorkItemForEffect] = Field(default_factory=list)
212
+
213
+
214
+ class _StyleRewritePlanPayloadForEffect(ContractModel):
215
+ """Raw JSON work-item payloads preserved after the typed plan slice passes."""
216
+
217
+ model_config = ConfigDict(extra="ignore")
218
+
219
+ work_items: list[JsonObject] = Field(default_factory=list)
220
+
221
+
222
+ class _StyleRewriteBlockerGroupForEffect(ContractModel):
223
+ """Authorization group proving the style rewrite lane can run automatically."""
224
+
225
+ model_config = ConfigDict(extra="ignore")
226
+
227
+ route: str = ""
228
+ automatic: bool = Field(default=False, strict=True)
229
+
230
+
231
+ class _StyleRewriteBlockerResolutionForEffect(ContractModel):
232
+ """Typed blocker-resolution slice used before emitting a specialist effect."""
233
+
234
+ model_config = ConfigDict(extra="ignore")
235
+
236
+ groups: list[_StyleRewriteBlockerGroupForEffect] = Field(default_factory=list)
237
+
238
+
239
+ class _StyleRewritePendingEffectSource(ContractModel):
240
+ """Runtime facts accepted by the effect module before it emits executable work."""
241
+
242
+ model_config = ConfigDict(extra="ignore")
243
+
244
+ run_id: str = Field(min_length=1)
245
+ requested_apply: bool = Field(default=False, strict=True)
246
+ effective_apply: bool = Field(default=False, strict=True)
247
+ requires_llm_rewrite_count: int = Field(default=0, ge=0, strict=True)
248
+ style_rewrite_plan: JsonObject | None = None
249
+ blocker_resolution: _StyleRewriteBlockerResolutionForEffect | None = None
250
+ style_rewrite_plan_path: str = ""
251
+ style_rewrite_manifest_path: str = ""
252
+
253
+
254
+ class SetupBootstrapEffectPayload(ContractModel):
255
+ kind: Literal["setup_bootstrap"] = "setup_bootstrap"
256
+
257
+
258
+ class VaultGuardEffectPayload(ContractModel):
259
+ kind: Literal["vault_guard"] = "vault_guard"
260
+
261
+
262
+ class DeterministicRepairsEffectPayload(ContractModel):
263
+ kind: Literal["deterministic_repairs"] = "deterministic_repairs"
264
+
265
+
266
+ class StyleRewriteEffectPayload(ContractModel):
267
+ kind: Literal["style_rewrite"] = "style_rewrite"
268
+ work_id: str = ""
269
+ target_path: str = ""
270
+ operation_payload: JsonObject = Field(default_factory=dict)
271
+
272
+
273
+ class StyleRewriteApplyEffectPayload(ContractModel):
274
+ kind: Literal["style_rewrite_apply"] = "style_rewrite_apply"
275
+ work_id: str = ""
276
+ target_path: str = ""
277
+
278
+
279
+ class TaxonomyEffectPayload(ContractModel):
280
+ kind: Literal["taxonomy"] = "taxonomy"
281
+
282
+
283
+ class VocabularyCuratorEffectPayload(ContractModel):
284
+ kind: Literal["vocabulary_curator"] = "vocabulary_curator"
285
+
286
+
287
+ class VocabularyEvalEffectPayload(ContractModel):
288
+ kind: Literal["vocabulary_eval"] = "vocabulary_eval"
289
+
290
+
291
+ class VocabularyApplyEffectPayload(ContractModel):
292
+ kind: Literal["vocabulary_apply"] = "vocabulary_apply"
293
+
294
+
295
+ class AtomicitySplitEffectPayload(ContractModel):
296
+ kind: Literal["atomicity_split"] = "atomicity_split"
297
+
298
+
299
+ class MergeEffectPayload(ContractModel):
300
+ kind: Literal["merge"] = "merge"
301
+
302
+
303
+ class RelatedNotesEffectPayload(ContractModel):
304
+ kind: Literal["related_notes"] = "related_notes"
305
+
306
+
307
+ class LinkEffectPayload(ContractModel):
308
+ kind: Literal["link"] = "link"
309
+ diagnose: bool = False
310
+ apply: bool = False
311
+ no_related_notes: bool = False
312
+
313
+
314
+ class FinalValidationEffectPayload(ContractModel):
315
+ kind: Literal["final_validation"] = "final_validation"
316
+
317
+
318
+ class RollbackEffectPayload(ContractModel):
319
+ kind: Literal["rollback"] = "rollback"
320
+
321
+
322
+ class WaitExternalIntentPayload(ContractModel):
323
+ """Internal effect intent keyed by `kind`; the canonical WAIT_EXTERNAL contract lives in WorkflowEffect.payload."""
324
+
325
+ kind: Literal["wait_external"] = "wait_external"
326
+ reason_code: str = ""
327
+
328
+
329
+ class HumanDecisionEffectPayload(ContractModel):
330
+ kind: Literal["human_decision"] = "human_decision"
331
+ reason_code: str = ""
332
+
333
+
334
+ FixWikiEffectPayload = Annotated[
335
+ SetupBootstrapEffectPayload
336
+ | VaultGuardEffectPayload
337
+ | DeterministicRepairsEffectPayload
338
+ | StyleRewriteEffectPayload
339
+ | StyleRewriteApplyEffectPayload
340
+ | TaxonomyEffectPayload
341
+ | VocabularyCuratorEffectPayload
342
+ | VocabularyEvalEffectPayload
343
+ | VocabularyApplyEffectPayload
344
+ | AtomicitySplitEffectPayload
345
+ | MergeEffectPayload
346
+ | RelatedNotesEffectPayload
347
+ | LinkEffectPayload
348
+ | FinalValidationEffectPayload
349
+ | RollbackEffectPayload
350
+ | WaitExternalIntentPayload
351
+ | HumanDecisionEffectPayload,
352
+ Field(discriminator="kind"),
353
+ ]
354
+ FixWikiEffectPayloadAdapter = TypeAdapter(FixWikiEffectPayload)
355
+
356
+
357
+ class FixWikiSetupBootstrapReadyOutcome(ContractModel):
358
+ code: Literal["setup.bootstrap.ready"] = "setup.bootstrap.ready"
359
+
360
+
361
+ class FixWikiSetupBootstrapBlockedOutcome(ContractModel):
362
+ code: Literal["setup.bootstrap.blocked"] = "setup.bootstrap.blocked"
363
+
364
+
365
+ class FixWikiVaultGuardReadyOutcome(ContractModel):
366
+ code: Literal["vault_guard.ready"] = "vault_guard.ready"
367
+
368
+
369
+ class FixWikiVaultGuardBlockedOutcome(ContractModel):
370
+ code: Literal["vault_guard.blocked"] = "vault_guard.blocked"
371
+
372
+
373
+ class FixWikiDeterministicAppliedOutcome(ContractModel):
374
+ code: Literal["deterministic.applied"] = "deterministic.applied"
375
+
376
+
377
+ class FixWikiDeterministicBlockedOutcome(ContractModel):
378
+ code: Literal["deterministic.blocked"] = "deterministic.blocked"
379
+
380
+
381
+ class FixWikiStyleSpecialistCompletedOutcome(ContractModel):
382
+ code: Literal["style.specialist_completed"] = "style.specialist_completed"
383
+
384
+
385
+ class FixWikiStyleCapacityWaitOutcome(ContractModel):
386
+ code: Literal["style.capacity_wait"] = "style.capacity_wait"
387
+
388
+
389
+ class FixWikiStyleReviewRequiredOutcome(ContractModel):
390
+ code: Literal["style.review_required"] = "style.review_required"
391
+
392
+
393
+ class FixWikiStyleApplyCompletedOutcome(ContractModel):
394
+ code: Literal["style.apply_completed"] = "style.apply_completed"
395
+
396
+
397
+ class FixWikiStyleBlockedOutcome(ContractModel):
398
+ code: Literal["style.blocked"] = "style.blocked"
399
+
400
+
401
+ class FixWikiTaxonomyDecisionRequiredOutcome(ContractModel):
402
+ code: Literal["taxonomy.decision_required"] = "taxonomy.decision_required"
403
+
404
+
405
+ class FixWikiTaxonomyAppliedOutcome(ContractModel):
406
+ code: Literal["taxonomy.applied"] = "taxonomy.applied"
407
+
408
+
409
+ class FixWikiTaxonomyBlockedOutcome(ContractModel):
410
+ code: Literal["taxonomy.blocked"] = "taxonomy.blocked"
411
+
412
+
413
+ class FixWikiVocabularyCuratorCompletedOutcome(ContractModel):
414
+ code: Literal["vocabulary.curator_completed"] = "vocabulary.curator_completed"
415
+
416
+
417
+ class FixWikiVocabularyEvalNeedsReviewOutcome(ContractModel):
418
+ code: Literal["vocabulary.eval_needs_review"] = "vocabulary.eval_needs_review"
419
+
420
+
421
+ class FixWikiVocabularyAppliedOutcome(ContractModel):
422
+ code: Literal["vocabulary.applied"] = "vocabulary.applied"
423
+
424
+
425
+ class FixWikiVocabularyIntegrityFailedOutcome(ContractModel):
426
+ code: Literal["vocabulary.integrity_failed"] = "vocabulary.integrity_failed"
427
+
428
+
429
+ class FixWikiAtomicitySplitAppliedOutcome(ContractModel):
430
+ code: Literal["atomicity.split_applied"] = "atomicity.split_applied"
431
+
432
+
433
+ class FixWikiAtomicityBlockedOutcome(ContractModel):
434
+ code: Literal["atomicity.blocked"] = "atomicity.blocked"
435
+
436
+
437
+ class FixWikiMergeAppliedOutcome(ContractModel):
438
+ code: Literal["merge.applied"] = "merge.applied"
439
+
440
+
441
+ class FixWikiMergeBlockedOutcome(ContractModel):
442
+ code: Literal["merge.blocked"] = "merge.blocked"
443
+
444
+
445
+ class FixWikiRelatedNotesExportCompletedOutcome(ContractModel):
446
+ code: Literal["related_notes.export_completed"] = "related_notes.export_completed"
447
+
448
+
449
+ class FixWikiRelatedNotesQuotaWaitOutcome(ContractModel):
450
+ code: Literal["related_notes.quota_wait"] = "related_notes.quota_wait"
451
+
452
+
453
+ class FixWikiRelatedNotesObsidianNotReadyOutcome(ContractModel):
454
+ code: Literal["related_notes.obsidian_not_ready"] = "related_notes.obsidian_not_ready"
455
+
456
+
457
+ class FixWikiRelatedNotesBlockedOutcome(ContractModel):
458
+ code: Literal["related_notes.blocked"] = "related_notes.blocked"
459
+
460
+
461
+ class FixWikiLinkCompletedOutcome(ContractModel):
462
+ code: Literal["link.completed"] = "link.completed"
463
+
464
+
465
+ class FixWikiLinkBlockedOutcome(ContractModel):
466
+ code: Literal["link.blocked"] = "link.blocked"
467
+
468
+
469
+ class FixWikiLinkGraphBlockedOutcome(ContractModel):
470
+ code: Literal["graph_blocked"] = "graph_blocked"
471
+
472
+
473
+ class FixWikiLinkerBlockedOutcome(ContractModel):
474
+ code: Literal["linker_blocked"] = "linker_blocked"
475
+
476
+
477
+ class FixWikiFinalValidationPassedOutcome(ContractModel):
478
+ code: Literal["final_validation.passed"] = "final_validation.passed"
479
+
480
+
481
+ class FixWikiFinalValidationWarningsOutcome(ContractModel):
482
+ code: Literal["final_validation.warnings"] = "final_validation.warnings"
483
+
484
+
485
+ class FixWikiFinalValidationFoundMoreWorkOutcome(ContractModel):
486
+ code: Literal["final_validation.found_more_work"] = "final_validation.found_more_work"
487
+ pending_lanes: list[FixWikiDiagnosisLane] = Field(min_length=1)
488
+ selected_lane: FixWikiDiagnosisLane
489
+
490
+ @field_validator("pending_lanes")
491
+ @classmethod
492
+ def _reject_duplicate_lanes(cls, value: list[FixWikiDiagnosisLane]) -> list[FixWikiDiagnosisLane]:
493
+ if len(set(value)) != len(value):
494
+ raise ValueError("duplicate diagnosis lanes are not allowed")
495
+ expected_order = sorted(value, key=FIX_WIKI_DIAGNOSIS_PRIORITY.index)
496
+ if list(value) != expected_order:
497
+ raise ValueError("diagnosis lanes must follow FIX_WIKI_DIAGNOSIS_PRIORITY")
498
+ return value
499
+
500
+ @model_validator(mode="after")
501
+ def _selected_lane_must_be_priority_head(self) -> FixWikiFinalValidationFoundMoreWorkOutcome:
502
+ if self.selected_lane != self.pending_lanes[0]:
503
+ raise ValueError("selected_lane must match the highest-priority pending lane")
504
+ return self
505
+
506
+
507
+ class FixWikiFinalValidationFailedOutcome(ContractModel):
508
+ code: Literal["final_validation.failed"] = "final_validation.failed"
509
+
510
+
511
+ class FixWikiRollbackCompletedOutcome(ContractModel):
512
+ code: Literal["rollback.completed"] = "rollback.completed"
513
+
514
+
515
+ class FixWikiRollbackFailedOutcome(ContractModel):
516
+ code: Literal["rollback.failed"] = "rollback.failed"
517
+
518
+
519
+ FixWikiEffectOutcome = Annotated[
520
+ FixWikiSetupBootstrapReadyOutcome
521
+ | FixWikiSetupBootstrapBlockedOutcome
522
+ | FixWikiVaultGuardReadyOutcome
523
+ | FixWikiVaultGuardBlockedOutcome
524
+ | FixWikiDeterministicAppliedOutcome
525
+ | FixWikiDeterministicBlockedOutcome
526
+ | FixWikiStyleSpecialistCompletedOutcome
527
+ | FixWikiStyleCapacityWaitOutcome
528
+ | FixWikiStyleReviewRequiredOutcome
529
+ | FixWikiStyleApplyCompletedOutcome
530
+ | FixWikiStyleBlockedOutcome
531
+ | FixWikiTaxonomyDecisionRequiredOutcome
532
+ | FixWikiTaxonomyAppliedOutcome
533
+ | FixWikiTaxonomyBlockedOutcome
534
+ | FixWikiVocabularyCuratorCompletedOutcome
535
+ | FixWikiVocabularyEvalNeedsReviewOutcome
536
+ | FixWikiVocabularyAppliedOutcome
537
+ | FixWikiVocabularyIntegrityFailedOutcome
538
+ | FixWikiAtomicitySplitAppliedOutcome
539
+ | FixWikiAtomicityBlockedOutcome
540
+ | FixWikiMergeAppliedOutcome
541
+ | FixWikiMergeBlockedOutcome
542
+ | FixWikiRelatedNotesExportCompletedOutcome
543
+ | FixWikiRelatedNotesQuotaWaitOutcome
544
+ | FixWikiRelatedNotesObsidianNotReadyOutcome
545
+ | FixWikiRelatedNotesBlockedOutcome
546
+ | FixWikiLinkCompletedOutcome
547
+ | FixWikiLinkBlockedOutcome
548
+ | FixWikiLinkGraphBlockedOutcome
549
+ | FixWikiLinkerBlockedOutcome
550
+ | FixWikiFinalValidationPassedOutcome
551
+ | FixWikiFinalValidationWarningsOutcome
552
+ | FixWikiFinalValidationFoundMoreWorkOutcome
553
+ | FixWikiFinalValidationFailedOutcome
554
+ | FixWikiRollbackCompletedOutcome
555
+ | FixWikiRollbackFailedOutcome,
556
+ Field(discriminator="code"),
557
+ ]
558
+ FixWikiEffectOutcomeAdapter = TypeAdapter(FixWikiEffectOutcome)
559
+
560
+
561
+ class EffectReturnEventKey(NamedTuple):
562
+ kind: WorkflowEffectKind
563
+ target: str
564
+ origin_state: str
565
+ effect_code: str
566
+
567
+
568
+ # Exact effect-result matrix: no status/payload fallback is allowed to decide the
569
+ # next event. Adapter payloads may remain as opaque audit evidence only; this
570
+ # table is the domain-owned routing contract.
571
+ EFFECT_RETURN_EVENT_MATRIX: dict[EffectReturnEventKey, type[FixWikiEvent]] = {
572
+ EffectReturnEventKey(
573
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
574
+ "/mednotes:setup",
575
+ FixWikiState.ENVIRONMENT_PATHS_MISSING.value,
576
+ "setup.bootstrap.ready",
577
+ ): SetupBootstrapReadyEvent,
578
+ EffectReturnEventKey(
579
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
580
+ "/mednotes:setup",
581
+ FixWikiState.ENVIRONMENT_PATHS_MISSING.value,
582
+ "setup.bootstrap.blocked",
583
+ ): SetupBootstrapBlockedEvent,
584
+ EffectReturnEventKey(
585
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
586
+ "/mednotes:setup",
587
+ FixWikiState.ENVIRONMENT_WIKI_DIR_MISSING.value,
588
+ "setup.bootstrap.ready",
589
+ ): SetupBootstrapReadyEvent,
590
+ EffectReturnEventKey(
591
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
592
+ "/mednotes:setup",
593
+ FixWikiState.ENVIRONMENT_WIKI_DIR_MISSING.value,
594
+ "setup.bootstrap.blocked",
595
+ ): SetupBootstrapBlockedEvent,
596
+ EffectReturnEventKey(
597
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
598
+ "/mednotes:setup",
599
+ FixWikiState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED.value,
600
+ "setup.bootstrap.ready",
601
+ ): SetupBootstrapReadyEvent,
602
+ EffectReturnEventKey(
603
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
604
+ "/mednotes:setup",
605
+ FixWikiState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED.value,
606
+ "setup.bootstrap.blocked",
607
+ ): SetupBootstrapBlockedEvent,
608
+ EffectReturnEventKey(
609
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
610
+ "fix_wiki.vault_guard",
611
+ FixWikiState.VAULT_GUARD_RUNNING.value,
612
+ "vault_guard.ready",
613
+ ): VaultGuardReadyEvent,
614
+ EffectReturnEventKey(
615
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
616
+ "fix_wiki.vault_guard",
617
+ FixWikiState.VAULT_GUARD_RUNNING.value,
618
+ "vault_guard.blocked",
619
+ ): VaultGuardBlockedEvent,
620
+ EffectReturnEventKey(
621
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
622
+ "fix_wiki.deterministic_repairs",
623
+ FixWikiState.DETERMINISTIC_REPAIRS_RUNNING.value,
624
+ "deterministic.applied",
625
+ ): DeterministicRepairsAppliedEvent,
626
+ EffectReturnEventKey(
627
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
628
+ "fix_wiki.deterministic_repairs",
629
+ FixWikiState.DETERMINISTIC_REPAIRS_RUNNING.value,
630
+ "deterministic.blocked",
631
+ ): DeterministicRepairsBlockedEvent,
632
+ EffectReturnEventKey(
633
+ WorkflowEffectKind.CALL_SPECIALIST_MODEL,
634
+ "med-knowledge-architect",
635
+ FixWikiState.STYLE_REWRITE_SPECIALIST_REQUESTED.value,
636
+ "style.specialist_completed",
637
+ ): StyleRewriteSpecialistCompletedEvent,
638
+ EffectReturnEventKey(
639
+ WorkflowEffectKind.CALL_SPECIALIST_MODEL,
640
+ "med-knowledge-architect",
641
+ FixWikiState.STYLE_REWRITE_SPECIALIST_REQUESTED.value,
642
+ "style.capacity_wait",
643
+ ): StyleRewriteCapacityWaitEvent,
644
+ EffectReturnEventKey(
645
+ WorkflowEffectKind.CALL_SPECIALIST_MODEL,
646
+ "med-knowledge-architect",
647
+ FixWikiState.STYLE_REWRITE_SPECIALIST_REQUESTED.value,
648
+ "style.review_required",
649
+ ): StyleRewriteReviewRequiredEvent,
650
+ EffectReturnEventKey(
651
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
652
+ "fix_wiki.style_rewrite_apply",
653
+ FixWikiState.STYLE_REWRITE_APPLY_RUNNING.value,
654
+ "style.apply_completed",
655
+ ): StyleRewriteAppliedEvent,
656
+ EffectReturnEventKey(
657
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
658
+ "fix_wiki.style_rewrite_apply",
659
+ FixWikiState.STYLE_REWRITE_APPLY_RUNNING.value,
660
+ "style.blocked",
661
+ ): StyleRewriteBlockedEvent,
662
+ EffectReturnEventKey(
663
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
664
+ "fix_wiki.taxonomy",
665
+ FixWikiState.TAXONOMY_APPLY_RUNNING.value,
666
+ "taxonomy.decision_required",
667
+ ): TaxonomyDecisionRequiredEvent,
668
+ EffectReturnEventKey(
669
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
670
+ "fix_wiki.taxonomy",
671
+ FixWikiState.TAXONOMY_APPLY_RUNNING.value,
672
+ "taxonomy.applied",
673
+ ): TaxonomyAppliedEvent,
674
+ EffectReturnEventKey(
675
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
676
+ "fix_wiki.taxonomy",
677
+ FixWikiState.TAXONOMY_APPLY_RUNNING.value,
678
+ "taxonomy.blocked",
679
+ ): TaxonomyBlockedEvent,
680
+ EffectReturnEventKey(
681
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
682
+ "fix_wiki.vocabulary_curator",
683
+ FixWikiState.VOCABULARY_CURATOR_RUNNING.value,
684
+ "vocabulary.curator_completed",
685
+ ): VocabularyCuratorCompletedEvent,
686
+ EffectReturnEventKey(
687
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
688
+ "fix_wiki.vocabulary_curator",
689
+ FixWikiState.VOCABULARY_SEMANTIC_INGESTION_PENDING.value,
690
+ "vocabulary.curator_completed",
691
+ ): VocabularyCuratorCompletedEvent,
692
+ EffectReturnEventKey(
693
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
694
+ "fix_wiki.vocabulary_eval",
695
+ FixWikiState.VOCABULARY_EVAL_RUNNING.value,
696
+ "vocabulary.eval_needs_review",
697
+ ): VocabularyEvalNeedsReviewEvent,
698
+ EffectReturnEventKey(
699
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
700
+ "fix_wiki.vocabulary_apply",
701
+ FixWikiState.VOCABULARY_APPLY_RUNNING.value,
702
+ "vocabulary.applied",
703
+ ): VocabularyAppliedEvent,
704
+ EffectReturnEventKey(
705
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
706
+ "fix_wiki.vocabulary_apply",
707
+ FixWikiState.VOCABULARY_APPLY_RUNNING.value,
708
+ "vocabulary.integrity_failed",
709
+ ): VocabularyIntegrityFailedEvent,
710
+ EffectReturnEventKey(
711
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
712
+ "fix_wiki.atomicity_split",
713
+ FixWikiState.ATOMICITY_SPLIT_RUNNING.value,
714
+ "atomicity.split_applied",
715
+ ): AtomicitySplitAppliedEvent,
716
+ EffectReturnEventKey(
717
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
718
+ "fix_wiki.atomicity_split",
719
+ FixWikiState.ATOMICITY_SPLIT_RUNNING.value,
720
+ "atomicity.blocked",
721
+ ): AtomicitySplitBlockedEvent,
722
+ EffectReturnEventKey(
723
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
724
+ "fix_wiki.note_merge",
725
+ FixWikiState.MERGE_RUNNING.value,
726
+ "merge.applied",
727
+ ): MergeAppliedEvent,
728
+ EffectReturnEventKey(
729
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
730
+ "fix_wiki.note_merge",
731
+ FixWikiState.MERGE_RUNNING.value,
732
+ "merge.blocked",
733
+ ): MergeBlockedEvent,
734
+ EffectReturnEventKey(
735
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
736
+ "related_notes.export",
737
+ FixWikiState.RELATED_NOTES_EXPORT_RUNNING.value,
738
+ "related_notes.export_completed",
739
+ ): RelatedNotesExportCompletedEvent,
740
+ EffectReturnEventKey(
741
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
742
+ "related_notes.export",
743
+ FixWikiState.RELATED_NOTES_EXPORT_RUNNING.value,
744
+ "related_notes.quota_wait",
745
+ ): RelatedNotesQuotaWaitEvent,
746
+ EffectReturnEventKey(
747
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
748
+ "related_notes.export",
749
+ FixWikiState.RELATED_NOTES_EXPORT_RUNNING.value,
750
+ "related_notes.obsidian_not_ready",
751
+ ): RelatedNotesObsidianNotReadyEvent,
752
+ EffectReturnEventKey(
753
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
754
+ "related_notes.export",
755
+ FixWikiState.RELATED_NOTES_EXPORT_RUNNING.value,
756
+ "related_notes.blocked",
757
+ ): RelatedNotesBlockedEvent,
758
+ EffectReturnEventKey(
759
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
760
+ "/mednotes:link",
761
+ FixWikiState.LINK_RUN_REQUESTED.value,
762
+ "link.completed",
763
+ ): LinkCompletedEvent,
764
+ EffectReturnEventKey(
765
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
766
+ "/mednotes:link",
767
+ FixWikiState.LINK_RUN_REQUESTED.value,
768
+ "link.blocked",
769
+ ): LinkerBlockedEvent,
770
+ EffectReturnEventKey(
771
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
772
+ "/mednotes:link",
773
+ FixWikiState.LINK_RUN_REQUESTED.value,
774
+ "graph_blocked",
775
+ ): LinkGraphBlockedEvent,
776
+ EffectReturnEventKey(
777
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
778
+ "/mednotes:link",
779
+ FixWikiState.LINK_RUN_REQUESTED.value,
780
+ "linker_blocked",
781
+ ): LinkerBlockedEvent,
782
+ EffectReturnEventKey(
783
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
784
+ "/mednotes:link",
785
+ FixWikiState.LINK_RUN_REQUESTED.value,
786
+ "related_notes.quota_wait",
787
+ ): RelatedNotesQuotaWaitEvent,
788
+ EffectReturnEventKey(
789
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
790
+ "fix_wiki.final_validation",
791
+ FixWikiState.FINAL_VALIDATION_RUNNING.value,
792
+ "final_validation.passed",
793
+ ): FinalValidationPassedEvent,
794
+ EffectReturnEventKey(
795
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
796
+ "fix_wiki.final_validation",
797
+ FixWikiState.FINAL_VALIDATION_RUNNING.value,
798
+ "final_validation.warnings",
799
+ ): FinalValidationWarningsEvent,
800
+ EffectReturnEventKey(
801
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
802
+ "fix_wiki.final_validation",
803
+ FixWikiState.FINAL_VALIDATION_RUNNING.value,
804
+ "final_validation.found_more_work",
805
+ ): FinalValidationFoundMoreWorkEvent,
806
+ EffectReturnEventKey(
807
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
808
+ "fix_wiki.final_validation",
809
+ FixWikiState.FINAL_VALIDATION_RUNNING.value,
810
+ "final_validation.failed",
811
+ ): FinalValidationFailedEvent,
812
+ EffectReturnEventKey(
813
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
814
+ "fix_wiki.rollback",
815
+ FixWikiState.ROLLBACK_RUNNING.value,
816
+ "rollback.completed",
817
+ ): RollbackCompletedEvent,
818
+ EffectReturnEventKey(
819
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
820
+ "fix_wiki.rollback",
821
+ FixWikiState.ROLLBACK_RUNNING.value,
822
+ "rollback.failed",
823
+ ): RollbackFailedEvent,
824
+ }
825
+
826
+
827
+ def fix_wiki_event_from_effect_result(model: object, result: WorkflowEffectResult) -> FixWikiEvent:
828
+ """Convert one typed effect result into the next StateChart event.
829
+
830
+ `model` is accepted for the future persisted-run integration; the current
831
+ routing contract is intentionally derived from the effect identity and the
832
+ domain outcome code only.
833
+ """
834
+
835
+ del model
836
+ outcome = FixWikiEffectOutcomeAdapter.validate_python(result.outcome.to_payload())
837
+ key = EffectReturnEventKey(
838
+ result.effect.kind,
839
+ result.effect.target,
840
+ result.effect.origin_state,
841
+ outcome.code,
842
+ )
843
+ event_cls = EFFECT_RETURN_EVENT_MATRIX.get(key)
844
+ if event_cls is None:
845
+ raise ValueError(f"missing fix-wiki effect return matrix row: {key}")
846
+ payload: JsonObject = {
847
+ "workflow": result.effect.workflow or FIX_WIKI_WORKFLOW,
848
+ "run_id": result.effect.run_id,
849
+ "current_state": result.effect.origin_state,
850
+ }
851
+ if isinstance(outcome, FixWikiFinalValidationFoundMoreWorkOutcome):
852
+ payload["pending_lanes"] = [lane.value for lane in outcome.pending_lanes]
853
+ payload["selected_lane"] = outcome.selected_lane.value
854
+ if event_cls is RelatedNotesQuotaWaitEvent:
855
+ recovery = _effect_related_notes_recovery_state(result)
856
+ if recovery is not None:
857
+ payload["related_notes_recovery_state"] = recovery
858
+ return event_cls.model_validate(payload)
859
+
860
+
861
+ def facts_after_effect_results(
862
+ facts: FixWikiFsmFacts,
863
+ effect_results: list[WorkflowEffectResult],
864
+ ) -> FixWikiFsmFacts:
865
+ """Fold typed effect results back into canonical fix-wiki facts.
866
+
867
+ The public workflow facade may execute the effects, but effect status
868
+ interpretation lives beside the effect contract so `health.py` does not
869
+ become a second recovery-state machine.
870
+ """
871
+
872
+ if not effect_results:
873
+ return facts
874
+ statechart_facts = _facts_after_statechart_effect_results(facts, effect_results)
875
+ if statechart_facts is not None:
876
+ return statechart_facts
877
+ last = effect_results[-1]
878
+ diagnostics = _scrub_fsm_diagnostic_context(facts.diagnostic_context)
879
+ diagnostics["effect_results"] = [result.to_payload() for result in effect_results]
880
+ match last.status:
881
+ case WorkflowEffectStatus.WAITING_EXTERNAL:
882
+ update = FixWikiEffectRuntimeUpdate(
883
+ external_wait_reason_code=_effect_external_wait_reason(last),
884
+ external_wait_resume_action=last.resume_action or last.next_action,
885
+ external_wait_payload=last.to_payload(),
886
+ next_action=last.next_action or last.resume_action,
887
+ diagnostic_context=diagnostics,
888
+ )
889
+ recovery_state = _effect_related_notes_recovery_state(last)
890
+ if _is_related_notes_effect(last.effect) or recovery_state is not None:
891
+ update_payload = {
892
+ **update.model_dump(mode="python"),
893
+ "related_notes_blocked": True,
894
+ }
895
+ if recovery_state is not None:
896
+ update_payload["related_notes_recovery_state"] = recovery_state.to_payload()
897
+ update = FixWikiEffectRuntimeUpdate.model_validate(update_payload)
898
+ return facts.with_runtime_updates(update.to_runtime_update())
899
+ case WorkflowEffectStatus.WAITING_HUMAN:
900
+ decision = _workflow_decision_from_waiting_human_effect(last)
901
+ human_decision_required = decision.kind == "ask_human"
902
+ update = FixWikiEffectRuntimeUpdate(
903
+ human_decision_required=human_decision_required,
904
+ decision=decision,
905
+ human_decision_packet=last.human_decision_packet,
906
+ pending_effects=[],
907
+ next_action=last.next_action or last.resume_action or decision.next_action,
908
+ diagnostic_context=diagnostics,
909
+ error_context=last.error_context,
910
+ )
911
+ return facts.with_runtime_updates(update.to_runtime_update())
912
+ case WorkflowEffectStatus.BLOCKED:
913
+ update = _blocked_effect_runtime_update(last)
914
+ update = FixWikiEffectRuntimeUpdate.model_validate(
915
+ {**update.model_dump(mode="python"), "diagnostic_context": diagnostics}
916
+ )
917
+ return facts.with_runtime_updates(update.to_runtime_update())
918
+ case WorkflowEffectStatus.FAILED:
919
+ update = FixWikiEffectRuntimeUpdate(
920
+ failed=True,
921
+ failed_reason_code=_effect_result_reason(last, fallback="workflow_effect_failed"),
922
+ next_action=last.next_action,
923
+ diagnostic_context=diagnostics,
924
+ error_context=last.error_context,
925
+ )
926
+ return facts.with_runtime_updates(update.to_runtime_update())
927
+ case _:
928
+ update = FixWikiEffectRuntimeUpdate(diagnostic_context=diagnostics)
929
+ return facts.with_runtime_updates(update.to_runtime_update())
930
+
931
+
932
+ def _facts_after_statechart_effect_results(
933
+ facts: FixWikiFsmFacts,
934
+ effect_results: list[WorkflowEffectResult],
935
+ ) -> FixWikiFsmFacts | None:
936
+ """Apply effect outcomes as first-class FixWikiMachine events.
937
+
938
+ Adapters may execute effects, but they do not decide the post-effect state.
939
+ A typed domain outcome must map through `EFFECT_RETURN_EVENT_MATRIX` and then
940
+ through python-statemachine before it can become public workflow state.
941
+ """
942
+
943
+ driving_results = [result for result in effect_results if _effect_result_drives_statechart(result)]
944
+ if not driving_results:
945
+ return None
946
+
947
+ diagnostics = _scrub_fsm_diagnostic_context(facts.diagnostic_context)
948
+ diagnostics["effect_results"] = [result.to_payload() for result in effect_results]
949
+ model = WorkflowModel.start(
950
+ workflow=FIX_WIKI_WORKFLOW,
951
+ run_id=facts.run_id,
952
+ initial_state=facts.initial_state.value,
953
+ )
954
+ machine = FixWikiMachine(model=model, state_field=WorkflowModel.STATECHART_STATE_FIELD)
955
+ send_workflow_event(machine, facts.event)
956
+ last_event: FixWikiEvent = facts.event
957
+ last_result: WorkflowEffectResult | None = None
958
+ for result in driving_results:
959
+ last_event = fix_wiki_event_from_effect_result(model, result)
960
+ send_workflow_event(machine, FixWikiBoundaryEventAdapter.validate_python(last_event.to_payload()))
961
+ last_result = result
962
+
963
+ runtime_update: dict[str, object] = {
964
+ "diagnostic_context": diagnostics,
965
+ "pending_effects": list(model.pending_effects),
966
+ }
967
+ if isinstance(last_event, RelatedNotesQuotaWaitEvent) and last_event.related_notes_recovery_state.status:
968
+ runtime_update.update(
969
+ {
970
+ "related_notes_blocked": True,
971
+ "related_notes_recovery_state": last_event.related_notes_recovery_state.to_payload(),
972
+ "external_wait_reason_code": last_event.related_notes_recovery_state.blocked_reason,
973
+ "external_wait_resume_action": last_event.related_notes_recovery_state.next_action,
974
+ }
975
+ )
976
+ elif isinstance(last_event, StyleRewriteCapacityWaitEvent) and last_result is not None:
977
+ resume_action = last_result.resume_action or last_result.next_action
978
+ runtime_update.update(
979
+ {
980
+ "external_wait_reason_code": _effect_result_reason(
981
+ last_result,
982
+ fallback="specialist_model_capacity_unavailable",
983
+ ),
984
+ "external_wait_resume_action": resume_action,
985
+ "external_wait_payload": last_result.to_payload(),
986
+ "next_action": last_result.next_action or resume_action,
987
+ "error_context": last_result.error_context,
988
+ }
989
+ )
990
+ runtime = facts.runtime.__class__.model_validate(
991
+ {**facts.runtime.model_dump(mode="python"), **runtime_update}
992
+ )
993
+ event = FixWikiBoundaryEventAdapter.validate_python(last_event.to_payload())
994
+ return FixWikiFsmFacts(
995
+ run_id=facts.run_id,
996
+ initial_state=FixWikiState(event.current_state),
997
+ event=event,
998
+ runtime=runtime,
999
+ machine_effects=list(model.pending_effects),
1000
+ )
1001
+
1002
+
1003
+ def _effect_result_drives_statechart(result: WorkflowEffectResult) -> bool:
1004
+ """Return true for effect kinds whose outcomes represent a new FSM event."""
1005
+
1006
+ if result.effect.kind in {WorkflowEffectKind.WAIT_EXTERNAL, WorkflowEffectKind.ASK_HUMAN}:
1007
+ return False
1008
+ return result.status in {
1009
+ WorkflowEffectStatus.COMPLETED,
1010
+ WorkflowEffectStatus.COMPLETED_WITH_WARNINGS,
1011
+ WorkflowEffectStatus.WAITING_AGENT,
1012
+ WorkflowEffectStatus.WAITING_EXTERNAL,
1013
+ WorkflowEffectStatus.WAITING_HUMAN,
1014
+ WorkflowEffectStatus.BLOCKED,
1015
+ WorkflowEffectStatus.FAILED,
1016
+ }
1017
+
1018
+
1019
+ def _scrub_fsm_diagnostic_context(context: JsonObject) -> JsonObject:
1020
+ """Keep diagnostics explanatory; executable routes live in FSM root contracts."""
1021
+
1022
+ diagnostics = dict(context)
1023
+ for key in (
1024
+ "orchestration_plan",
1025
+ "continuation_plan",
1026
+ "pending_effects",
1027
+ "action_directives",
1028
+ ):
1029
+ diagnostics.pop(key, None)
1030
+ return diagnostics
1031
+
1032
+
1033
+ def _effect_result_reason(result: WorkflowEffectResult, *, fallback: str) -> str:
1034
+ """Return the typed reason from an effect result without reading diagnostics."""
1035
+
1036
+ error_context = _FixWikiEffectErrorContextFields.model_validate(result.error_context)
1037
+ for value in (
1038
+ error_context.blocked_reason,
1039
+ error_context.root_cause,
1040
+ getattr(result.outcome, "reason_code", ""),
1041
+ ):
1042
+ text = value.strip() if isinstance(value, str) else ""
1043
+ if text:
1044
+ return text
1045
+ return fallback
1046
+
1047
+
1048
+ def _blocked_effect_runtime_update(result: WorkflowEffectResult) -> FixWikiEffectRuntimeUpdate:
1049
+ """Classify blocked effect results by the effect that actually blocked."""
1050
+
1051
+ reason = _effect_result_reason(result, fallback="workflow_effect_blocked")
1052
+ if result.effect.kind == WorkflowEffectKind.RUN_SUBWORKFLOW and result.effect.target == "/mednotes:link":
1053
+ return FixWikiEffectRuntimeUpdate(
1054
+ next_action=result.next_action,
1055
+ error_context=result.error_context,
1056
+ linker_blocked=True,
1057
+ )
1058
+ if _is_related_notes_effect(result.effect):
1059
+ recovery_state = _effect_related_notes_recovery_state(result)
1060
+ return FixWikiEffectRuntimeUpdate(
1061
+ next_action=result.next_action,
1062
+ error_context=result.error_context,
1063
+ related_notes_blocked=True,
1064
+ related_notes_recovery_state=recovery_state.to_payload() if recovery_state is not None else {},
1065
+ )
1066
+ return FixWikiEffectRuntimeUpdate(
1067
+ next_action=result.next_action,
1068
+ error_context=result.error_context,
1069
+ failed=True,
1070
+ failed_reason_code=reason,
1071
+ )
1072
+
1073
+
1074
+ def _workflow_decision_from_waiting_human_effect(result: WorkflowEffectResult) -> WorkflowDecision:
1075
+ """Convert a typed ask-human effect result into the fix-wiki decision root."""
1076
+
1077
+ packet = result.human_decision_packet
1078
+ if packet is None:
1079
+ raise ValueError("waiting_human effect result requires human_decision_packet")
1080
+ next_action = result.next_action or result.resume_action or packet.resume_action
1081
+ return decision_from_payload(
1082
+ {
1083
+ "human_decision_packet": packet.to_payload(),
1084
+ "next_action": next_action,
1085
+ }
1086
+ )
1087
+
1088
+
1089
+ def _effect_related_notes_recovery_state(result: WorkflowEffectResult) -> RelatedNotesRecoveryStateEffectPayload | None:
1090
+ """Return typed Related Notes recovery state from official effect payloads only."""
1091
+
1092
+ try:
1093
+ wait_payload = WaitExternalEffectPayload.from_effect_payload(result.payload)
1094
+ except ValueError:
1095
+ wait_payload = None
1096
+ if wait_payload is not None and wait_payload.related_notes_recovery_state is not None:
1097
+ return wait_payload.related_notes_recovery_state
1098
+ try:
1099
+ recovery_payload = RelatedNotesRecoveryEffectPayload.from_operation_payload(result.payload)
1100
+ except ValueError:
1101
+ return _link_child_related_notes_recovery_state(result)
1102
+ return recovery_payload.related_notes_recovery_state
1103
+
1104
+
1105
+ def _link_child_related_notes_recovery_state(
1106
+ result: WorkflowEffectResult,
1107
+ ) -> RelatedNotesRecoveryStateEffectPayload | None:
1108
+ """Read Related Notes progress from a typed child `/mednotes:link` FSM result."""
1109
+
1110
+ if result.effect.kind != WorkflowEffectKind.RUN_SUBWORKFLOW or result.effect.target != "/mednotes:link":
1111
+ return None
1112
+ try:
1113
+ child_payload = _FixWikiLinkChildPayloadFields.model_validate(result.payload)
1114
+ except ValueError:
1115
+ return None
1116
+ return child_payload.diagnostic_context.related_notes_recovery_state
1117
+
1118
+
1119
+ def _effect_external_wait_reason(result: WorkflowEffectResult) -> str:
1120
+ error_context = _FixWikiEffectErrorContextFields.model_validate(result.error_context)
1121
+ if error_context.blocked_reason:
1122
+ return error_context.blocked_reason
1123
+ recovery_state = _effect_related_notes_recovery_state(result)
1124
+ if recovery_state is not None and recovery_state.blocked_reason:
1125
+ return recovery_state.blocked_reason
1126
+ if result.effect.kind == WorkflowEffectKind.CALL_SPECIALIST_MODEL:
1127
+ return "specialist_model_capacity_unavailable"
1128
+ if _is_related_notes_effect(result.effect):
1129
+ return "related_notes_headless_quota_exhausted"
1130
+ return "workflow_external_wait"
1131
+
1132
+
1133
+ def _is_related_notes_effect(effect: WorkflowEffect) -> bool:
1134
+ """Recognize Wiki-domain Related Notes work through target, not kernel kind."""
1135
+
1136
+ return effect.kind == WorkflowEffectKind.RUN_SUBWORKFLOW and effect.target in {
1137
+ "related_notes.export",
1138
+ "related_notes_export",
1139
+ "related_notes.section",
1140
+ }
1141
+
1142
+
1143
+ def style_rewrite_specialist_effect_from_request(
1144
+ request: FixWikiStyleRewriteSpecialistEffectRequest,
1145
+ ) -> WorkflowEffect:
1146
+ """Build the rich specialist-call effect from a typed fix-wiki request."""
1147
+
1148
+ batch_items = request.current_batch_items
1149
+ authoring_max_concurrency = min(request.authoring_max_concurrency, len(batch_items))
1150
+ return WorkflowEffect(
1151
+ workflow=FIX_WIKI_WORKFLOW,
1152
+ run_id=request.run_id,
1153
+ effect_id=f"style-rewrite-{request.work_id}",
1154
+ origin_state=FixWikiState.STYLE_REWRITE_SPECIALIST_REQUESTED.value,
1155
+ kind=WorkflowEffectKind.CALL_SPECIALIST_MODEL,
1156
+ target=request.agent_name,
1157
+ payload={
1158
+ "kind": "style_rewrite",
1159
+ "work_id": request.work_id,
1160
+ "agent": request.agent_name,
1161
+ "title": request.title,
1162
+ "execution_mode": "parallel_authoring_serial_apply",
1163
+ "authoring_mode": "parallel",
1164
+ "authoring_max_concurrency": authoring_max_concurrency,
1165
+ "apply_mode": "serial",
1166
+ "serial_apply_required": True,
1167
+ "wait_for_all_authoring_outputs_before_apply": True,
1168
+ "current_batch_items": batch_items,
1169
+ "current_batch_item_count": len(batch_items),
1170
+ "plan_path": request.plan_path,
1171
+ "manifest_path": request.manifest_path,
1172
+ "style_rewrite_plan_path": request.plan_path,
1173
+ "style_rewrite_manifest_path": request.manifest_path,
1174
+ },
1175
+ requires_receipt=True,
1176
+ requires_attestation=True,
1177
+ model_policy={
1178
+ "policy": "medical_specialist_authoring.v1",
1179
+ "required_model_tier": "specialist",
1180
+ "preferred_model_tier": "pro",
1181
+ "forbid_flash_fallback": True,
1182
+ },
1183
+ )
1184
+
1185
+
1186
+ def pending_effect_payloads_from_fix_wiki_runtime_source(source: object) -> list[JsonObject]:
1187
+ """Derive executable pending effects from typed runtime facts in the effects layer."""
1188
+
1189
+ fields = _StyleRewritePendingEffectSource.model_validate(source)
1190
+ if not fields.requested_apply or not fields.effective_apply:
1191
+ return []
1192
+ rewrite_plan = fields.style_rewrite_plan
1193
+ if rewrite_plan is None:
1194
+ return []
1195
+ typed_plan = _StyleRewritePlanForEffect.model_validate(rewrite_plan)
1196
+ plan_payload = _StyleRewritePlanPayloadForEffect.model_validate(rewrite_plan)
1197
+ if typed_plan.status != "ready":
1198
+ return []
1199
+ if fields.requires_llm_rewrite_count <= 0:
1200
+ return []
1201
+ blocker_resolution = fields.blocker_resolution
1202
+ if blocker_resolution is None or not _has_automatic_style_rewrite_group_for_effect(blocker_resolution):
1203
+ return []
1204
+ work_items = list(typed_plan.work_items)
1205
+ if not work_items:
1206
+ return []
1207
+ first_item = work_items[0]
1208
+ work_id = first_item.work_id.strip()
1209
+ target_path = first_item.target_path.strip()
1210
+ if not work_id or not target_path:
1211
+ return []
1212
+ agent_name = first_item.agent or typed_plan.agent or "med-knowledge-architect"
1213
+ current_batch_items: list[JsonObject] = []
1214
+ for index, work_item in enumerate(work_items):
1215
+ item_payload = (
1216
+ plan_payload.work_items[index]
1217
+ if index < len(plan_payload.work_items)
1218
+ else JsonObjectAdapter.validate_python(work_item.model_dump(exclude_defaults=True, exclude_none=True))
1219
+ )
1220
+ current_batch_items.append(item_payload)
1221
+ effect = style_rewrite_specialist_effect_from_request(
1222
+ FixWikiStyleRewriteSpecialistEffectRequest(
1223
+ run_id=fields.run_id,
1224
+ work_id=work_id,
1225
+ target_path=target_path,
1226
+ agent_name=agent_name,
1227
+ title=first_item.title,
1228
+ plan_path=fields.style_rewrite_plan_path,
1229
+ manifest_path=fields.style_rewrite_manifest_path,
1230
+ current_batch_items=current_batch_items,
1231
+ authoring_max_concurrency=typed_plan.max_concurrency or len(current_batch_items),
1232
+ )
1233
+ )
1234
+ return [effect.to_payload()]
1235
+
1236
+
1237
+ def _has_automatic_style_rewrite_group_for_effect(
1238
+ blocker_resolution: _StyleRewriteBlockerResolutionForEffect,
1239
+ ) -> bool:
1240
+ return any(group.route == "style_rewrite" and group.automatic for group in blocker_resolution.groups)
1241
+
1242
+
1243
+ def missing_fix_wiki_effect_adapter_is_optional(effect: WorkflowEffect) -> bool:
1244
+ """Return true only for agent-mediated effects that may wait for the agent."""
1245
+
1246
+ return effect.kind in {
1247
+ WorkflowEffectKind.ASK_HUMAN,
1248
+ WorkflowEffectKind.CALL_SPECIALIST_MODEL,
1249
+ }
1250
+
1251
+
1252
+ def effect_result_stops_fix_wiki_execution(result: WorkflowEffectResult) -> bool:
1253
+ """Return whether an effect result should pause the current fix-wiki pass."""
1254
+
1255
+ return result.status in {
1256
+ WorkflowEffectStatus.WAITING_EXTERNAL,
1257
+ WorkflowEffectStatus.WAITING_HUMAN,
1258
+ WorkflowEffectStatus.BLOCKED,
1259
+ WorkflowEffectStatus.FAILED,
1260
+ }