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,3097 @@
1
+ """Operational StateChart for `/mednotes:process-chats`.
2
+
3
+ This module is pure domain code: it defines workflow states, typed boundary
4
+ events and executable effect intents. Actions never execute IO; adapters consume
5
+ the emitted `WorkflowEffect` objects outside the statechart.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Callable
10
+ from dataclasses import dataclass
11
+ from enum import StrEnum
12
+ from typing import Annotated, ClassVar, Literal, TypedDict
13
+
14
+ from pydantic import Field, TypeAdapter, field_validator
15
+ from statemachine import StateChart
16
+ from statemachine.states import States
17
+
18
+ from mednotes.domains.wiki.contracts.effect_payloads import LinkWorkflowRunEffectPayload, WaitExternalEffectPayload
19
+ from mednotes.kernel.base import ContractModel, JsonObject
20
+ from mednotes.kernel.effects import WorkflowEffect, WorkflowEffectKind, WorkflowEffectResult
21
+ from mednotes.kernel.fsm_model import WorkflowModel
22
+ from mednotes.kernel.fsm_transition_result import WorkflowTransitionResult
23
+ from mednotes.kernel.state_machine import WorkflowStateCategory
24
+ from mednotes.kernel.workflow import (
25
+ DecisionEvidence,
26
+ HumanDecisionOption,
27
+ HumanDecisionPacket,
28
+ RejectedAutomation,
29
+ WorkflowAutomationKind,
30
+ WorkflowDecision,
31
+ WorkflowDecisionKind,
32
+ )
33
+
34
+ PROCESS_CHATS_WORKFLOW: Literal["/mednotes:process-chats"] = "/mednotes:process-chats"
35
+
36
+
37
+ class _ProcessChatsEventCommonKwargs(TypedDict):
38
+ workflow: str
39
+ run_id: str
40
+ current_state: str
41
+
42
+
43
+ class ProcessChatsState(StrEnum):
44
+ ENVIRONMENT_CHECKING = "environment.checking"
45
+ ENVIRONMENT_PATHS_MISSING = "environment.paths_missing"
46
+ ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED = "environment.windows_path_or_venv_blocked"
47
+ BACKLOG_NO_PENDING_RAW_CHATS = "backlog.no_pending_raw_chats"
48
+ BACKLOG_NO_TRIAGED_RAW_CHATS = "backlog.no_triaged_raw_chats"
49
+ BACKLOG_TRIAGED_RAW_CHATS_READY = "backlog.triaged_raw_chats_ready"
50
+ VAULT_GUARD_DECISION_REQUIRED = "vault_guard.decision_required"
51
+ VAULT_GUARD_REJECTED = "vault_guard.rejected"
52
+ TRIAGE_PLANNING = "triage.planning"
53
+ ARCHITECT_WORK_REQUESTED = "architect.work_requested"
54
+ ARCHITECT_AWAITING_SPECIALIST_CAPACITY = "architect.awaiting_specialist_capacity"
55
+ ARCHITECT_REVIEWING_OUTPUT = "architect.reviewing_output"
56
+ SUBAGENT_PLAN_ATTESTATION_REQUIRED = "subagent_plan_attestation.required"
57
+ SUBAGENT_PLAN_ATTESTATION_INVALID = "subagent_plan_attestation.invalid"
58
+ NOTE_VALIDATION_RUNNING = "note_validation.running"
59
+ NOTE_VALIDATION_COVERAGE_GAP = "note_validation.coverage_gap"
60
+ NOTE_VALIDATION_MANIFEST_MISMATCH = "note_validation.manifest_mismatch"
61
+ NOTE_VALIDATION_CONTENT_INVALID = "note_validation.content_invalid"
62
+ STAGING_MANIFEST_READY = "staging.manifest_ready"
63
+ PUBLISH_AWAITING_CONFIRMATION = "publish.awaiting_confirmation"
64
+ PUBLISH_CANCELLED_BY_HUMAN = "publish.cancelled_by_human"
65
+ PUBLISH_APPLY_REQUESTED = "publish.apply_requested"
66
+ PUBLISH_PAUSED_FOR_QUOTA = "publish.paused_for_quota"
67
+ PUBLISH_DRY_RUN_RECEIPT_REQUIRED = "publish.dry_run_receipt_required"
68
+ PUBLISH_STALE_RECEIPT = "publish.stale_receipt"
69
+ PUBLISH_DUPLICATE_TARGET = "publish.duplicate_target"
70
+ PUBLISH_PROVENANCE_GAP = "publish.provenance_gap"
71
+ PUBLISH_RECEIPT_INVALID = "publish.receipt_invalid"
72
+ LINK_RUN_REQUESTED = "link.run_requested"
73
+ CONTRACT_GAP_MISSING_NEXT_ACTION = "contract_gap.missing_next_action"
74
+ CONTRACT_GAP_MISSING_ERROR_CONTEXT = "contract_gap.missing_error_context"
75
+ AGENT_TOOL_CONTRACT_VIOLATION = "agent_tool_contract_violation"
76
+ ROLLBACK_RECORDED = "rollback.recorded"
77
+ PUBLISHED = "published"
78
+ COMPLETED_WITH_LINK_BLOCKERS = "completed_with_link_blockers"
79
+ TERMINAL_FAILURE_RECORDED = "terminal.failure_recorded"
80
+
81
+
82
+ class ProcessChatsErrorContext(ContractModel):
83
+ """Typed problem context passed across retries without reading raw payloads."""
84
+
85
+ root_cause: str = Field(min_length=1)
86
+ affected_artifact: str = Field(min_length=1)
87
+ retry_scope: str = Field(min_length=1)
88
+ next_action: str = Field(min_length=1)
89
+
90
+
91
+ class ProcessChatsEvent(ContractModel):
92
+ """Base event for process-chats facts accepted by the StateChart."""
93
+
94
+ workflow: str = PROCESS_CHATS_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_process_chats(cls, value: str) -> str:
102
+ if value != PROCESS_CHATS_WORKFLOW:
103
+ raise ValueError(f"process-chats event workflow must be {PROCESS_CHATS_WORKFLOW}")
104
+ return value
105
+
106
+
107
+ def _event_name(event: ProcessChatsEvent) -> str:
108
+ """Return the concrete Literal discriminator declared by each event class."""
109
+
110
+ name = getattr(event, "name", "")
111
+ if not isinstance(name, str) or not name.strip():
112
+ raise ValueError("process-chats events must declare a name discriminator")
113
+ return name
114
+
115
+
116
+ class EnvironmentCheckedEvent(ProcessChatsEvent):
117
+ name: Literal["environment_checked"] = "environment_checked"
118
+ wiki_dir_configured: bool
119
+ raw_dir_configured: bool
120
+
121
+
122
+ class PathsMissingEvent(ProcessChatsEvent):
123
+ name: Literal["paths_missing"] = "paths_missing"
124
+ reason_code: Literal["paths_missing", "wiki_dir_missing"]
125
+ missing_path_kind: Literal["wiki_dir", "raw_dir", "both"]
126
+ setup_target: Literal["/mednotes:setup paths"]
127
+
128
+
129
+ class PathsConfiguredEvent(ProcessChatsEvent):
130
+ name: Literal["paths_configured"] = "paths_configured"
131
+ config_path: str = Field(min_length=1)
132
+
133
+
134
+ class WindowsPathOrVenvBlockedEvent(ProcessChatsEvent):
135
+ name: Literal["windows_path_or_venv_blocked"] = "windows_path_or_venv_blocked"
136
+ reason_code: Literal["environment_blocker.windows_path_or_venv"]
137
+ setup_target: Literal["/mednotes:setup bootstrap"]
138
+ error_context: ProcessChatsErrorContext
139
+
140
+
141
+ class EnvironmentBootstrapCompletedEvent(ProcessChatsEvent):
142
+ name: Literal["environment_bootstrap_completed"] = "environment_bootstrap_completed"
143
+ bootstrap_summary: str = Field(min_length=1)
144
+
145
+
146
+ class NoPendingRawChatsEvent(ProcessChatsEvent):
147
+ name: Literal["no_pending_raw_chats"] = "no_pending_raw_chats"
148
+ triaged_count: int = Field(default=0, ge=0, strict=True)
149
+
150
+
151
+ class NoTriagedRawChatsEvent(ProcessChatsEvent):
152
+ name: Literal["no_triaged_raw_chats"] = "no_triaged_raw_chats"
153
+ pending_count: int = Field(default=0, ge=0, strict=True)
154
+
155
+
156
+ class TriagedRawChatsAvailableEvent(ProcessChatsEvent):
157
+ name: Literal["triaged_raw_chats_available"] = "triaged_raw_chats_available"
158
+ triaged_count: int = Field(ge=1, strict=True)
159
+
160
+
161
+ class TriagePlanCreatedEvent(ProcessChatsEvent):
162
+ name: Literal["triage_plan_created"] = "triage_plan_created"
163
+ note_plan_hash: str = Field(min_length=1)
164
+ raw_file_count: int = Field(ge=0, strict=True)
165
+ exhaustive: bool
166
+
167
+
168
+ class SubagentPlanAttestationMissingEvent(ProcessChatsEvent):
169
+ name: Literal["subagent_plan_attestation_missing"] = "subagent_plan_attestation_missing"
170
+ reason_code: Literal["subagent_plan_attestation_required"]
171
+ error_context: ProcessChatsErrorContext
172
+
173
+
174
+ class SubagentPlanAttestationInvalidEvent(ProcessChatsEvent):
175
+ name: Literal["subagent_plan_attestation_invalid"] = "subagent_plan_attestation_invalid"
176
+ reason_code: Literal["subagent_plan_attestation_invalid"]
177
+ error_context: ProcessChatsErrorContext
178
+
179
+
180
+ class SubagentPlanAttestationSuppliedEvent(ProcessChatsEvent):
181
+ name: Literal["subagent_plan_attestation_supplied"] = "subagent_plan_attestation_supplied"
182
+ attestation_hash: str = Field(min_length=1)
183
+
184
+
185
+ class ArchitectSpecialistCapacityBlockedEvent(ProcessChatsEvent):
186
+ name: Literal["architect_specialist_capacity_blocked"] = "architect_specialist_capacity_blocked"
187
+ reason_code: Literal["specialist_model_unavailable", "specialist_model_quota_exhausted"]
188
+ resume_action: str = Field(min_length=1)
189
+
190
+
191
+ class ArchitectSpecialistCapacityRestoredEvent(ProcessChatsEvent):
192
+ name: Literal["architect_specialist_capacity_restored"] = "architect_specialist_capacity_restored"
193
+ restored_by: str = Field(min_length=1)
194
+
195
+
196
+ class ArchitectWorkCompletedEvent(ProcessChatsEvent):
197
+ name: Literal["architect_work_completed"] = "architect_work_completed"
198
+ receipt_id: str = Field(min_length=1)
199
+ attestation_hash: str = Field(min_length=1)
200
+ coverage_path: str = Field(min_length=1)
201
+ manifest_path: str = Field(min_length=1)
202
+
203
+
204
+ class ArchitectOutputAcceptedEvent(ProcessChatsEvent):
205
+ name: Literal["architect_output_accepted"] = "architect_output_accepted"
206
+ coverage_path: str = Field(min_length=1)
207
+ manifest_path: str = Field(min_length=1)
208
+ note_plan_hash: str = Field(min_length=1)
209
+
210
+
211
+ class ArchitectOutputInvalidEvent(ProcessChatsEvent):
212
+ name: Literal["architect_output_invalid"] = "architect_output_invalid"
213
+ reason_code: str = Field(min_length=1)
214
+ error_context: ProcessChatsErrorContext
215
+
216
+
217
+ class NoteValidationCoverageGapEvent(ProcessChatsEvent):
218
+ name: Literal["note_validation_coverage_gap"] = "note_validation_coverage_gap"
219
+ reason_code: Literal["coverage_path_missing", "coverage_invalid"]
220
+ error_context: ProcessChatsErrorContext
221
+
222
+
223
+ class NoteValidationManifestMismatchEvent(ProcessChatsEvent):
224
+ name: Literal["note_validation_manifest_mismatch"] = "note_validation_manifest_mismatch"
225
+ reason_code: Literal["manifest_invalid", "manifest_mismatch"]
226
+ error_context: ProcessChatsErrorContext
227
+
228
+
229
+ class NoteValidationContentInvalidEvent(ProcessChatsEvent):
230
+ name: Literal["note_validation_content_invalid"] = "note_validation_content_invalid"
231
+ reason_code: Literal["ValidationError", "validation_errors", "validation_failed", "requires_llm_rewrite"]
232
+ error_context: ProcessChatsErrorContext
233
+
234
+
235
+ class NoteValidationRetryRequestedEvent(ProcessChatsEvent):
236
+ name: Literal["note_validation_retry_requested"] = "note_validation_retry_requested"
237
+ resolved_by: str = Field(min_length=1)
238
+
239
+
240
+ class NotesValidatedEvent(ProcessChatsEvent):
241
+ name: Literal["notes_validated"] = "notes_validated"
242
+ manifest_path: str = Field(min_length=1)
243
+ coverage_path: str = Field(min_length=1)
244
+ staged_note_count: int = Field(ge=0, strict=True)
245
+
246
+
247
+ class PublishPreviewProducedEvent(ProcessChatsEvent):
248
+ name: Literal["publish_preview_produced"] = "publish_preview_produced"
249
+ manifest_path: str = Field(min_length=1)
250
+ dry_run_receipt_path: str = Field(min_length=1)
251
+ receipt_id: str = Field(min_length=1)
252
+
253
+
254
+ class HumanPublishApprovalEvent(ProcessChatsEvent):
255
+ name: Literal["publish_approved_by_human"] = "publish_approved_by_human"
256
+ approved_by: str = Field(min_length=1)
257
+ manifest_path: str = Field(min_length=1)
258
+ dry_run_receipt_path: str = Field(min_length=1)
259
+
260
+
261
+ class HumanPublishCancellationEvent(ProcessChatsEvent):
262
+ name: Literal["publish_cancelled_by_human"] = "publish_cancelled_by_human"
263
+ cancelled_by: str = Field(min_length=1)
264
+
265
+
266
+ class PublishBatchCompletedEvent(ProcessChatsEvent):
267
+ name: Literal["publish_batch_completed"] = "publish_batch_completed"
268
+ receipt_id: str = Field(min_length=1)
269
+ published_count: int = Field(ge=0, strict=True)
270
+ link_trigger_context_path: str = Field(min_length=1)
271
+
272
+
273
+ class PublishDryRunReceiptRequiredEvent(ProcessChatsEvent):
274
+ name: Literal["publish_dry_run_receipt_required"] = "publish_dry_run_receipt_required"
275
+ reason_code: Literal["dry_run_receipt_required"]
276
+ next_action: str = Field(min_length=1)
277
+ error_context: ProcessChatsErrorContext
278
+
279
+
280
+ class PublishStaleReceiptEvent(ProcessChatsEvent):
281
+ name: Literal["publish_stale_receipt"] = "publish_stale_receipt"
282
+ reason_code: Literal["dry_run_receipt_invalid", "new_taxonomy_leaf_requires_dry_run_authorization", "stale_receipt"]
283
+ next_action: str = Field(min_length=1)
284
+ error_context: ProcessChatsErrorContext
285
+
286
+
287
+ class PublishDuplicateTargetEvent(ProcessChatsEvent):
288
+ name: Literal["publish_duplicate_target"] = "publish_duplicate_target"
289
+ reason_code: Literal["duplicate_target", "duplicate_obsidian_target"]
290
+ next_action: str = Field(min_length=1)
291
+ error_context: ProcessChatsErrorContext
292
+
293
+
294
+ class PublishProvenanceGapEvent(ProcessChatsEvent):
295
+ name: Literal["publish_provenance_gap"] = "publish_provenance_gap"
296
+ reason_code: Literal["provenance_gap"]
297
+ next_action: str = Field(min_length=1)
298
+ error_context: ProcessChatsErrorContext
299
+
300
+
301
+ class PublishReceiptInvalidEvent(ProcessChatsEvent):
302
+ name: Literal["publish_receipt_invalid"] = "publish_receipt_invalid"
303
+ reason_code: Literal["publish_receipt_invalid"]
304
+ next_action: str = Field(min_length=1)
305
+ error_context: ProcessChatsErrorContext
306
+
307
+
308
+ class PublishBlockerResolvedEvent(ProcessChatsEvent):
309
+ name: Literal["publish_blocker_resolved"] = "publish_blocker_resolved"
310
+ resolved_by: str = Field(min_length=1)
311
+ manifest_path: str = Field(min_length=1)
312
+ dry_run_receipt_path: str = Field(min_length=1)
313
+
314
+
315
+ class ExternalQuotaReportedEvent(ProcessChatsEvent):
316
+ name: Literal["external_quota_reported"] = "external_quota_reported"
317
+ quota_kind: Literal["publish_batch"]
318
+ resume_action: str = Field(min_length=1)
319
+
320
+
321
+ class ExternalReadyEvent(ProcessChatsEvent):
322
+ name: Literal["external_ready"] = "external_ready"
323
+ restored_by: str = Field(min_length=1)
324
+ manifest_path: str = Field(min_length=1)
325
+ dry_run_receipt_path: str = Field(min_length=1)
326
+
327
+
328
+ class LinkRunCompletedEvent(ProcessChatsEvent):
329
+ name: Literal["link_run_completed"] = "link_run_completed"
330
+ receipt_id: str = Field(min_length=1)
331
+ changed_files: list[str] = Field(default_factory=list)
332
+
333
+
334
+ class LinkRunBlockedEvent(ProcessChatsEvent):
335
+ name: Literal["link_run_blocked"] = "link_run_blocked"
336
+ reason_code: str = Field(min_length=1)
337
+ next_action: str = Field(min_length=1)
338
+ error_context: ProcessChatsErrorContext
339
+
340
+
341
+ class ProcessChatsPublishRuntimeObservation(ContractModel):
342
+ """Typed publish/link facts; ProcessChatsMachine owns leaf-state priority."""
343
+
344
+ source_state: ProcessChatsState
345
+ preview_ready: bool = False
346
+ publish_completed: bool = False
347
+ link_completed: bool = False
348
+ link_blocked: bool = False
349
+ rollback_recorded: bool = False
350
+ blocked: bool = False
351
+ quota_wait: bool = False
352
+ validation_coverage_gap: bool = False
353
+ validation_manifest_mismatch: bool = False
354
+ validation_content_invalid: bool = False
355
+ publish_dry_run_receipt_required: bool = False
356
+ publish_stale_receipt: bool = False
357
+ publish_duplicate_target: bool = False
358
+ publish_provenance_gap: bool = False
359
+ reason_code: str = ""
360
+ next_action: str = ""
361
+ manifest_path: str = ""
362
+ dry_run_receipt_path: str = ""
363
+ receipt_id: str = ""
364
+ published_count: int = Field(default=0, ge=0, strict=True)
365
+ link_trigger_context_path: str = ""
366
+ link_receipt_id: str = ""
367
+ link_changed_files: list[str] = Field(default_factory=list)
368
+ error_context: ProcessChatsErrorContext | None = None
369
+
370
+
371
+ class ProcessChatsPublishRuntimeObservedEvent(ProcessChatsEvent):
372
+ """Single publish-runtime observation event consumed by ProcessChatsMachine."""
373
+
374
+ name: Literal["publish_runtime_observed"] = "publish_runtime_observed"
375
+ observation: ProcessChatsPublishRuntimeObservation
376
+
377
+
378
+ class MissingNextActionEvent(ProcessChatsEvent):
379
+ name: Literal["missing_next_action"] = "missing_next_action"
380
+ contract_source: str = Field(min_length=1)
381
+ next_action_hint: str = Field(min_length=1)
382
+
383
+
384
+ class MissingErrorContextEvent(ProcessChatsEvent):
385
+ name: Literal["missing_error_context"] = "missing_error_context"
386
+ contract_source: str = Field(min_length=1)
387
+ error_context_hint: str = Field(min_length=1)
388
+
389
+
390
+ class AgentToolContractViolationEvent(ProcessChatsEvent):
391
+ name: Literal["agent_tool_contract_violation"] = "agent_tool_contract_violation"
392
+ origin_event: str = Field(min_length=1)
393
+ error_context: ProcessChatsErrorContext
394
+
395
+
396
+ class VaultGuardRequiredEvent(ProcessChatsEvent):
397
+ name: Literal["vault_guard_required"] = "vault_guard_required"
398
+ reason_code: Literal["vault_guard_required"]
399
+ changed_file_count: int = Field(ge=0, strict=True)
400
+
401
+
402
+ class VaultGuardConfirmedEvent(ProcessChatsEvent):
403
+ name: Literal["vault_guard_confirmed"] = "vault_guard_confirmed"
404
+ confirmed_by: str = Field(min_length=1)
405
+
406
+
407
+ class VaultGuardRejectedEvent(ProcessChatsEvent):
408
+ name: Literal["vault_guard_rejected"] = "vault_guard_rejected"
409
+ rejected_by: str = Field(min_length=1)
410
+
411
+
412
+ class RollbackCompletedEvent(ProcessChatsEvent):
413
+ name: Literal["rollback_completed"] = "rollback_completed"
414
+ rollback_receipt_id: str = Field(min_length=1)
415
+
416
+
417
+ class RollbackFailureRecordedEvent(ProcessChatsEvent):
418
+ name: Literal["rollback_failure_recorded"] = "rollback_failure_recorded"
419
+ error_context: ProcessChatsErrorContext
420
+
421
+
422
+ ProcessChatsBoundaryEvent = Annotated[
423
+ EnvironmentCheckedEvent
424
+ | PathsMissingEvent
425
+ | PathsConfiguredEvent
426
+ | WindowsPathOrVenvBlockedEvent
427
+ | EnvironmentBootstrapCompletedEvent
428
+ | NoPendingRawChatsEvent
429
+ | NoTriagedRawChatsEvent
430
+ | TriagedRawChatsAvailableEvent
431
+ | TriagePlanCreatedEvent
432
+ | SubagentPlanAttestationMissingEvent
433
+ | SubagentPlanAttestationInvalidEvent
434
+ | SubagentPlanAttestationSuppliedEvent
435
+ | ArchitectSpecialistCapacityBlockedEvent
436
+ | ArchitectSpecialistCapacityRestoredEvent
437
+ | ArchitectWorkCompletedEvent
438
+ | ArchitectOutputAcceptedEvent
439
+ | ArchitectOutputInvalidEvent
440
+ | NoteValidationCoverageGapEvent
441
+ | NoteValidationManifestMismatchEvent
442
+ | NoteValidationContentInvalidEvent
443
+ | NoteValidationRetryRequestedEvent
444
+ | NotesValidatedEvent
445
+ | PublishPreviewProducedEvent
446
+ | HumanPublishApprovalEvent
447
+ | HumanPublishCancellationEvent
448
+ | PublishBatchCompletedEvent
449
+ | PublishDryRunReceiptRequiredEvent
450
+ | PublishStaleReceiptEvent
451
+ | PublishDuplicateTargetEvent
452
+ | PublishProvenanceGapEvent
453
+ | PublishReceiptInvalidEvent
454
+ | PublishBlockerResolvedEvent
455
+ | ExternalQuotaReportedEvent
456
+ | ExternalReadyEvent
457
+ | LinkRunCompletedEvent
458
+ | LinkRunBlockedEvent
459
+ | ProcessChatsPublishRuntimeObservedEvent
460
+ | MissingNextActionEvent
461
+ | MissingErrorContextEvent
462
+ | AgentToolContractViolationEvent
463
+ | VaultGuardRequiredEvent
464
+ | VaultGuardConfirmedEvent
465
+ | VaultGuardRejectedEvent
466
+ | RollbackCompletedEvent
467
+ | RollbackFailureRecordedEvent,
468
+ Field(discriminator="name"),
469
+ ]
470
+ ProcessChatsBoundaryEventAdapter = TypeAdapter(ProcessChatsBoundaryEvent)
471
+
472
+
473
+ class SetupPathsEffectPayload(ContractModel):
474
+ schema_version: Literal["medical-notes-workbench.process-chats.setup-paths-effect.v1"] = (
475
+ "medical-notes-workbench.process-chats.setup-paths-effect.v1"
476
+ )
477
+ kind: Literal["setup_paths"] = "setup_paths"
478
+ missing_path_kind: Literal["wiki_dir", "raw_dir", "both"]
479
+
480
+
481
+ class SetupBootstrapEffectPayload(ContractModel):
482
+ schema_version: Literal["medical-notes-workbench.process-chats.setup-bootstrap-effect.v1"] = (
483
+ "medical-notes-workbench.process-chats.setup-bootstrap-effect.v1"
484
+ )
485
+ kind: Literal["setup_bootstrap"] = "setup_bootstrap"
486
+ reason_code: Literal["environment_blocker.windows_path_or_venv"]
487
+
488
+
489
+ class ArchitectSpecialistEffectPayload(ContractModel):
490
+ schema_version: Literal["medical-notes-workbench.process-chats.architect-effect.v1"] = (
491
+ "medical-notes-workbench.process-chats.architect-effect.v1"
492
+ )
493
+ kind: Literal["architect_specialist"] = "architect_specialist"
494
+ note_plan_hash: str
495
+ raw_file_count: int = Field(ge=0, strict=True)
496
+
497
+
498
+ class ArchitectPlanningEffectPayload(ContractModel):
499
+ schema_version: Literal["medical-notes-workbench.process-chats.architect-planning-effect.v1"] = (
500
+ "medical-notes-workbench.process-chats.architect-planning-effect.v1"
501
+ )
502
+ kind: Literal["architect_planning"] = "architect_planning"
503
+ triaged_count: int = Field(ge=1, strict=True)
504
+ command: Literal["plan-subagents --phase architect"] = "plan-subagents --phase architect"
505
+
506
+
507
+ class PlanAttestationEffectPayload(ContractModel):
508
+ schema_version: Literal["medical-notes-workbench.process-chats.plan-attestation-effect.v1"] = (
509
+ "medical-notes-workbench.process-chats.plan-attestation-effect.v1"
510
+ )
511
+ kind: Literal["plan_attestation"] = "plan_attestation"
512
+ reason_code: str = Field(min_length=1)
513
+
514
+
515
+ class ResumeArchitectWorkEffectPayload(ContractModel):
516
+ schema_version: Literal["medical-notes-workbench.process-chats.resume-architect-effect.v1"] = (
517
+ "medical-notes-workbench.process-chats.resume-architect-effect.v1"
518
+ )
519
+ kind: Literal["resume_architect_work"] = "resume_architect_work"
520
+ resolved_by: str = Field(min_length=1)
521
+
522
+
523
+ class ResumePublishBlockerEffectPayload(ContractModel):
524
+ schema_version: Literal["medical-notes-workbench.process-chats.resume-publish-blocker-effect.v1"] = (
525
+ "medical-notes-workbench.process-chats.resume-publish-blocker-effect.v1"
526
+ )
527
+ kind: Literal["resume_publish_blocker"] = "resume_publish_blocker"
528
+ reason_code: str = Field(min_length=1)
529
+
530
+
531
+ class VaultGuardDecisionEffectPayload(ContractModel):
532
+ schema_version: Literal["medical-notes-workbench.process-chats.vault-guard-decision-effect.v1"] = (
533
+ "medical-notes-workbench.process-chats.vault-guard-decision-effect.v1"
534
+ )
535
+ kind: Literal["vault_guard_decision"] = "vault_guard_decision"
536
+ changed_file_count: int = Field(ge=0, strict=True)
537
+
538
+
539
+ class HumanPublishDecisionEffectPayload(ContractModel):
540
+ schema_version: Literal["medical-notes-workbench.process-chats.publish-decision-effect.v1"] = (
541
+ "medical-notes-workbench.process-chats.publish-decision-effect.v1"
542
+ )
543
+ kind: Literal["publish_decision"] = "publish_decision"
544
+ manifest_path: str = Field(min_length=1)
545
+ dry_run_receipt_path: str = Field(min_length=1)
546
+
547
+
548
+ class PublishBatchEffectPayload(ContractModel):
549
+ schema_version: Literal["medical-notes-workbench.process-chats.publish-batch-effect.v1"]
550
+ kind: Literal["publish_batch"]
551
+ manifest_path: str = Field(min_length=1)
552
+ dry_run_receipt_path: str = Field(min_length=1)
553
+
554
+
555
+ class RollbackEffectPayload(ContractModel):
556
+ schema_version: Literal["medical-notes-workbench.process-chats.rollback-effect.v1"] = (
557
+ "medical-notes-workbench.process-chats.rollback-effect.v1"
558
+ )
559
+ kind: Literal["rollback"] = "rollback"
560
+ failed_origin_state: str = Field(min_length=1)
561
+
562
+
563
+ class FailureFinalizationEffectPayload(ContractModel):
564
+ schema_version: Literal["medical-notes-workbench.process-chats.failure-finalization-effect.v1"] = (
565
+ "medical-notes-workbench.process-chats.failure-finalization-effect.v1"
566
+ )
567
+ kind: Literal["failure_finalization"] = "failure_finalization"
568
+ rollback_state: Literal["rollback.recorded"] = ProcessChatsState.ROLLBACK_RECORDED.value
569
+
570
+
571
+ ProcessChatsEffectPayload = Annotated[
572
+ SetupPathsEffectPayload
573
+ | SetupBootstrapEffectPayload
574
+ | ArchitectSpecialistEffectPayload
575
+ | ArchitectPlanningEffectPayload
576
+ | WaitExternalEffectPayload
577
+ | PlanAttestationEffectPayload
578
+ | ResumeArchitectWorkEffectPayload
579
+ | ResumePublishBlockerEffectPayload
580
+ | VaultGuardDecisionEffectPayload
581
+ | HumanPublishDecisionEffectPayload
582
+ | PublishBatchEffectPayload
583
+ | LinkWorkflowRunEffectPayload
584
+ | RollbackEffectPayload
585
+ | FailureFinalizationEffectPayload,
586
+ Field(discriminator="kind"),
587
+ ]
588
+ ProcessChatsEffectPayloadAdapter = TypeAdapter(ProcessChatsEffectPayload)
589
+
590
+
591
+ class SetupPathsConfiguredOutcome(ContractModel):
592
+ code: Literal["setup.paths_configured"] = "setup.paths_configured"
593
+ config_path: str = Field(min_length=1)
594
+
595
+
596
+ class SetupBootstrapCompletedOutcome(ContractModel):
597
+ code: Literal["setup.bootstrap_completed"] = "setup.bootstrap_completed"
598
+ bootstrap_summary: str = Field(min_length=1)
599
+
600
+
601
+ class ArchitectCompletedOutcome(ContractModel):
602
+ code: Literal["architect.completed"] = "architect.completed"
603
+ receipt_id: str = Field(min_length=1)
604
+ attestation_hash: str = Field(min_length=1)
605
+ coverage_path: str = Field(min_length=1)
606
+ manifest_path: str = Field(min_length=1)
607
+
608
+
609
+ class ArchitectCapacityBlockedOutcome(ContractModel):
610
+ code: Literal["architect.capacity_blocked"] = "architect.capacity_blocked"
611
+ reason_code: Literal["specialist_model_unavailable", "specialist_model_quota_exhausted"]
612
+ resume_action: str = Field(min_length=1)
613
+
614
+
615
+ class AgentToolContractViolationOutcome(ContractModel):
616
+ code: Literal["agent_tool_contract_violation"] = "agent_tool_contract_violation"
617
+ origin_event: str = Field(min_length=1)
618
+ error_context: ProcessChatsErrorContext
619
+
620
+
621
+ class ExternalReadyOutcome(ContractModel):
622
+ code: Literal["external.ready"] = "external.ready"
623
+ restored_by: str = Field(min_length=1)
624
+ manifest_path: str = Field(min_length=1)
625
+ dry_run_receipt_path: str = Field(min_length=1)
626
+
627
+
628
+ class AttestationSuppliedOutcome(ContractModel):
629
+ code: Literal["attestation.supplied"] = "attestation.supplied"
630
+ attestation_hash: str = Field(min_length=1)
631
+
632
+
633
+ class ValidationRetryRequestedOutcome(ContractModel):
634
+ code: Literal["validation.retry_requested"] = "validation.retry_requested"
635
+ resolved_by: str = Field(min_length=1)
636
+
637
+
638
+ class HumanConfirmedOutcome(ContractModel):
639
+ code: Literal["human.confirmed"] = "human.confirmed"
640
+ confirmed_by: str = Field(min_length=1)
641
+
642
+
643
+ class HumanRejectedOutcome(ContractModel):
644
+ code: Literal["human.rejected"] = "human.rejected"
645
+ rejected_by: str = Field(min_length=1)
646
+
647
+
648
+ class HumanApprovedOutcome(ContractModel):
649
+ code: Literal["human.approved"] = "human.approved"
650
+ approved_by: str = Field(min_length=1)
651
+ manifest_path: str = Field(min_length=1)
652
+ dry_run_receipt_path: str = Field(min_length=1)
653
+
654
+
655
+ class HumanCancelledOutcome(ContractModel):
656
+ code: Literal["human.cancelled"] = "human.cancelled"
657
+ cancelled_by: str = Field(min_length=1)
658
+
659
+
660
+ class PublishBatchCompletedOutcome(ContractModel):
661
+ code: Literal["publish.completed"] = "publish.completed"
662
+ receipt_id: str = Field(min_length=1)
663
+ published_count: int = Field(ge=0, strict=True)
664
+ link_trigger_context_path: str = Field(min_length=1)
665
+
666
+
667
+ class PublishDryRunReceiptRequiredOutcome(ContractModel):
668
+ code: Literal["publish.dry_run_receipt_required"] = "publish.dry_run_receipt_required"
669
+ reason_code: Literal["dry_run_receipt_required"] = "dry_run_receipt_required"
670
+ next_action: str = Field(min_length=1)
671
+ error_context: ProcessChatsErrorContext
672
+
673
+
674
+ class PublishStaleReceiptOutcome(ContractModel):
675
+ code: Literal["publish.stale_receipt"] = "publish.stale_receipt"
676
+ reason_code: Literal["dry_run_receipt_invalid", "new_taxonomy_leaf_requires_dry_run_authorization", "stale_receipt"]
677
+ next_action: str = Field(min_length=1)
678
+ error_context: ProcessChatsErrorContext
679
+
680
+
681
+ class PublishDuplicateTargetOutcome(ContractModel):
682
+ code: Literal["publish.duplicate_target"] = "publish.duplicate_target"
683
+ reason_code: Literal["duplicate_target", "duplicate_obsidian_target"]
684
+ next_action: str = Field(min_length=1)
685
+ error_context: ProcessChatsErrorContext
686
+
687
+
688
+ class PublishProvenanceGapOutcome(ContractModel):
689
+ code: Literal["publish.provenance_gap"] = "publish.provenance_gap"
690
+ reason_code: Literal["provenance_gap"] = "provenance_gap"
691
+ next_action: str = Field(min_length=1)
692
+ error_context: ProcessChatsErrorContext
693
+
694
+
695
+ class PublishReceiptInvalidOutcome(ContractModel):
696
+ code: Literal["publish.receipt_invalid"] = "publish.receipt_invalid"
697
+ reason_code: Literal["publish_receipt_invalid"] = "publish_receipt_invalid"
698
+ next_action: str = Field(min_length=1)
699
+ error_context: ProcessChatsErrorContext
700
+
701
+
702
+ class ExternalQuotaReportedOutcome(ContractModel):
703
+ code: Literal["external.quota_reported"] = "external.quota_reported"
704
+ quota_kind: Literal["publish_batch"]
705
+ resume_action: str = Field(min_length=1)
706
+
707
+
708
+ class PublishBlockerResolvedOutcome(ContractModel):
709
+ code: Literal["publish.blocker_resolved"] = "publish.blocker_resolved"
710
+ resolved_by: str = Field(min_length=1)
711
+ manifest_path: str = Field(min_length=1)
712
+ dry_run_receipt_path: str = Field(min_length=1)
713
+
714
+
715
+ class LinkCompletedOutcome(ContractModel):
716
+ code: Literal["link.completed"] = "link.completed"
717
+ receipt_id: str = Field(min_length=1)
718
+ changed_files: list[str] = Field(default_factory=list)
719
+
720
+
721
+ class LinkBlockedOutcome(ContractModel):
722
+ code: Literal["link.blocked"] = "link.blocked"
723
+ reason_code: str = Field(min_length=1)
724
+ next_action: str = Field(min_length=1)
725
+ error_context: ProcessChatsErrorContext
726
+
727
+
728
+ class RollbackCompletedOutcome(ContractModel):
729
+ code: Literal["rollback.completed"] = "rollback.completed"
730
+ rollback_receipt_id: str = Field(min_length=1)
731
+
732
+
733
+ class RollbackFailureRecordedOutcome(ContractModel):
734
+ code: Literal["rollback.failure_recorded"] = "rollback.failure_recorded"
735
+ error_context: ProcessChatsErrorContext
736
+
737
+
738
+ class ContractMissingNextActionOutcome(ContractModel):
739
+ code: Literal["contract_gap.missing_next_action"] = "contract_gap.missing_next_action"
740
+ next_action_hint: str = Field(min_length=1)
741
+
742
+
743
+ class ContractMissingErrorContextOutcome(ContractModel):
744
+ code: Literal["contract_gap.missing_error_context"] = "contract_gap.missing_error_context"
745
+ error_context_hint: str = Field(min_length=1)
746
+
747
+
748
+ ProcessChatsEffectOutcome = Annotated[
749
+ SetupPathsConfiguredOutcome
750
+ | SetupBootstrapCompletedOutcome
751
+ | ArchitectCompletedOutcome
752
+ | ArchitectCapacityBlockedOutcome
753
+ | AgentToolContractViolationOutcome
754
+ | ExternalReadyOutcome
755
+ | AttestationSuppliedOutcome
756
+ | ValidationRetryRequestedOutcome
757
+ | HumanConfirmedOutcome
758
+ | HumanRejectedOutcome
759
+ | HumanApprovedOutcome
760
+ | HumanCancelledOutcome
761
+ | PublishBatchCompletedOutcome
762
+ | PublishDryRunReceiptRequiredOutcome
763
+ | PublishStaleReceiptOutcome
764
+ | PublishDuplicateTargetOutcome
765
+ | PublishProvenanceGapOutcome
766
+ | PublishReceiptInvalidOutcome
767
+ | ExternalQuotaReportedOutcome
768
+ | PublishBlockerResolvedOutcome
769
+ | LinkCompletedOutcome
770
+ | LinkBlockedOutcome
771
+ | RollbackCompletedOutcome
772
+ | RollbackFailureRecordedOutcome
773
+ | ContractMissingNextActionOutcome
774
+ | ContractMissingErrorContextOutcome,
775
+ Field(discriminator="code"),
776
+ ]
777
+ ProcessChatsEffectOutcomeAdapter = TypeAdapter(ProcessChatsEffectOutcome)
778
+
779
+
780
+ @dataclass(frozen=True)
781
+ class ProcessChatsEffectReturnEventRow:
782
+ """One authorized adapter outcome -> boundary event edge."""
783
+
784
+ kind: WorkflowEffectKind
785
+ target: str
786
+ origin_state: str
787
+ outcome_code: str
788
+ event_name: str
789
+ event_model: type[ProcessChatsEvent]
790
+
791
+
792
+ @dataclass(frozen=True)
793
+ class ProcessChatsEffectReturnEventMatrix:
794
+ """Exact lookup table for effect-result conversion; no status guessing."""
795
+
796
+ rows: tuple[ProcessChatsEffectReturnEventRow, ...]
797
+
798
+ def lookup(
799
+ self,
800
+ *,
801
+ kind: WorkflowEffectKind,
802
+ target: str,
803
+ origin_state: str,
804
+ outcome_code: str,
805
+ ) -> ProcessChatsEffectReturnEventRow:
806
+ for row in self.rows:
807
+ if (
808
+ row.kind == kind
809
+ and row.target == target
810
+ and row.origin_state == origin_state
811
+ and row.outcome_code == outcome_code
812
+ ):
813
+ return row
814
+ raise ValueError(
815
+ "no process-chats effect return row for "
816
+ f"{kind.value}:{target}:{origin_state}:{outcome_code}"
817
+ )
818
+
819
+ def outcome_codes(self) -> set[str]:
820
+ return {row.outcome_code for row in self.rows}
821
+
822
+
823
+ def _row(
824
+ kind: WorkflowEffectKind,
825
+ target: str,
826
+ origin: ProcessChatsState,
827
+ outcome_code: str,
828
+ event_name: str,
829
+ event_model: type[ProcessChatsEvent],
830
+ ) -> ProcessChatsEffectReturnEventRow:
831
+ return ProcessChatsEffectReturnEventRow(
832
+ kind=kind,
833
+ target=target,
834
+ origin_state=origin.value,
835
+ outcome_code=outcome_code,
836
+ event_name=event_name,
837
+ event_model=event_model,
838
+ )
839
+
840
+
841
+ PROCESS_CHATS_EFFECT_RETURN_EVENT_MATRIX = ProcessChatsEffectReturnEventMatrix(
842
+ rows=(
843
+ _row(
844
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
845
+ "/mednotes:setup paths",
846
+ ProcessChatsState.ENVIRONMENT_PATHS_MISSING,
847
+ "setup.paths_configured",
848
+ "paths_configured",
849
+ PathsConfiguredEvent,
850
+ ),
851
+ _row(
852
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
853
+ "/mednotes:setup paths",
854
+ ProcessChatsState.ENVIRONMENT_PATHS_MISSING,
855
+ "contract_gap.missing_next_action",
856
+ "missing_next_action",
857
+ MissingNextActionEvent,
858
+ ),
859
+ _row(
860
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
861
+ "/mednotes:setup bootstrap",
862
+ ProcessChatsState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED,
863
+ "setup.bootstrap_completed",
864
+ "environment_bootstrap_completed",
865
+ EnvironmentBootstrapCompletedEvent,
866
+ ),
867
+ _row(
868
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
869
+ "/mednotes:setup bootstrap",
870
+ ProcessChatsState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED,
871
+ "contract_gap.missing_error_context",
872
+ "missing_error_context",
873
+ MissingErrorContextEvent,
874
+ ),
875
+ _row(
876
+ WorkflowEffectKind.CALL_SPECIALIST_MODEL,
877
+ "med-knowledge-architect",
878
+ ProcessChatsState.ARCHITECT_WORK_REQUESTED,
879
+ "architect.completed",
880
+ "architect_work_completed",
881
+ ArchitectWorkCompletedEvent,
882
+ ),
883
+ _row(
884
+ WorkflowEffectKind.CALL_SPECIALIST_MODEL,
885
+ "med-knowledge-architect",
886
+ ProcessChatsState.ARCHITECT_WORK_REQUESTED,
887
+ "architect.capacity_blocked",
888
+ "architect_specialist_capacity_blocked",
889
+ ArchitectSpecialistCapacityBlockedEvent,
890
+ ),
891
+ _row(
892
+ WorkflowEffectKind.CALL_SPECIALIST_MODEL,
893
+ "med-knowledge-architect",
894
+ ProcessChatsState.ARCHITECT_WORK_REQUESTED,
895
+ "agent_tool_contract_violation",
896
+ "agent_tool_contract_violation",
897
+ AgentToolContractViolationEvent,
898
+ ),
899
+ _row(
900
+ WorkflowEffectKind.WAIT_EXTERNAL,
901
+ "wait_external.specialist_capacity",
902
+ ProcessChatsState.ARCHITECT_AWAITING_SPECIALIST_CAPACITY,
903
+ "external.ready",
904
+ "architect_specialist_capacity_restored",
905
+ ArchitectSpecialistCapacityRestoredEvent,
906
+ ),
907
+ _row(
908
+ WorkflowEffectKind.WAIT_EXTERNAL,
909
+ "wait_external.specialist_capacity",
910
+ ProcessChatsState.ARCHITECT_AWAITING_SPECIALIST_CAPACITY,
911
+ "contract_gap.missing_next_action",
912
+ "missing_next_action",
913
+ MissingNextActionEvent,
914
+ ),
915
+ _row(
916
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
917
+ "agent.plan_attestation",
918
+ ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_REQUIRED,
919
+ "attestation.supplied",
920
+ "subagent_plan_attestation_supplied",
921
+ SubagentPlanAttestationSuppliedEvent,
922
+ ),
923
+ _row(
924
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
925
+ "agent.plan_attestation",
926
+ ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_REQUIRED,
927
+ "contract_gap.missing_error_context",
928
+ "missing_error_context",
929
+ MissingErrorContextEvent,
930
+ ),
931
+ _row(
932
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
933
+ "agent.plan_attestation",
934
+ ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_INVALID,
935
+ "attestation.supplied",
936
+ "subagent_plan_attestation_supplied",
937
+ SubagentPlanAttestationSuppliedEvent,
938
+ ),
939
+ _row(
940
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
941
+ "agent.plan_attestation",
942
+ ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_INVALID,
943
+ "contract_gap.missing_error_context",
944
+ "missing_error_context",
945
+ MissingErrorContextEvent,
946
+ ),
947
+ *(
948
+ _row(
949
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
950
+ "workflow.resume_architect_work",
951
+ origin,
952
+ "validation.retry_requested",
953
+ "note_validation_retry_requested",
954
+ NoteValidationRetryRequestedEvent,
955
+ )
956
+ for origin in (
957
+ ProcessChatsState.NOTE_VALIDATION_COVERAGE_GAP,
958
+ ProcessChatsState.NOTE_VALIDATION_MANIFEST_MISMATCH,
959
+ ProcessChatsState.NOTE_VALIDATION_CONTENT_INVALID,
960
+ )
961
+ ),
962
+ *(
963
+ _row(
964
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
965
+ "workflow.resume_architect_work",
966
+ origin,
967
+ "contract_gap.missing_error_context",
968
+ "missing_error_context",
969
+ MissingErrorContextEvent,
970
+ )
971
+ for origin in (
972
+ ProcessChatsState.NOTE_VALIDATION_COVERAGE_GAP,
973
+ ProcessChatsState.NOTE_VALIDATION_MANIFEST_MISMATCH,
974
+ ProcessChatsState.NOTE_VALIDATION_CONTENT_INVALID,
975
+ )
976
+ ),
977
+ _row(
978
+ WorkflowEffectKind.ASK_HUMAN,
979
+ "human.vault_guard_decision",
980
+ ProcessChatsState.VAULT_GUARD_DECISION_REQUIRED,
981
+ "human.confirmed",
982
+ "vault_guard_confirmed",
983
+ VaultGuardConfirmedEvent,
984
+ ),
985
+ _row(
986
+ WorkflowEffectKind.ASK_HUMAN,
987
+ "human.vault_guard_decision",
988
+ ProcessChatsState.VAULT_GUARD_DECISION_REQUIRED,
989
+ "human.rejected",
990
+ "vault_guard_rejected",
991
+ VaultGuardRejectedEvent,
992
+ ),
993
+ _row(
994
+ WorkflowEffectKind.ASK_HUMAN,
995
+ "human.publish_decision",
996
+ ProcessChatsState.PUBLISH_AWAITING_CONFIRMATION,
997
+ "human.approved",
998
+ "publish_approved_by_human",
999
+ HumanPublishApprovalEvent,
1000
+ ),
1001
+ _row(
1002
+ WorkflowEffectKind.ASK_HUMAN,
1003
+ "human.publish_decision",
1004
+ ProcessChatsState.PUBLISH_AWAITING_CONFIRMATION,
1005
+ "human.cancelled",
1006
+ "publish_cancelled_by_human",
1007
+ HumanPublishCancellationEvent,
1008
+ ),
1009
+ _row(
1010
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1011
+ "publish-batch",
1012
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
1013
+ "publish.completed",
1014
+ "publish_batch_completed",
1015
+ PublishBatchCompletedEvent,
1016
+ ),
1017
+ _row(
1018
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1019
+ "publish-batch",
1020
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
1021
+ "publish.dry_run_receipt_required",
1022
+ "publish_dry_run_receipt_required",
1023
+ PublishDryRunReceiptRequiredEvent,
1024
+ ),
1025
+ _row(
1026
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1027
+ "publish-batch",
1028
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
1029
+ "publish.stale_receipt",
1030
+ "publish_stale_receipt",
1031
+ PublishStaleReceiptEvent,
1032
+ ),
1033
+ _row(
1034
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1035
+ "publish-batch",
1036
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
1037
+ "publish.duplicate_target",
1038
+ "publish_duplicate_target",
1039
+ PublishDuplicateTargetEvent,
1040
+ ),
1041
+ _row(
1042
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1043
+ "publish-batch",
1044
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
1045
+ "publish.provenance_gap",
1046
+ "publish_provenance_gap",
1047
+ PublishProvenanceGapEvent,
1048
+ ),
1049
+ _row(
1050
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1051
+ "publish-batch",
1052
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
1053
+ "publish.receipt_invalid",
1054
+ "publish_receipt_invalid",
1055
+ PublishReceiptInvalidEvent,
1056
+ ),
1057
+ _row(
1058
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1059
+ "publish-batch",
1060
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
1061
+ "external.quota_reported",
1062
+ "external_quota_reported",
1063
+ ExternalQuotaReportedEvent,
1064
+ ),
1065
+ _row(
1066
+ WorkflowEffectKind.WAIT_EXTERNAL,
1067
+ "wait_external.publish_quota",
1068
+ ProcessChatsState.PUBLISH_PAUSED_FOR_QUOTA,
1069
+ "external.ready",
1070
+ "external_ready",
1071
+ ExternalReadyEvent,
1072
+ ),
1073
+ _row(
1074
+ WorkflowEffectKind.WAIT_EXTERNAL,
1075
+ "wait_external.publish_quota",
1076
+ ProcessChatsState.PUBLISH_PAUSED_FOR_QUOTA,
1077
+ "contract_gap.missing_next_action",
1078
+ "missing_next_action",
1079
+ MissingNextActionEvent,
1080
+ ),
1081
+ *(
1082
+ _row(
1083
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1084
+ "workflow.resume_publish_blocker",
1085
+ origin,
1086
+ "publish.blocker_resolved",
1087
+ "publish_blocker_resolved",
1088
+ PublishBlockerResolvedEvent,
1089
+ )
1090
+ for origin in (
1091
+ ProcessChatsState.PUBLISH_DRY_RUN_RECEIPT_REQUIRED,
1092
+ ProcessChatsState.PUBLISH_STALE_RECEIPT,
1093
+ ProcessChatsState.PUBLISH_DUPLICATE_TARGET,
1094
+ ProcessChatsState.PUBLISH_PROVENANCE_GAP,
1095
+ ProcessChatsState.PUBLISH_RECEIPT_INVALID,
1096
+ )
1097
+ ),
1098
+ *(
1099
+ _row(
1100
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1101
+ "workflow.resume_publish_blocker",
1102
+ origin,
1103
+ "contract_gap.missing_error_context",
1104
+ "missing_error_context",
1105
+ MissingErrorContextEvent,
1106
+ )
1107
+ for origin in (
1108
+ ProcessChatsState.PUBLISH_DRY_RUN_RECEIPT_REQUIRED,
1109
+ ProcessChatsState.PUBLISH_STALE_RECEIPT,
1110
+ ProcessChatsState.PUBLISH_DUPLICATE_TARGET,
1111
+ ProcessChatsState.PUBLISH_PROVENANCE_GAP,
1112
+ ProcessChatsState.PUBLISH_RECEIPT_INVALID,
1113
+ )
1114
+ ),
1115
+ _row(
1116
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1117
+ "/mednotes:link",
1118
+ ProcessChatsState.LINK_RUN_REQUESTED,
1119
+ "link.completed",
1120
+ "link_run_completed",
1121
+ LinkRunCompletedEvent,
1122
+ ),
1123
+ _row(
1124
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1125
+ "/mednotes:link",
1126
+ ProcessChatsState.LINK_RUN_REQUESTED,
1127
+ "link.blocked",
1128
+ "link_run_blocked",
1129
+ LinkRunBlockedEvent,
1130
+ ),
1131
+ _row(
1132
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1133
+ "vault.rollback",
1134
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
1135
+ "rollback.completed",
1136
+ "rollback_completed",
1137
+ RollbackCompletedEvent,
1138
+ ),
1139
+ _row(
1140
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1141
+ "vault.rollback",
1142
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
1143
+ "contract_gap.missing_error_context",
1144
+ "missing_error_context",
1145
+ MissingErrorContextEvent,
1146
+ ),
1147
+ _row(
1148
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1149
+ "vault.rollback",
1150
+ ProcessChatsState.LINK_RUN_REQUESTED,
1151
+ "rollback.completed",
1152
+ "rollback_completed",
1153
+ RollbackCompletedEvent,
1154
+ ),
1155
+ _row(
1156
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1157
+ "vault.rollback",
1158
+ ProcessChatsState.LINK_RUN_REQUESTED,
1159
+ "contract_gap.missing_error_context",
1160
+ "missing_error_context",
1161
+ MissingErrorContextEvent,
1162
+ ),
1163
+ _row(
1164
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1165
+ "workflow.failure_finalization",
1166
+ ProcessChatsState.ROLLBACK_RECORDED,
1167
+ "rollback.failure_recorded",
1168
+ "rollback_failure_recorded",
1169
+ RollbackFailureRecordedEvent,
1170
+ ),
1171
+ _row(
1172
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
1173
+ "workflow.failure_finalization",
1174
+ ProcessChatsState.ROLLBACK_RECORDED,
1175
+ "contract_gap.missing_error_context",
1176
+ "missing_error_context",
1177
+ MissingErrorContextEvent,
1178
+ ),
1179
+ )
1180
+ )
1181
+
1182
+
1183
+ class ProcessChatsMachine(StateChart[WorkflowModel]):
1184
+ """Operational statechart for process-chats; actions emit intents, never IO."""
1185
+
1186
+ allow_event_without_transition = False
1187
+ catch_errors_as_events = False
1188
+ states = States.from_enum(
1189
+ ProcessChatsState,
1190
+ initial=ProcessChatsState.ENVIRONMENT_CHECKING,
1191
+ final={
1192
+ ProcessChatsState.VAULT_GUARD_REJECTED,
1193
+ ProcessChatsState.BACKLOG_NO_PENDING_RAW_CHATS,
1194
+ ProcessChatsState.BACKLOG_NO_TRIAGED_RAW_CHATS,
1195
+ ProcessChatsState.PUBLISH_CANCELLED_BY_HUMAN,
1196
+ ProcessChatsState.CONTRACT_GAP_MISSING_NEXT_ACTION,
1197
+ ProcessChatsState.CONTRACT_GAP_MISSING_ERROR_CONTEXT,
1198
+ ProcessChatsState.AGENT_TOOL_CONTRACT_VIOLATION,
1199
+ ProcessChatsState.PUBLISHED,
1200
+ ProcessChatsState.COMPLETED_WITH_LINK_BLOCKERS,
1201
+ ProcessChatsState.TERMINAL_FAILURE_RECORDED,
1202
+ },
1203
+ use_enum_instance=False,
1204
+ )
1205
+
1206
+ environment_checked = states.ENVIRONMENT_CHECKING.to(states.TRIAGE_PLANNING)
1207
+ paths_missing = states.ENVIRONMENT_CHECKING.to(states.ENVIRONMENT_PATHS_MISSING)
1208
+ windows_path_or_venv_blocked = states.ENVIRONMENT_CHECKING.to(
1209
+ states.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED
1210
+ )
1211
+ paths_configured = states.ENVIRONMENT_PATHS_MISSING.to(states.ENVIRONMENT_CHECKING)
1212
+ environment_bootstrap_completed = states.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED.to(
1213
+ states.ENVIRONMENT_CHECKING
1214
+ )
1215
+ no_pending_raw_chats = states.ENVIRONMENT_CHECKING.to(states.BACKLOG_NO_PENDING_RAW_CHATS)
1216
+ no_triaged_raw_chats = states.TRIAGE_PLANNING.to(states.BACKLOG_NO_TRIAGED_RAW_CHATS)
1217
+ triaged_raw_chats_available = states.ENVIRONMENT_CHECKING.to(states.BACKLOG_TRIAGED_RAW_CHATS_READY)
1218
+ triage_plan_created = (
1219
+ states.TRIAGE_PLANNING.to(states.ARCHITECT_WORK_REQUESTED)
1220
+ | states.BACKLOG_TRIAGED_RAW_CHATS_READY.to(states.ARCHITECT_WORK_REQUESTED)
1221
+ )
1222
+ subagent_plan_attestation_missing = states.ARCHITECT_WORK_REQUESTED.to(
1223
+ states.SUBAGENT_PLAN_ATTESTATION_REQUIRED
1224
+ )
1225
+ subagent_plan_attestation_invalid = states.ARCHITECT_WORK_REQUESTED.to(
1226
+ states.SUBAGENT_PLAN_ATTESTATION_INVALID
1227
+ )
1228
+ subagent_plan_attestation_supplied = (
1229
+ states.SUBAGENT_PLAN_ATTESTATION_REQUIRED.to(states.ARCHITECT_WORK_REQUESTED)
1230
+ | states.SUBAGENT_PLAN_ATTESTATION_INVALID.to(states.ARCHITECT_WORK_REQUESTED)
1231
+ )
1232
+ architect_specialist_capacity_blocked = states.ARCHITECT_WORK_REQUESTED.to(
1233
+ states.ARCHITECT_AWAITING_SPECIALIST_CAPACITY
1234
+ )
1235
+ architect_specialist_capacity_restored = states.ARCHITECT_AWAITING_SPECIALIST_CAPACITY.to(
1236
+ states.ARCHITECT_WORK_REQUESTED
1237
+ )
1238
+ architect_work_completed = states.ARCHITECT_WORK_REQUESTED.to(states.ARCHITECT_REVIEWING_OUTPUT)
1239
+ architect_output_accepted = states.ARCHITECT_REVIEWING_OUTPUT.to(states.NOTE_VALIDATION_RUNNING)
1240
+ architect_output_invalid = states.ARCHITECT_REVIEWING_OUTPUT.to(states.NOTE_VALIDATION_CONTENT_INVALID)
1241
+ note_validation_coverage_gap = states.NOTE_VALIDATION_RUNNING.to(states.NOTE_VALIDATION_COVERAGE_GAP)
1242
+ note_validation_manifest_mismatch = states.NOTE_VALIDATION_RUNNING.to(
1243
+ states.NOTE_VALIDATION_MANIFEST_MISMATCH
1244
+ )
1245
+ note_validation_content_invalid = states.NOTE_VALIDATION_RUNNING.to(states.NOTE_VALIDATION_CONTENT_INVALID)
1246
+ note_validation_retry_requested = (
1247
+ states.NOTE_VALIDATION_COVERAGE_GAP.to(states.ARCHITECT_WORK_REQUESTED)
1248
+ | states.NOTE_VALIDATION_MANIFEST_MISMATCH.to(states.ARCHITECT_WORK_REQUESTED)
1249
+ | states.NOTE_VALIDATION_CONTENT_INVALID.to(states.ARCHITECT_WORK_REQUESTED)
1250
+ )
1251
+ notes_validated = states.NOTE_VALIDATION_RUNNING.to(states.STAGING_MANIFEST_READY)
1252
+ publish_preview_produced = states.STAGING_MANIFEST_READY.to(states.PUBLISH_AWAITING_CONFIRMATION)
1253
+ vault_guard_required = states.STAGING_MANIFEST_READY.to(states.VAULT_GUARD_DECISION_REQUIRED)
1254
+ vault_guard_confirmed = states.VAULT_GUARD_DECISION_REQUIRED.to(states.STAGING_MANIFEST_READY)
1255
+ vault_guard_rejected = states.VAULT_GUARD_DECISION_REQUIRED.to(states.VAULT_GUARD_REJECTED)
1256
+ publish_approved_by_human = states.PUBLISH_AWAITING_CONFIRMATION.to(states.PUBLISH_APPLY_REQUESTED)
1257
+ publish_cancelled_by_human = states.PUBLISH_AWAITING_CONFIRMATION.to(states.PUBLISH_CANCELLED_BY_HUMAN)
1258
+ publish_batch_completed = states.PUBLISH_APPLY_REQUESTED.to(states.LINK_RUN_REQUESTED)
1259
+ publish_runtime_observed = (
1260
+ states.ROLLBACK_RECORDED.to(
1261
+ states.TERMINAL_FAILURE_RECORDED,
1262
+ cond="_observed_rollback_recorded",
1263
+ on="_on_observed_rollback_recorded",
1264
+ )
1265
+ | states.STAGING_MANIFEST_READY.to(
1266
+ states.PUBLISH_AWAITING_CONFIRMATION,
1267
+ cond="_observed_preview_ready",
1268
+ on="_on_observed_publish_preview",
1269
+ )
1270
+ | states.PUBLISH_APPLY_REQUESTED.to(
1271
+ states.LINK_RUN_REQUESTED,
1272
+ cond="_observed_publish_completed",
1273
+ on="_on_observed_publish_completed",
1274
+ )
1275
+ | states.LINK_RUN_REQUESTED.to(
1276
+ states.PUBLISHED,
1277
+ cond="_observed_link_completed",
1278
+ on="_on_observed_link_completed",
1279
+ )
1280
+ | states.LINK_RUN_REQUESTED.to(
1281
+ states.COMPLETED_WITH_LINK_BLOCKERS,
1282
+ cond="_observed_link_blocked",
1283
+ on="_on_observed_link_blocked",
1284
+ )
1285
+ | states.PUBLISH_APPLY_REQUESTED.to(
1286
+ states.PUBLISH_PAUSED_FOR_QUOTA,
1287
+ cond="_observed_quota_wait",
1288
+ on="_on_observed_quota_wait",
1289
+ )
1290
+ | states.NOTE_VALIDATION_RUNNING.to(
1291
+ states.NOTE_VALIDATION_COVERAGE_GAP,
1292
+ cond="_observed_coverage_gap",
1293
+ on="_on_observed_blocked",
1294
+ )
1295
+ | states.NOTE_VALIDATION_RUNNING.to(
1296
+ states.NOTE_VALIDATION_MANIFEST_MISMATCH,
1297
+ cond="_observed_manifest_mismatch",
1298
+ on="_on_observed_blocked",
1299
+ )
1300
+ | states.NOTE_VALIDATION_RUNNING.to(
1301
+ states.NOTE_VALIDATION_CONTENT_INVALID,
1302
+ cond="_observed_content_invalid",
1303
+ on="_on_observed_blocked",
1304
+ )
1305
+ | states.PUBLISH_APPLY_REQUESTED.to(
1306
+ states.PUBLISH_DRY_RUN_RECEIPT_REQUIRED,
1307
+ cond="_observed_dry_run_receipt_required",
1308
+ on="_on_observed_publish_blocked",
1309
+ )
1310
+ | states.PUBLISH_APPLY_REQUESTED.to(
1311
+ states.PUBLISH_STALE_RECEIPT,
1312
+ cond="_observed_stale_receipt",
1313
+ on="_on_observed_publish_blocked",
1314
+ )
1315
+ | states.PUBLISH_APPLY_REQUESTED.to(
1316
+ states.PUBLISH_DUPLICATE_TARGET,
1317
+ cond="_observed_duplicate_target",
1318
+ on="_on_observed_publish_blocked",
1319
+ )
1320
+ | states.PUBLISH_APPLY_REQUESTED.to(
1321
+ states.PUBLISH_PROVENANCE_GAP,
1322
+ cond="_observed_provenance_gap",
1323
+ on="_on_observed_publish_blocked",
1324
+ )
1325
+ | states.PUBLISH_APPLY_REQUESTED.to(
1326
+ states.PUBLISH_RECEIPT_INVALID,
1327
+ cond="_observed_blocked",
1328
+ on="_on_observed_publish_blocked",
1329
+ )
1330
+ )
1331
+ publish_dry_run_receipt_required = states.PUBLISH_APPLY_REQUESTED.to(
1332
+ states.PUBLISH_DRY_RUN_RECEIPT_REQUIRED
1333
+ )
1334
+ publish_stale_receipt = states.PUBLISH_APPLY_REQUESTED.to(states.PUBLISH_STALE_RECEIPT)
1335
+ publish_duplicate_target = states.PUBLISH_APPLY_REQUESTED.to(states.PUBLISH_DUPLICATE_TARGET)
1336
+ publish_provenance_gap = states.PUBLISH_APPLY_REQUESTED.to(states.PUBLISH_PROVENANCE_GAP)
1337
+ publish_receipt_invalid = states.PUBLISH_APPLY_REQUESTED.to(states.PUBLISH_RECEIPT_INVALID)
1338
+ publish_blocker_resolved = (
1339
+ states.PUBLISH_DRY_RUN_RECEIPT_REQUIRED.to(states.PUBLISH_APPLY_REQUESTED)
1340
+ | states.PUBLISH_STALE_RECEIPT.to(states.PUBLISH_APPLY_REQUESTED)
1341
+ | states.PUBLISH_DUPLICATE_TARGET.to(states.PUBLISH_APPLY_REQUESTED)
1342
+ | states.PUBLISH_PROVENANCE_GAP.to(states.PUBLISH_APPLY_REQUESTED)
1343
+ | states.PUBLISH_RECEIPT_INVALID.to(states.PUBLISH_APPLY_REQUESTED)
1344
+ )
1345
+ link_run_completed = states.LINK_RUN_REQUESTED.to(states.PUBLISHED)
1346
+ link_run_blocked = states.LINK_RUN_REQUESTED.to(states.COMPLETED_WITH_LINK_BLOCKERS)
1347
+ external_quota_reported = states.PUBLISH_APPLY_REQUESTED.to(states.PUBLISH_PAUSED_FOR_QUOTA)
1348
+ external_ready = states.PUBLISH_PAUSED_FOR_QUOTA.to(states.PUBLISH_APPLY_REQUESTED)
1349
+ missing_next_action = (
1350
+ states.ENVIRONMENT_PATHS_MISSING.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1351
+ | states.VAULT_GUARD_DECISION_REQUIRED.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1352
+ | states.ARCHITECT_AWAITING_SPECIALIST_CAPACITY.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1353
+ | states.ARCHITECT_REVIEWING_OUTPUT.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1354
+ | states.NOTE_VALIDATION_RUNNING.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1355
+ | states.NOTE_VALIDATION_COVERAGE_GAP.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1356
+ | states.NOTE_VALIDATION_MANIFEST_MISMATCH.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1357
+ | states.NOTE_VALIDATION_CONTENT_INVALID.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1358
+ | states.PUBLISH_APPLY_REQUESTED.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1359
+ | states.PUBLISH_PAUSED_FOR_QUOTA.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1360
+ | states.PUBLISH_DRY_RUN_RECEIPT_REQUIRED.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1361
+ | states.PUBLISH_STALE_RECEIPT.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1362
+ | states.PUBLISH_DUPLICATE_TARGET.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1363
+ | states.PUBLISH_PROVENANCE_GAP.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1364
+ | states.PUBLISH_RECEIPT_INVALID.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1365
+ | states.LINK_RUN_REQUESTED.to(states.CONTRACT_GAP_MISSING_NEXT_ACTION)
1366
+ )
1367
+ missing_error_context = (
1368
+ states.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1369
+ | states.SUBAGENT_PLAN_ATTESTATION_REQUIRED.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1370
+ | states.SUBAGENT_PLAN_ATTESTATION_INVALID.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1371
+ | states.ARCHITECT_REVIEWING_OUTPUT.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1372
+ | states.NOTE_VALIDATION_RUNNING.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1373
+ | states.NOTE_VALIDATION_COVERAGE_GAP.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1374
+ | states.NOTE_VALIDATION_MANIFEST_MISMATCH.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1375
+ | states.NOTE_VALIDATION_CONTENT_INVALID.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1376
+ | states.PUBLISH_APPLY_REQUESTED.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1377
+ | states.PUBLISH_DRY_RUN_RECEIPT_REQUIRED.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1378
+ | states.PUBLISH_STALE_RECEIPT.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1379
+ | states.PUBLISH_DUPLICATE_TARGET.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1380
+ | states.PUBLISH_PROVENANCE_GAP.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1381
+ | states.PUBLISH_RECEIPT_INVALID.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1382
+ | states.LINK_RUN_REQUESTED.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1383
+ | states.ROLLBACK_RECORDED.to(states.CONTRACT_GAP_MISSING_ERROR_CONTEXT)
1384
+ )
1385
+ agent_tool_contract_violation = (
1386
+ states.ARCHITECT_WORK_REQUESTED.to(states.AGENT_TOOL_CONTRACT_VIOLATION)
1387
+ | states.PUBLISH_APPLY_REQUESTED.to(states.AGENT_TOOL_CONTRACT_VIOLATION)
1388
+ | states.LINK_RUN_REQUESTED.to(states.AGENT_TOOL_CONTRACT_VIOLATION)
1389
+ )
1390
+ rollback_completed = (
1391
+ states.PUBLISH_APPLY_REQUESTED.to(states.ROLLBACK_RECORDED)
1392
+ | states.LINK_RUN_REQUESTED.to(states.ROLLBACK_RECORDED)
1393
+ )
1394
+ rollback_failure_recorded = states.ROLLBACK_RECORDED.to(states.TERMINAL_FAILURE_RECORDED)
1395
+
1396
+ _PHASE_BY_STATE: ClassVar[dict[ProcessChatsState, str]] = {
1397
+ ProcessChatsState.ENVIRONMENT_CHECKING: "environment",
1398
+ ProcessChatsState.ENVIRONMENT_PATHS_MISSING: "environment",
1399
+ ProcessChatsState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED: "environment",
1400
+ ProcessChatsState.BACKLOG_NO_PENDING_RAW_CHATS: "backlog",
1401
+ ProcessChatsState.BACKLOG_NO_TRIAGED_RAW_CHATS: "backlog",
1402
+ ProcessChatsState.BACKLOG_TRIAGED_RAW_CHATS_READY: "backlog",
1403
+ ProcessChatsState.VAULT_GUARD_DECISION_REQUIRED: "vault_guard",
1404
+ ProcessChatsState.VAULT_GUARD_REJECTED: "vault_guard",
1405
+ ProcessChatsState.TRIAGE_PLANNING: "triage",
1406
+ ProcessChatsState.ARCHITECT_WORK_REQUESTED: "architect",
1407
+ ProcessChatsState.ARCHITECT_AWAITING_SPECIALIST_CAPACITY: "architect",
1408
+ ProcessChatsState.ARCHITECT_REVIEWING_OUTPUT: "architect",
1409
+ ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_REQUIRED: "subagent_plan_attestation",
1410
+ ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_INVALID: "subagent_plan_attestation",
1411
+ ProcessChatsState.NOTE_VALIDATION_RUNNING: "note_validation",
1412
+ ProcessChatsState.NOTE_VALIDATION_COVERAGE_GAP: "note_validation",
1413
+ ProcessChatsState.NOTE_VALIDATION_MANIFEST_MISMATCH: "note_validation",
1414
+ ProcessChatsState.NOTE_VALIDATION_CONTENT_INVALID: "note_validation",
1415
+ ProcessChatsState.STAGING_MANIFEST_READY: "staging",
1416
+ ProcessChatsState.PUBLISH_AWAITING_CONFIRMATION: "publish",
1417
+ ProcessChatsState.PUBLISH_CANCELLED_BY_HUMAN: "publish",
1418
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED: "publish",
1419
+ ProcessChatsState.PUBLISH_PAUSED_FOR_QUOTA: "publish",
1420
+ ProcessChatsState.PUBLISH_DRY_RUN_RECEIPT_REQUIRED: "publish",
1421
+ ProcessChatsState.PUBLISH_STALE_RECEIPT: "publish",
1422
+ ProcessChatsState.PUBLISH_DUPLICATE_TARGET: "publish",
1423
+ ProcessChatsState.PUBLISH_PROVENANCE_GAP: "publish",
1424
+ ProcessChatsState.PUBLISH_RECEIPT_INVALID: "publish",
1425
+ ProcessChatsState.LINK_RUN_REQUESTED: "link",
1426
+ ProcessChatsState.CONTRACT_GAP_MISSING_NEXT_ACTION: "contract_gap",
1427
+ ProcessChatsState.CONTRACT_GAP_MISSING_ERROR_CONTEXT: "contract_gap",
1428
+ ProcessChatsState.AGENT_TOOL_CONTRACT_VIOLATION: "agent_tool_contract",
1429
+ ProcessChatsState.ROLLBACK_RECORDED: "rollback",
1430
+ ProcessChatsState.PUBLISHED: "published",
1431
+ ProcessChatsState.COMPLETED_WITH_LINK_BLOCKERS: "link",
1432
+ ProcessChatsState.TERMINAL_FAILURE_RECORDED: "failed",
1433
+ }
1434
+
1435
+ def category_for_state(self, state: str) -> WorkflowStateCategory:
1436
+ match ProcessChatsState(state):
1437
+ case (
1438
+ ProcessChatsState.ENVIRONMENT_CHECKING
1439
+ | ProcessChatsState.TRIAGE_PLANNING
1440
+ | ProcessChatsState.ARCHITECT_REVIEWING_OUTPUT
1441
+ | ProcessChatsState.NOTE_VALIDATION_RUNNING
1442
+ | ProcessChatsState.STAGING_MANIFEST_READY
1443
+ ):
1444
+ return WorkflowStateCategory.RUNNING
1445
+ case (
1446
+ ProcessChatsState.ENVIRONMENT_PATHS_MISSING
1447
+ | ProcessChatsState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED
1448
+ | ProcessChatsState.ARCHITECT_WORK_REQUESTED
1449
+ | ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_REQUIRED
1450
+ | ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_INVALID
1451
+ | ProcessChatsState.NOTE_VALIDATION_COVERAGE_GAP
1452
+ | ProcessChatsState.NOTE_VALIDATION_MANIFEST_MISMATCH
1453
+ | ProcessChatsState.NOTE_VALIDATION_CONTENT_INVALID
1454
+ | ProcessChatsState.PUBLISH_APPLY_REQUESTED
1455
+ | ProcessChatsState.PUBLISH_DRY_RUN_RECEIPT_REQUIRED
1456
+ | ProcessChatsState.PUBLISH_STALE_RECEIPT
1457
+ | ProcessChatsState.PUBLISH_DUPLICATE_TARGET
1458
+ | ProcessChatsState.PUBLISH_PROVENANCE_GAP
1459
+ | ProcessChatsState.PUBLISH_RECEIPT_INVALID
1460
+ | ProcessChatsState.LINK_RUN_REQUESTED
1461
+ | ProcessChatsState.ROLLBACK_RECORDED
1462
+ | ProcessChatsState.BACKLOG_TRIAGED_RAW_CHATS_READY
1463
+ ):
1464
+ return WorkflowStateCategory.WAITING_AGENT
1465
+ case ProcessChatsState.PUBLISH_PAUSED_FOR_QUOTA | ProcessChatsState.ARCHITECT_AWAITING_SPECIALIST_CAPACITY:
1466
+ return WorkflowStateCategory.WAITING_EXTERNAL
1467
+ case ProcessChatsState.VAULT_GUARD_DECISION_REQUIRED | ProcessChatsState.PUBLISH_AWAITING_CONFIRMATION:
1468
+ return WorkflowStateCategory.WAITING_HUMAN
1469
+ case (
1470
+ ProcessChatsState.CONTRACT_GAP_MISSING_NEXT_ACTION
1471
+ | ProcessChatsState.CONTRACT_GAP_MISSING_ERROR_CONTEXT
1472
+ | ProcessChatsState.AGENT_TOOL_CONTRACT_VIOLATION
1473
+ | ProcessChatsState.COMPLETED_WITH_LINK_BLOCKERS
1474
+ ):
1475
+ return WorkflowStateCategory.BLOCKED
1476
+ case (
1477
+ ProcessChatsState.PUBLISHED
1478
+ | ProcessChatsState.BACKLOG_NO_PENDING_RAW_CHATS
1479
+ | ProcessChatsState.BACKLOG_NO_TRIAGED_RAW_CHATS
1480
+ ):
1481
+ return WorkflowStateCategory.COMPLETED
1482
+ case ProcessChatsState.PUBLISH_CANCELLED_BY_HUMAN | ProcessChatsState.VAULT_GUARD_REJECTED:
1483
+ return WorkflowStateCategory.COMPLETED_WITH_WARNINGS
1484
+ case ProcessChatsState.TERMINAL_FAILURE_RECORDED:
1485
+ return WorkflowStateCategory.FAILED
1486
+ raise ValueError(f"unknown process-chats state: {state}")
1487
+
1488
+ def on_environment_checked(self, workflow_event: EnvironmentCheckedEvent) -> WorkflowTransitionResult:
1489
+ return self._transition(workflow_event, ProcessChatsState.TRIAGE_PLANNING, "environment_checked")
1490
+
1491
+ def on_paths_missing(self, workflow_event: PathsMissingEvent) -> WorkflowTransitionResult:
1492
+ effect = _setup_paths_effect(workflow_event, ProcessChatsState.ENVIRONMENT_PATHS_MISSING)
1493
+ return self._transition(
1494
+ workflow_event,
1495
+ ProcessChatsState.ENVIRONMENT_PATHS_MISSING,
1496
+ workflow_event.reason_code,
1497
+ effects=[effect],
1498
+ resume_action="/mednotes:setup",
1499
+ )
1500
+
1501
+ def on_windows_path_or_venv_blocked(
1502
+ self, workflow_event: WindowsPathOrVenvBlockedEvent
1503
+ ) -> WorkflowTransitionResult:
1504
+ effect = _setup_bootstrap_effect(workflow_event, ProcessChatsState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED)
1505
+ return self._transition(
1506
+ workflow_event,
1507
+ ProcessChatsState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED,
1508
+ workflow_event.reason_code,
1509
+ effects=[effect],
1510
+ resume_action="/mednotes:setup",
1511
+ )
1512
+
1513
+ def on_paths_configured(self, workflow_event: PathsConfiguredEvent) -> WorkflowTransitionResult:
1514
+ return self._transition(workflow_event, ProcessChatsState.ENVIRONMENT_CHECKING, "setup.paths_configured")
1515
+
1516
+ def on_environment_bootstrap_completed(
1517
+ self, workflow_event: EnvironmentBootstrapCompletedEvent
1518
+ ) -> WorkflowTransitionResult:
1519
+ return self._transition(
1520
+ workflow_event,
1521
+ ProcessChatsState.ENVIRONMENT_CHECKING,
1522
+ "setup.bootstrap_completed",
1523
+ )
1524
+
1525
+ def on_no_pending_raw_chats(self, workflow_event: NoPendingRawChatsEvent) -> WorkflowTransitionResult:
1526
+ return self._transition(
1527
+ workflow_event,
1528
+ ProcessChatsState.BACKLOG_NO_PENDING_RAW_CHATS,
1529
+ "no_pending_raw_chats",
1530
+ )
1531
+
1532
+ def on_no_triaged_raw_chats(self, workflow_event: NoTriagedRawChatsEvent) -> WorkflowTransitionResult:
1533
+ return self._transition(
1534
+ workflow_event,
1535
+ ProcessChatsState.BACKLOG_NO_TRIAGED_RAW_CHATS,
1536
+ "no_triaged_raw_chats",
1537
+ )
1538
+
1539
+ def on_triaged_raw_chats_available(
1540
+ self,
1541
+ workflow_event: TriagedRawChatsAvailableEvent,
1542
+ ) -> WorkflowTransitionResult:
1543
+ effect = _architect_planning_effect(workflow_event, ProcessChatsState.BACKLOG_TRIAGED_RAW_CHATS_READY)
1544
+ return self._transition(
1545
+ workflow_event,
1546
+ ProcessChatsState.BACKLOG_TRIAGED_RAW_CHATS_READY,
1547
+ "triaged_raw_chats_available",
1548
+ effects=[effect],
1549
+ resume_action="Continuar com list-triados e plan-subagents --phase architect.",
1550
+ )
1551
+
1552
+ def on_triage_plan_created(self, workflow_event: TriagePlanCreatedEvent) -> WorkflowTransitionResult:
1553
+ effect = _architect_effect(workflow_event, ProcessChatsState.ARCHITECT_WORK_REQUESTED)
1554
+ return self._transition(
1555
+ workflow_event,
1556
+ ProcessChatsState.ARCHITECT_WORK_REQUESTED,
1557
+ "triage_plan_created",
1558
+ effects=[effect],
1559
+ )
1560
+
1561
+ def on_subagent_plan_attestation_missing(
1562
+ self, workflow_event: SubagentPlanAttestationMissingEvent
1563
+ ) -> WorkflowTransitionResult:
1564
+ effect = _plan_attestation_effect(workflow_event, ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_REQUIRED)
1565
+ return self._transition(
1566
+ workflow_event,
1567
+ ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_REQUIRED,
1568
+ workflow_event.reason_code,
1569
+ effects=[effect],
1570
+ )
1571
+
1572
+ def on_subagent_plan_attestation_invalid(
1573
+ self, workflow_event: SubagentPlanAttestationInvalidEvent
1574
+ ) -> WorkflowTransitionResult:
1575
+ effect = _plan_attestation_effect(workflow_event, ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_INVALID)
1576
+ return self._transition(
1577
+ workflow_event,
1578
+ ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_INVALID,
1579
+ workflow_event.reason_code,
1580
+ effects=[effect],
1581
+ )
1582
+
1583
+ def on_subagent_plan_attestation_supplied(
1584
+ self, workflow_event: SubagentPlanAttestationSuppliedEvent
1585
+ ) -> WorkflowTransitionResult:
1586
+ effect = _architect_retry_effect(workflow_event, ProcessChatsState.ARCHITECT_WORK_REQUESTED)
1587
+ return self._transition(
1588
+ workflow_event,
1589
+ ProcessChatsState.ARCHITECT_WORK_REQUESTED,
1590
+ "attestation.supplied",
1591
+ effects=[effect],
1592
+ )
1593
+
1594
+ def on_architect_specialist_capacity_blocked(
1595
+ self, workflow_event: ArchitectSpecialistCapacityBlockedEvent
1596
+ ) -> WorkflowTransitionResult:
1597
+ effect = _wait_external_effect(
1598
+ workflow_event,
1599
+ ProcessChatsState.ARCHITECT_AWAITING_SPECIALIST_CAPACITY,
1600
+ target="wait_external.specialist_capacity",
1601
+ wait_target="specialist_capacity",
1602
+ resume_action=workflow_event.resume_action,
1603
+ )
1604
+ return self._transition(
1605
+ workflow_event,
1606
+ ProcessChatsState.ARCHITECT_AWAITING_SPECIALIST_CAPACITY,
1607
+ workflow_event.reason_code,
1608
+ effects=[effect],
1609
+ resume_action=workflow_event.resume_action,
1610
+ )
1611
+
1612
+ def on_architect_specialist_capacity_restored(
1613
+ self, workflow_event: ArchitectSpecialistCapacityRestoredEvent
1614
+ ) -> WorkflowTransitionResult:
1615
+ effect = _architect_retry_effect(workflow_event, ProcessChatsState.ARCHITECT_WORK_REQUESTED)
1616
+ return self._transition(
1617
+ workflow_event,
1618
+ ProcessChatsState.ARCHITECT_WORK_REQUESTED,
1619
+ "external.ready",
1620
+ effects=[effect],
1621
+ )
1622
+
1623
+ def on_architect_work_completed(self, workflow_event: ArchitectWorkCompletedEvent) -> WorkflowTransitionResult:
1624
+ return self._transition(
1625
+ workflow_event,
1626
+ ProcessChatsState.ARCHITECT_REVIEWING_OUTPUT,
1627
+ "architect.completed",
1628
+ )
1629
+
1630
+ def on_architect_output_accepted(self, workflow_event: ArchitectOutputAcceptedEvent) -> WorkflowTransitionResult:
1631
+ return self._transition(
1632
+ workflow_event,
1633
+ ProcessChatsState.NOTE_VALIDATION_RUNNING,
1634
+ "architect_output_accepted",
1635
+ )
1636
+
1637
+ def on_architect_output_invalid(self, workflow_event: ArchitectOutputInvalidEvent) -> WorkflowTransitionResult:
1638
+ effect = _resume_architect_work_effect(workflow_event, ProcessChatsState.NOTE_VALIDATION_CONTENT_INVALID)
1639
+ return self._transition(
1640
+ workflow_event,
1641
+ ProcessChatsState.NOTE_VALIDATION_CONTENT_INVALID,
1642
+ workflow_event.reason_code,
1643
+ effects=[effect],
1644
+ resume_action=workflow_event.error_context.next_action,
1645
+ )
1646
+
1647
+ def on_note_validation_coverage_gap(
1648
+ self, workflow_event: NoteValidationCoverageGapEvent
1649
+ ) -> WorkflowTransitionResult:
1650
+ return self._recoverable_validation_block(
1651
+ workflow_event,
1652
+ ProcessChatsState.NOTE_VALIDATION_COVERAGE_GAP,
1653
+ )
1654
+
1655
+ def on_note_validation_manifest_mismatch(
1656
+ self, workflow_event: NoteValidationManifestMismatchEvent
1657
+ ) -> WorkflowTransitionResult:
1658
+ return self._recoverable_validation_block(
1659
+ workflow_event,
1660
+ ProcessChatsState.NOTE_VALIDATION_MANIFEST_MISMATCH,
1661
+ )
1662
+
1663
+ def on_note_validation_content_invalid(
1664
+ self, workflow_event: NoteValidationContentInvalidEvent
1665
+ ) -> WorkflowTransitionResult:
1666
+ return self._recoverable_validation_block(
1667
+ workflow_event,
1668
+ ProcessChatsState.NOTE_VALIDATION_CONTENT_INVALID,
1669
+ )
1670
+
1671
+ def _recoverable_validation_block(
1672
+ self,
1673
+ workflow_event: (
1674
+ NoteValidationCoverageGapEvent
1675
+ | NoteValidationManifestMismatchEvent
1676
+ | NoteValidationContentInvalidEvent
1677
+ ),
1678
+ to_state: ProcessChatsState,
1679
+ ) -> WorkflowTransitionResult:
1680
+ effect = _resume_architect_work_effect(workflow_event, to_state)
1681
+ return self._transition(
1682
+ workflow_event,
1683
+ to_state,
1684
+ workflow_event.reason_code,
1685
+ effects=[effect],
1686
+ resume_action=workflow_event.error_context.next_action,
1687
+ )
1688
+
1689
+ def on_note_validation_retry_requested(
1690
+ self, workflow_event: NoteValidationRetryRequestedEvent
1691
+ ) -> WorkflowTransitionResult:
1692
+ effect = _architect_retry_effect(workflow_event, ProcessChatsState.ARCHITECT_WORK_REQUESTED)
1693
+ return self._transition(
1694
+ workflow_event,
1695
+ ProcessChatsState.ARCHITECT_WORK_REQUESTED,
1696
+ "validation.retry_requested",
1697
+ effects=[effect],
1698
+ )
1699
+
1700
+ def on_notes_validated(self, workflow_event: NotesValidatedEvent) -> WorkflowTransitionResult:
1701
+ return self._transition(workflow_event, ProcessChatsState.STAGING_MANIFEST_READY, "notes_validated")
1702
+
1703
+ def on_publish_preview_produced(self, workflow_event: PublishPreviewProducedEvent) -> WorkflowTransitionResult:
1704
+ to_state = ProcessChatsState.PUBLISH_AWAITING_CONFIRMATION
1705
+ decision = _ask_human_decision(
1706
+ state=to_state,
1707
+ reason_code="publish_confirmation_required",
1708
+ public_summary="Confirmar publicação das notas preparadas?",
1709
+ next_action="Confirmar ou cancelar a publicação.",
1710
+ recommended_option_id="approve",
1711
+ options=(
1712
+ HumanDecisionOption(id="approve", label="Publicar", description="Aplicar o lote preparado."),
1713
+ HumanDecisionOption(id="cancel", label="Cancelar", description="Encerrar sem mutar a Wiki."),
1714
+ ),
1715
+ )
1716
+ effect = _human_publish_decision_effect(workflow_event, to_state)
1717
+ return self._transition(
1718
+ workflow_event,
1719
+ to_state,
1720
+ "publish_preview_produced",
1721
+ effects=[effect],
1722
+ decision=decision,
1723
+ human_decision_packet=HumanDecisionPacket.model_validate(decision.to_human_decision_packet()),
1724
+ resume_action=decision.resume_action,
1725
+ )
1726
+
1727
+ def on_vault_guard_required(self, workflow_event: VaultGuardRequiredEvent) -> WorkflowTransitionResult:
1728
+ to_state = ProcessChatsState.VAULT_GUARD_DECISION_REQUIRED
1729
+ decision = _ask_human_decision(
1730
+ state=to_state,
1731
+ reason_code=workflow_event.reason_code,
1732
+ public_summary="Confirmar ponto de restauração antes de alterar a Wiki?",
1733
+ next_action="Confirmar a proteção do vault ou cancelar a operação.",
1734
+ recommended_option_id="confirm",
1735
+ options=(
1736
+ HumanDecisionOption(id="confirm", label="Confirmar", description="Continuar com proteção do vault."),
1737
+ HumanDecisionOption(id="reject", label="Cancelar", description="Encerrar sem mutar a Wiki."),
1738
+ ),
1739
+ )
1740
+ effect = _vault_guard_decision_effect(workflow_event, to_state)
1741
+ return self._transition(
1742
+ workflow_event,
1743
+ to_state,
1744
+ workflow_event.reason_code,
1745
+ effects=[effect],
1746
+ decision=decision,
1747
+ human_decision_packet=HumanDecisionPacket.model_validate(decision.to_human_decision_packet()),
1748
+ resume_action=decision.resume_action,
1749
+ )
1750
+
1751
+ def on_vault_guard_confirmed(self, workflow_event: VaultGuardConfirmedEvent) -> WorkflowTransitionResult:
1752
+ return self._transition(workflow_event, ProcessChatsState.STAGING_MANIFEST_READY, "human.confirmed")
1753
+
1754
+ def on_vault_guard_rejected(self, workflow_event: VaultGuardRejectedEvent) -> WorkflowTransitionResult:
1755
+ return self._transition(workflow_event, ProcessChatsState.VAULT_GUARD_REJECTED, "human.rejected")
1756
+
1757
+ def on_publish_approved_by_human(self, workflow_event: HumanPublishApprovalEvent) -> WorkflowTransitionResult:
1758
+ effect = _publish_batch_effect(workflow_event, ProcessChatsState.PUBLISH_APPLY_REQUESTED)
1759
+ return self._transition(
1760
+ workflow_event,
1761
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
1762
+ "human.approved",
1763
+ effects=[effect],
1764
+ )
1765
+
1766
+ def on_publish_cancelled_by_human(self, workflow_event: HumanPublishCancellationEvent) -> WorkflowTransitionResult:
1767
+ return self._transition(
1768
+ workflow_event,
1769
+ ProcessChatsState.PUBLISH_CANCELLED_BY_HUMAN,
1770
+ "human.cancelled",
1771
+ )
1772
+
1773
+ def on_publish_batch_completed(self, workflow_event: PublishBatchCompletedEvent) -> WorkflowTransitionResult:
1774
+ effect = _link_effect(workflow_event, ProcessChatsState.LINK_RUN_REQUESTED)
1775
+ return self._transition(
1776
+ workflow_event,
1777
+ ProcessChatsState.LINK_RUN_REQUESTED,
1778
+ "publish.completed",
1779
+ effects=[effect],
1780
+ )
1781
+
1782
+ def _on_observed_publish_preview(
1783
+ self,
1784
+ workflow_event: ProcessChatsPublishRuntimeObservedEvent,
1785
+ target: object,
1786
+ ) -> WorkflowTransitionResult:
1787
+ to_state = ProcessChatsState(str(getattr(target, "value", target)))
1788
+ decision = _ask_human_decision(
1789
+ state=to_state,
1790
+ reason_code="publish_confirmation_required",
1791
+ public_summary="Confirmar publicação das notas preparadas?",
1792
+ next_action="Confirmar ou cancelar a publicação.",
1793
+ recommended_option_id="approve",
1794
+ options=(
1795
+ HumanDecisionOption(id="approve", label="Publicar", description="Aplicar o lote preparado."),
1796
+ HumanDecisionOption(id="cancel", label="Cancelar", description="Encerrar sem mutar a Wiki."),
1797
+ ),
1798
+ )
1799
+ observation = workflow_event.observation
1800
+ effect = _workflow_effect(
1801
+ workflow_event,
1802
+ to_state,
1803
+ kind=WorkflowEffectKind.ASK_HUMAN,
1804
+ target="human.publish_decision",
1805
+ payload=HumanPublishDecisionEffectPayload(
1806
+ manifest_path=observation.manifest_path,
1807
+ dry_run_receipt_path=observation.dry_run_receipt_path,
1808
+ ),
1809
+ requires_receipt=False,
1810
+ )
1811
+ return self._transition(
1812
+ workflow_event,
1813
+ to_state,
1814
+ "publish_preview_produced",
1815
+ effects=[effect],
1816
+ decision=decision,
1817
+ human_decision_packet=HumanDecisionPacket.model_validate(decision.to_human_decision_packet()),
1818
+ resume_action=decision.resume_action,
1819
+ )
1820
+
1821
+ def _on_observed_publish_completed(
1822
+ self,
1823
+ workflow_event: ProcessChatsPublishRuntimeObservedEvent,
1824
+ target: object,
1825
+ ) -> WorkflowTransitionResult:
1826
+ to_state = ProcessChatsState(str(getattr(target, "value", target)))
1827
+ observation = workflow_event.observation
1828
+ effect = _workflow_effect(
1829
+ workflow_event,
1830
+ to_state,
1831
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
1832
+ target="/mednotes:link",
1833
+ payload=LinkWorkflowRunEffectPayload(
1834
+ kind="link_run",
1835
+ diagnose=False,
1836
+ apply=True,
1837
+ trigger_context_path=observation.link_trigger_context_path,
1838
+ no_related_notes=False,
1839
+ ),
1840
+ mutates_resources=True,
1841
+ rollback_declared=True,
1842
+ )
1843
+ return self._transition(workflow_event, to_state, "publish.completed", effects=[effect])
1844
+
1845
+ def _on_observed_link_completed(
1846
+ self,
1847
+ workflow_event: ProcessChatsPublishRuntimeObservedEvent,
1848
+ target: object,
1849
+ ) -> WorkflowTransitionResult:
1850
+ return self._transition(workflow_event, ProcessChatsState(str(getattr(target, "value", target))), "link.completed")
1851
+
1852
+ def _on_observed_link_blocked(
1853
+ self,
1854
+ workflow_event: ProcessChatsPublishRuntimeObservedEvent,
1855
+ target: object,
1856
+ ) -> WorkflowTransitionResult:
1857
+ to_state = ProcessChatsState(str(getattr(target, "value", target)))
1858
+ reason_code = workflow_event.observation.reason_code or "process_chats_linker_blocked"
1859
+ next_action = workflow_event.observation.next_action or "Resolver pendências de conexões/grafo pela rota oficial."
1860
+ decision = _hard_block_decision(state=to_state, reason_code=reason_code, next_action=next_action)
1861
+ return self._transition(workflow_event, to_state, reason_code, decision=decision)
1862
+
1863
+ def _on_observed_quota_wait(
1864
+ self,
1865
+ workflow_event: ProcessChatsPublishRuntimeObservedEvent,
1866
+ target: object,
1867
+ ) -> WorkflowTransitionResult:
1868
+ to_state = ProcessChatsState(str(getattr(target, "value", target)))
1869
+ resume_action = workflow_event.observation.next_action or "Aguardar cota externa e retomar publicação."
1870
+ effect = _wait_external_effect(
1871
+ workflow_event,
1872
+ to_state,
1873
+ target="wait_external.publish_quota",
1874
+ wait_target="publish_quota",
1875
+ resume_action=resume_action,
1876
+ )
1877
+ return self._transition(
1878
+ workflow_event,
1879
+ to_state,
1880
+ workflow_event.observation.reason_code or "external.quota_reported",
1881
+ effects=[effect],
1882
+ resume_action=resume_action,
1883
+ )
1884
+
1885
+ def _on_observed_blocked(
1886
+ self,
1887
+ workflow_event: ProcessChatsPublishRuntimeObservedEvent,
1888
+ target: object,
1889
+ ) -> WorkflowTransitionResult:
1890
+ to_state = ProcessChatsState(str(getattr(target, "value", target)))
1891
+ observation = workflow_event.observation
1892
+ effect = _resume_architect_work_effect(workflow_event, to_state)
1893
+ return self._transition(
1894
+ workflow_event,
1895
+ to_state,
1896
+ observation.reason_code or to_state.value,
1897
+ effects=[effect],
1898
+ resume_action=observation.next_action,
1899
+ )
1900
+
1901
+ def _on_observed_publish_blocked(
1902
+ self,
1903
+ workflow_event: ProcessChatsPublishRuntimeObservedEvent,
1904
+ target: object,
1905
+ ) -> WorkflowTransitionResult:
1906
+ to_state = ProcessChatsState(str(getattr(target, "value", target)))
1907
+ observation = workflow_event.observation
1908
+ effect = _workflow_effect(
1909
+ workflow_event,
1910
+ to_state,
1911
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
1912
+ target="workflow.resume_publish_blocker",
1913
+ payload=ResumePublishBlockerEffectPayload(reason_code=observation.reason_code or "publish_receipt_invalid"),
1914
+ resume_action=observation.next_action,
1915
+ )
1916
+ return self._transition(
1917
+ workflow_event,
1918
+ to_state,
1919
+ observation.reason_code or "publish_receipt_invalid",
1920
+ effects=[effect],
1921
+ resume_action=observation.next_action,
1922
+ )
1923
+
1924
+ def _on_observed_rollback_recorded(
1925
+ self,
1926
+ workflow_event: ProcessChatsPublishRuntimeObservedEvent,
1927
+ target: object,
1928
+ ) -> WorkflowTransitionResult:
1929
+ to_state = ProcessChatsState(str(getattr(target, "value", target)))
1930
+ context = _publish_observation_error_context(workflow_event)
1931
+ decision = _failed_decision(
1932
+ state=to_state,
1933
+ reason_code=context.root_cause,
1934
+ next_action=context.next_action,
1935
+ )
1936
+ return self._transition(workflow_event, to_state, "rollback.failure_recorded", decision=decision)
1937
+
1938
+ def _observed_preview_ready(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1939
+ return workflow_event.observation.preview_ready
1940
+
1941
+ def _observed_publish_completed(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1942
+ return workflow_event.observation.publish_completed
1943
+
1944
+ def _observed_link_completed(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1945
+ return workflow_event.observation.link_completed
1946
+
1947
+ def _observed_link_blocked(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1948
+ return workflow_event.observation.link_blocked
1949
+
1950
+ def _observed_rollback_recorded(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1951
+ return workflow_event.observation.rollback_recorded
1952
+
1953
+ def _observed_quota_wait(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1954
+ return workflow_event.observation.quota_wait
1955
+
1956
+ def _observed_coverage_gap(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1957
+ return workflow_event.observation.validation_coverage_gap
1958
+
1959
+ def _observed_manifest_mismatch(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1960
+ return workflow_event.observation.validation_manifest_mismatch
1961
+
1962
+ def _observed_content_invalid(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1963
+ return workflow_event.observation.validation_content_invalid
1964
+
1965
+ def _observed_dry_run_receipt_required(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1966
+ return workflow_event.observation.publish_dry_run_receipt_required
1967
+
1968
+ def _observed_stale_receipt(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1969
+ return workflow_event.observation.publish_stale_receipt
1970
+
1971
+ def _observed_duplicate_target(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1972
+ return workflow_event.observation.publish_duplicate_target
1973
+
1974
+ def _observed_provenance_gap(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1975
+ return workflow_event.observation.publish_provenance_gap
1976
+
1977
+ def _observed_blocked(self, workflow_event: ProcessChatsPublishRuntimeObservedEvent) -> bool:
1978
+ return workflow_event.observation.blocked
1979
+
1980
+ def on_publish_dry_run_receipt_required(
1981
+ self, workflow_event: PublishDryRunReceiptRequiredEvent
1982
+ ) -> WorkflowTransitionResult:
1983
+ return self._recoverable_publish_block(
1984
+ workflow_event,
1985
+ ProcessChatsState.PUBLISH_DRY_RUN_RECEIPT_REQUIRED,
1986
+ )
1987
+
1988
+ def on_publish_stale_receipt(self, workflow_event: PublishStaleReceiptEvent) -> WorkflowTransitionResult:
1989
+ return self._recoverable_publish_block(workflow_event, ProcessChatsState.PUBLISH_STALE_RECEIPT)
1990
+
1991
+ def on_publish_duplicate_target(self, workflow_event: PublishDuplicateTargetEvent) -> WorkflowTransitionResult:
1992
+ return self._recoverable_publish_block(workflow_event, ProcessChatsState.PUBLISH_DUPLICATE_TARGET)
1993
+
1994
+ def on_publish_provenance_gap(self, workflow_event: PublishProvenanceGapEvent) -> WorkflowTransitionResult:
1995
+ return self._recoverable_publish_block(workflow_event, ProcessChatsState.PUBLISH_PROVENANCE_GAP)
1996
+
1997
+ def on_publish_receipt_invalid(self, workflow_event: PublishReceiptInvalidEvent) -> WorkflowTransitionResult:
1998
+ return self._recoverable_publish_block(workflow_event, ProcessChatsState.PUBLISH_RECEIPT_INVALID)
1999
+
2000
+ def _recoverable_publish_block(
2001
+ self,
2002
+ workflow_event: (
2003
+ PublishDryRunReceiptRequiredEvent
2004
+ | PublishStaleReceiptEvent
2005
+ | PublishDuplicateTargetEvent
2006
+ | PublishProvenanceGapEvent
2007
+ | PublishReceiptInvalidEvent
2008
+ ),
2009
+ to_state: ProcessChatsState,
2010
+ ) -> WorkflowTransitionResult:
2011
+ effect = _resume_publish_blocker_effect(workflow_event, to_state)
2012
+ return self._transition(
2013
+ workflow_event,
2014
+ to_state,
2015
+ workflow_event.reason_code,
2016
+ effects=[effect],
2017
+ resume_action=workflow_event.next_action,
2018
+ )
2019
+
2020
+ def on_publish_blocker_resolved(self, workflow_event: PublishBlockerResolvedEvent) -> WorkflowTransitionResult:
2021
+ effect = _publish_batch_effect(workflow_event, ProcessChatsState.PUBLISH_APPLY_REQUESTED)
2022
+ return self._transition(
2023
+ workflow_event,
2024
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
2025
+ "publish.blocker_resolved",
2026
+ effects=[effect],
2027
+ )
2028
+
2029
+ def on_external_quota_reported(self, workflow_event: ExternalQuotaReportedEvent) -> WorkflowTransitionResult:
2030
+ effect = _wait_external_effect(
2031
+ workflow_event,
2032
+ ProcessChatsState.PUBLISH_PAUSED_FOR_QUOTA,
2033
+ target="wait_external.publish_quota",
2034
+ wait_target="publish_quota",
2035
+ resume_action=workflow_event.resume_action,
2036
+ )
2037
+ return self._transition(
2038
+ workflow_event,
2039
+ ProcessChatsState.PUBLISH_PAUSED_FOR_QUOTA,
2040
+ "external.quota_reported",
2041
+ effects=[effect],
2042
+ resume_action=workflow_event.resume_action,
2043
+ )
2044
+
2045
+ def on_external_ready(self, workflow_event: ExternalReadyEvent) -> WorkflowTransitionResult:
2046
+ effect = _publish_batch_effect(workflow_event, ProcessChatsState.PUBLISH_APPLY_REQUESTED)
2047
+ return self._transition(
2048
+ workflow_event,
2049
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
2050
+ "external.ready",
2051
+ effects=[effect],
2052
+ )
2053
+
2054
+ def on_link_run_completed(self, workflow_event: LinkRunCompletedEvent) -> WorkflowTransitionResult:
2055
+ return self._transition(workflow_event, ProcessChatsState.PUBLISHED, "link.completed")
2056
+
2057
+ def on_link_run_blocked(self, workflow_event: LinkRunBlockedEvent) -> WorkflowTransitionResult:
2058
+ decision = _hard_block_decision(
2059
+ state=ProcessChatsState.COMPLETED_WITH_LINK_BLOCKERS,
2060
+ reason_code=workflow_event.reason_code,
2061
+ next_action=workflow_event.next_action,
2062
+ )
2063
+ return self._transition(
2064
+ workflow_event,
2065
+ ProcessChatsState.COMPLETED_WITH_LINK_BLOCKERS,
2066
+ workflow_event.reason_code,
2067
+ decision=decision,
2068
+ )
2069
+
2070
+ def on_missing_next_action(self, workflow_event: MissingNextActionEvent) -> WorkflowTransitionResult:
2071
+ decision = _hard_block_decision(
2072
+ state=ProcessChatsState.CONTRACT_GAP_MISSING_NEXT_ACTION,
2073
+ reason_code="contract_gap.missing_next_action",
2074
+ next_action=workflow_event.next_action_hint,
2075
+ )
2076
+ return self._transition(
2077
+ workflow_event,
2078
+ ProcessChatsState.CONTRACT_GAP_MISSING_NEXT_ACTION,
2079
+ "contract_gap.missing_next_action",
2080
+ decision=decision,
2081
+ )
2082
+
2083
+ def on_missing_error_context(self, workflow_event: MissingErrorContextEvent) -> WorkflowTransitionResult:
2084
+ decision = _hard_block_decision(
2085
+ state=ProcessChatsState.CONTRACT_GAP_MISSING_ERROR_CONTEXT,
2086
+ reason_code="contract_gap.missing_error_context",
2087
+ next_action=workflow_event.error_context_hint,
2088
+ )
2089
+ return self._transition(
2090
+ workflow_event,
2091
+ ProcessChatsState.CONTRACT_GAP_MISSING_ERROR_CONTEXT,
2092
+ "contract_gap.missing_error_context",
2093
+ decision=decision,
2094
+ )
2095
+
2096
+ def on_agent_tool_contract_violation(
2097
+ self, workflow_event: AgentToolContractViolationEvent
2098
+ ) -> WorkflowTransitionResult:
2099
+ to_state = ProcessChatsState.AGENT_TOOL_CONTRACT_VIOLATION
2100
+ decision = _hard_block_decision(
2101
+ state=to_state,
2102
+ reason_code="agent_tool_contract_violation",
2103
+ next_action=workflow_event.error_context.next_action,
2104
+ )
2105
+ return self._transition(workflow_event, to_state, "agent_tool_contract_violation", decision=decision)
2106
+
2107
+ def on_rollback_completed(self, workflow_event: RollbackCompletedEvent) -> WorkflowTransitionResult:
2108
+ effect = _failure_finalization_effect(workflow_event, ProcessChatsState.ROLLBACK_RECORDED)
2109
+ return self._transition(
2110
+ workflow_event,
2111
+ ProcessChatsState.ROLLBACK_RECORDED,
2112
+ "rollback.completed",
2113
+ effects=[effect],
2114
+ )
2115
+
2116
+ def on_rollback_failure_recorded(self, workflow_event: RollbackFailureRecordedEvent) -> WorkflowTransitionResult:
2117
+ decision = _failed_decision(
2118
+ state=ProcessChatsState.TERMINAL_FAILURE_RECORDED,
2119
+ reason_code=workflow_event.error_context.root_cause,
2120
+ next_action=workflow_event.error_context.next_action,
2121
+ )
2122
+ return self._transition(
2123
+ workflow_event,
2124
+ ProcessChatsState.TERMINAL_FAILURE_RECORDED,
2125
+ "rollback.failure_recorded",
2126
+ decision=decision,
2127
+ )
2128
+
2129
+ def _transition(
2130
+ self,
2131
+ workflow_event: ProcessChatsEvent,
2132
+ to_state: ProcessChatsState,
2133
+ reason_code: str,
2134
+ *,
2135
+ effects: list[WorkflowEffect] | None = None,
2136
+ decision: WorkflowDecision | None = None,
2137
+ human_decision_packet: HumanDecisionPacket | None = None,
2138
+ resume_action: str = "",
2139
+ ) -> WorkflowTransitionResult:
2140
+ return WorkflowTransitionResult(
2141
+ workflow=workflow_event.workflow,
2142
+ run_id=workflow_event.run_id,
2143
+ from_state=workflow_event.current_state,
2144
+ to_state=to_state.value,
2145
+ trigger=_event_name(workflow_event),
2146
+ reason_code=reason_code,
2147
+ effects=effects or [],
2148
+ decision=decision,
2149
+ human_decision_packet=human_decision_packet,
2150
+ resume_action=resume_action,
2151
+ )
2152
+
2153
+
2154
+ def _workflow_effect(
2155
+ workflow_event: ProcessChatsEvent,
2156
+ to_state: ProcessChatsState,
2157
+ *,
2158
+ kind: WorkflowEffectKind,
2159
+ target: str,
2160
+ payload: ProcessChatsEffectPayload,
2161
+ mutates_resources: bool = False,
2162
+ rollback_declared: bool = False,
2163
+ requires_receipt: bool = True,
2164
+ requires_attestation: bool = False,
2165
+ model_policy: dict[str, str] | None = None,
2166
+ resume_action: str = "",
2167
+ ) -> WorkflowEffect:
2168
+ payload_model = ProcessChatsEffectPayloadAdapter.validate_python(payload)
2169
+ return WorkflowEffect(
2170
+ workflow=workflow_event.workflow,
2171
+ run_id=workflow_event.run_id,
2172
+ effect_id=f"{workflow_event.run_id}:{_event_name(workflow_event)}:{target}",
2173
+ origin_state=to_state.value,
2174
+ kind=kind,
2175
+ target=target,
2176
+ payload=payload_model.to_payload(),
2177
+ mutates_resources=mutates_resources,
2178
+ rollback_declared=rollback_declared,
2179
+ no_resource_mutation=not mutates_resources,
2180
+ requires_receipt=requires_receipt,
2181
+ requires_attestation=requires_attestation,
2182
+ model_policy=model_policy or {},
2183
+ resume_action=resume_action,
2184
+ )
2185
+
2186
+
2187
+ def _setup_paths_effect(event: PathsMissingEvent, to_state: ProcessChatsState) -> WorkflowEffect:
2188
+ return _workflow_effect(
2189
+ event,
2190
+ to_state,
2191
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2192
+ target=event.setup_target,
2193
+ payload=SetupPathsEffectPayload(missing_path_kind=event.missing_path_kind),
2194
+ )
2195
+
2196
+
2197
+ def _setup_bootstrap_effect(event: WindowsPathOrVenvBlockedEvent, to_state: ProcessChatsState) -> WorkflowEffect:
2198
+ return _workflow_effect(
2199
+ event,
2200
+ to_state,
2201
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2202
+ target=event.setup_target,
2203
+ payload=SetupBootstrapEffectPayload(reason_code=event.reason_code),
2204
+ )
2205
+
2206
+
2207
+ def _architect_effect(event: TriagePlanCreatedEvent, to_state: ProcessChatsState) -> WorkflowEffect:
2208
+ return _workflow_effect(
2209
+ event,
2210
+ to_state,
2211
+ kind=WorkflowEffectKind.CALL_SPECIALIST_MODEL,
2212
+ target="med-knowledge-architect",
2213
+ payload=ArchitectSpecialistEffectPayload(
2214
+ note_plan_hash=event.note_plan_hash,
2215
+ raw_file_count=event.raw_file_count,
2216
+ ),
2217
+ requires_attestation=True,
2218
+ model_policy={"specialist": "med-knowledge-architect"},
2219
+ )
2220
+
2221
+
2222
+ def _architect_planning_effect(event: TriagedRawChatsAvailableEvent, to_state: ProcessChatsState) -> WorkflowEffect:
2223
+ return _workflow_effect(
2224
+ event,
2225
+ to_state,
2226
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2227
+ target="/mednotes:process-chats plan-subagents architect",
2228
+ payload=ArchitectPlanningEffectPayload(triaged_count=event.triaged_count),
2229
+ requires_receipt=False,
2230
+ resume_action="Continuar com list-triados e plan-subagents --phase architect.",
2231
+ )
2232
+
2233
+
2234
+ def _architect_retry_effect(event: ProcessChatsEvent, to_state: ProcessChatsState) -> WorkflowEffect:
2235
+ return _workflow_effect(
2236
+ event,
2237
+ to_state,
2238
+ kind=WorkflowEffectKind.CALL_SPECIALIST_MODEL,
2239
+ target="med-knowledge-architect",
2240
+ payload=ArchitectSpecialistEffectPayload(note_plan_hash="resume", raw_file_count=0),
2241
+ requires_attestation=True,
2242
+ model_policy={"specialist": "med-knowledge-architect"},
2243
+ )
2244
+
2245
+
2246
+ def _resume_architect_work_effect(event: ProcessChatsEvent, to_state: ProcessChatsState) -> WorkflowEffect:
2247
+ return _workflow_effect(
2248
+ event,
2249
+ to_state,
2250
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2251
+ target="workflow.resume_architect_work",
2252
+ payload=ResumeArchitectWorkEffectPayload(resolved_by="architect_retry"),
2253
+ )
2254
+
2255
+
2256
+ def _resume_publish_blocker_effect(
2257
+ event: (
2258
+ PublishDryRunReceiptRequiredEvent
2259
+ | PublishStaleReceiptEvent
2260
+ | PublishDuplicateTargetEvent
2261
+ | PublishProvenanceGapEvent
2262
+ | PublishReceiptInvalidEvent
2263
+ ),
2264
+ to_state: ProcessChatsState,
2265
+ ) -> WorkflowEffect:
2266
+ return _workflow_effect(
2267
+ event,
2268
+ to_state,
2269
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2270
+ target="workflow.resume_publish_blocker",
2271
+ payload=ResumePublishBlockerEffectPayload(reason_code=event.reason_code),
2272
+ resume_action=event.next_action,
2273
+ )
2274
+
2275
+
2276
+ def _plan_attestation_effect(
2277
+ event: SubagentPlanAttestationMissingEvent | SubagentPlanAttestationInvalidEvent,
2278
+ to_state: ProcessChatsState,
2279
+ ) -> WorkflowEffect:
2280
+ return _workflow_effect(
2281
+ event,
2282
+ to_state,
2283
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2284
+ target="agent.plan_attestation",
2285
+ payload=PlanAttestationEffectPayload(reason_code=event.reason_code),
2286
+ )
2287
+
2288
+
2289
+ def _wait_external_effect(
2290
+ event: ProcessChatsEvent,
2291
+ to_state: ProcessChatsState,
2292
+ *,
2293
+ target: str,
2294
+ wait_target: Literal["specialist_capacity", "publish_quota"],
2295
+ resume_action: str,
2296
+ ) -> WorkflowEffect:
2297
+ return _workflow_effect(
2298
+ event,
2299
+ to_state,
2300
+ kind=WorkflowEffectKind.WAIT_EXTERNAL,
2301
+ target=target,
2302
+ payload=WaitExternalEffectPayload(
2303
+ wait_target=wait_target,
2304
+ blocked_reason=target,
2305
+ next_action=resume_action,
2306
+ resume_supported=True,
2307
+ ),
2308
+ requires_receipt=False,
2309
+ resume_action=resume_action,
2310
+ )
2311
+
2312
+
2313
+ def _publish_observation_error_context(
2314
+ event: ProcessChatsPublishRuntimeObservedEvent,
2315
+ ) -> ProcessChatsErrorContext:
2316
+ observation = event.observation
2317
+ if observation.error_context is not None:
2318
+ return observation.error_context
2319
+ reason = observation.reason_code or "process_chats_publish_runtime_blocked"
2320
+ return ProcessChatsErrorContext(
2321
+ root_cause=reason,
2322
+ affected_artifact=observation.manifest_path or "process-chats-manifest",
2323
+ retry_scope="process-chats",
2324
+ next_action=observation.next_action or "Corrigir o bloqueio e retomar /mednotes:process-chats.",
2325
+ )
2326
+
2327
+
2328
+ def _human_publish_decision_effect(event: PublishPreviewProducedEvent, to_state: ProcessChatsState) -> WorkflowEffect:
2329
+ return _workflow_effect(
2330
+ event,
2331
+ to_state,
2332
+ kind=WorkflowEffectKind.ASK_HUMAN,
2333
+ target="human.publish_decision",
2334
+ payload=HumanPublishDecisionEffectPayload(
2335
+ manifest_path=event.manifest_path,
2336
+ dry_run_receipt_path=event.dry_run_receipt_path,
2337
+ ),
2338
+ requires_receipt=False,
2339
+ )
2340
+
2341
+
2342
+ def _vault_guard_decision_effect(event: VaultGuardRequiredEvent, to_state: ProcessChatsState) -> WorkflowEffect:
2343
+ return _workflow_effect(
2344
+ event,
2345
+ to_state,
2346
+ kind=WorkflowEffectKind.ASK_HUMAN,
2347
+ target="human.vault_guard_decision",
2348
+ payload=VaultGuardDecisionEffectPayload(changed_file_count=event.changed_file_count),
2349
+ requires_receipt=False,
2350
+ )
2351
+
2352
+
2353
+ def _publish_batch_effect(event: ProcessChatsEvent, to_state: ProcessChatsState) -> WorkflowEffect:
2354
+ if not isinstance(event, HumanPublishApprovalEvent | ExternalReadyEvent | PublishBlockerResolvedEvent):
2355
+ raise TypeError("publish-batch effect requires an event with validated receipt paths")
2356
+ return _workflow_effect(
2357
+ event,
2358
+ to_state,
2359
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2360
+ target="publish-batch",
2361
+ payload=PublishBatchEffectPayload(
2362
+ schema_version="medical-notes-workbench.process-chats.publish-batch-effect.v1",
2363
+ kind="publish_batch",
2364
+ manifest_path=event.manifest_path,
2365
+ dry_run_receipt_path=event.dry_run_receipt_path,
2366
+ ),
2367
+ mutates_resources=True,
2368
+ rollback_declared=True,
2369
+ )
2370
+
2371
+
2372
+ def _link_effect(event: PublishBatchCompletedEvent, to_state: ProcessChatsState) -> WorkflowEffect:
2373
+ return _workflow_effect(
2374
+ event,
2375
+ to_state,
2376
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2377
+ target="/mednotes:link",
2378
+ payload=LinkWorkflowRunEffectPayload(
2379
+ kind="link_run",
2380
+ diagnose=False,
2381
+ apply=True,
2382
+ trigger_context_path=event.link_trigger_context_path,
2383
+ no_related_notes=False,
2384
+ ),
2385
+ mutates_resources=True,
2386
+ rollback_declared=True,
2387
+ )
2388
+
2389
+
2390
+ def _rollback_effect(event: ProcessChatsEvent, to_state: ProcessChatsState) -> WorkflowEffect:
2391
+ return _workflow_effect(
2392
+ event,
2393
+ to_state,
2394
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2395
+ target="vault.rollback",
2396
+ payload=RollbackEffectPayload(failed_origin_state=event.current_state),
2397
+ mutates_resources=True,
2398
+ rollback_declared=True,
2399
+ )
2400
+
2401
+
2402
+ def _failure_finalization_effect(event: ProcessChatsEvent, to_state: ProcessChatsState) -> WorkflowEffect:
2403
+ return _workflow_effect(
2404
+ event,
2405
+ to_state,
2406
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2407
+ target="workflow.failure_finalization",
2408
+ payload=FailureFinalizationEffectPayload(),
2409
+ )
2410
+
2411
+
2412
+ def _hard_block_decision(*, state: ProcessChatsState, reason_code: str, next_action: str) -> WorkflowDecision:
2413
+ return WorkflowDecision(
2414
+ kind=WorkflowDecisionKind.HARD_BLOCK,
2415
+ phase=ProcessChatsMachine._PHASE_BY_STATE[state],
2416
+ reason_code=reason_code,
2417
+ public_summary=f"Process-chats bloqueado em {state.value}.",
2418
+ developer_summary=f"Process-chats reached blocked leaf {state.value}.",
2419
+ evidence=[
2420
+ DecisionEvidence(
2421
+ summary=f"Estado operacional bloqueado: {state.value}",
2422
+ technical_code=reason_code,
2423
+ source="process_chats_machine",
2424
+ )
2425
+ ],
2426
+ next_action=next_action,
2427
+ )
2428
+
2429
+
2430
+ def _failed_decision(*, state: ProcessChatsState, reason_code: str, next_action: str) -> WorkflowDecision:
2431
+ return WorkflowDecision(
2432
+ kind=WorkflowDecisionKind.FAILED,
2433
+ phase=ProcessChatsMachine._PHASE_BY_STATE[state],
2434
+ reason_code=reason_code,
2435
+ public_summary="Process-chats falhou após registrar rollback/recuperação.",
2436
+ developer_summary=f"Process-chats reached failed leaf {state.value}.",
2437
+ evidence=[
2438
+ DecisionEvidence(
2439
+ summary=f"Estado terminal de falha: {state.value}",
2440
+ technical_code=reason_code,
2441
+ source="process_chats_machine",
2442
+ )
2443
+ ],
2444
+ next_action=next_action,
2445
+ )
2446
+
2447
+
2448
+ def _ask_human_decision(
2449
+ *,
2450
+ state: ProcessChatsState,
2451
+ reason_code: str,
2452
+ public_summary: str,
2453
+ next_action: str,
2454
+ recommended_option_id: str,
2455
+ options: tuple[HumanDecisionOption, ...],
2456
+ ) -> WorkflowDecision:
2457
+ return WorkflowDecision(
2458
+ kind=WorkflowDecisionKind.ASK_HUMAN,
2459
+ phase=ProcessChatsMachine._PHASE_BY_STATE[state],
2460
+ reason_code=reason_code,
2461
+ public_summary=public_summary,
2462
+ developer_summary="A decisão humana protege uma mutação ou parada limpa do workflow.",
2463
+ evidence=[
2464
+ DecisionEvidence(
2465
+ summary=f"Estado aguardando decisão humana: {state.value}",
2466
+ technical_code=reason_code,
2467
+ source="process_chats_machine",
2468
+ )
2469
+ ],
2470
+ next_action=next_action,
2471
+ resume_action=next_action,
2472
+ rejected_automations=_rejected_automations(reason_code),
2473
+ recommended_option_id=recommended_option_id,
2474
+ options=list(options),
2475
+ human_decision_kind=reason_code,
2476
+ )
2477
+
2478
+
2479
+ def _rejected_automations(reason_code: str) -> list[RejectedAutomation]:
2480
+ return [
2481
+ RejectedAutomation(
2482
+ kind=kind,
2483
+ reason_code=reason_code,
2484
+ reason="A escolha altera segurança, mutação ou publicação e exige confirmação explícita.",
2485
+ )
2486
+ for kind in WorkflowAutomationKind
2487
+ ]
2488
+
2489
+
2490
+ def process_chats_event_from_effect_result(
2491
+ model: WorkflowModel,
2492
+ result: WorkflowEffectResult,
2493
+ ) -> ProcessChatsBoundaryEvent:
2494
+ """Convert an adapter result to one typed event using only the outcome matrix."""
2495
+
2496
+ if result.effect.workflow != model.workflow or result.effect.run_id != model.run_id:
2497
+ raise ValueError("effect result belongs to a different process-chats run")
2498
+ if result.effect.origin_state != model.state:
2499
+ raise ValueError("effect origin_state does not match current workflow state")
2500
+ outcome = ProcessChatsEffectOutcomeAdapter.validate_python(result.outcome.model_dump(mode="json"))
2501
+ row = PROCESS_CHATS_EFFECT_RETURN_EVENT_MATRIX.lookup(
2502
+ kind=result.effect.kind,
2503
+ target=result.effect.target,
2504
+ origin_state=result.effect.origin_state,
2505
+ outcome_code=outcome.code,
2506
+ )
2507
+ event = _event_from_outcome(row, model=model, outcome=outcome)
2508
+ return ProcessChatsBoundaryEventAdapter.validate_python(event.to_payload())
2509
+
2510
+
2511
+ def _event_from_outcome(
2512
+ row: ProcessChatsEffectReturnEventRow,
2513
+ *,
2514
+ model: WorkflowModel,
2515
+ outcome: ProcessChatsEffectOutcome,
2516
+ ) -> ProcessChatsEvent:
2517
+ common: _ProcessChatsEventCommonKwargs = {
2518
+ "workflow": PROCESS_CHATS_WORKFLOW,
2519
+ "run_id": model.run_id,
2520
+ "current_state": model.state,
2521
+ }
2522
+ match outcome:
2523
+ case SetupPathsConfiguredOutcome():
2524
+ return PathsConfiguredEvent(**common, config_path=outcome.config_path)
2525
+ case SetupBootstrapCompletedOutcome():
2526
+ return EnvironmentBootstrapCompletedEvent(**common, bootstrap_summary=outcome.bootstrap_summary)
2527
+ case ArchitectCompletedOutcome():
2528
+ return ArchitectWorkCompletedEvent(
2529
+ **common,
2530
+ receipt_id=outcome.receipt_id,
2531
+ attestation_hash=outcome.attestation_hash,
2532
+ coverage_path=outcome.coverage_path,
2533
+ manifest_path=outcome.manifest_path,
2534
+ )
2535
+ case ArchitectCapacityBlockedOutcome():
2536
+ return ArchitectSpecialistCapacityBlockedEvent(
2537
+ **common,
2538
+ reason_code=outcome.reason_code,
2539
+ resume_action=outcome.resume_action,
2540
+ )
2541
+ case AgentToolContractViolationOutcome():
2542
+ return AgentToolContractViolationEvent(
2543
+ **common,
2544
+ origin_event=outcome.origin_event,
2545
+ error_context=outcome.error_context,
2546
+ )
2547
+ case ExternalReadyOutcome():
2548
+ if row.event_model is ArchitectSpecialistCapacityRestoredEvent:
2549
+ return ArchitectSpecialistCapacityRestoredEvent(**common, restored_by=outcome.restored_by)
2550
+ return ExternalReadyEvent(
2551
+ **common,
2552
+ restored_by=outcome.restored_by,
2553
+ manifest_path=outcome.manifest_path,
2554
+ dry_run_receipt_path=outcome.dry_run_receipt_path,
2555
+ )
2556
+ case AttestationSuppliedOutcome():
2557
+ return SubagentPlanAttestationSuppliedEvent(**common, attestation_hash=outcome.attestation_hash)
2558
+ case ValidationRetryRequestedOutcome():
2559
+ return NoteValidationRetryRequestedEvent(**common, resolved_by=outcome.resolved_by)
2560
+ case HumanConfirmedOutcome():
2561
+ return VaultGuardConfirmedEvent(**common, confirmed_by=outcome.confirmed_by)
2562
+ case HumanRejectedOutcome():
2563
+ return VaultGuardRejectedEvent(**common, rejected_by=outcome.rejected_by)
2564
+ case HumanApprovedOutcome():
2565
+ return HumanPublishApprovalEvent(
2566
+ **common,
2567
+ approved_by=outcome.approved_by,
2568
+ manifest_path=outcome.manifest_path,
2569
+ dry_run_receipt_path=outcome.dry_run_receipt_path,
2570
+ )
2571
+ case HumanCancelledOutcome():
2572
+ return HumanPublishCancellationEvent(**common, cancelled_by=outcome.cancelled_by)
2573
+ case PublishBatchCompletedOutcome():
2574
+ return PublishBatchCompletedEvent(
2575
+ **common,
2576
+ receipt_id=outcome.receipt_id,
2577
+ published_count=outcome.published_count,
2578
+ link_trigger_context_path=outcome.link_trigger_context_path,
2579
+ )
2580
+ case PublishDryRunReceiptRequiredOutcome():
2581
+ return PublishDryRunReceiptRequiredEvent(
2582
+ **common,
2583
+ reason_code=outcome.reason_code,
2584
+ next_action=outcome.next_action,
2585
+ error_context=outcome.error_context,
2586
+ )
2587
+ case PublishStaleReceiptOutcome():
2588
+ return PublishStaleReceiptEvent(
2589
+ **common,
2590
+ reason_code=outcome.reason_code,
2591
+ next_action=outcome.next_action,
2592
+ error_context=outcome.error_context,
2593
+ )
2594
+ case PublishDuplicateTargetOutcome():
2595
+ return PublishDuplicateTargetEvent(
2596
+ **common,
2597
+ reason_code=outcome.reason_code,
2598
+ next_action=outcome.next_action,
2599
+ error_context=outcome.error_context,
2600
+ )
2601
+ case PublishProvenanceGapOutcome():
2602
+ return PublishProvenanceGapEvent(
2603
+ **common,
2604
+ reason_code=outcome.reason_code,
2605
+ next_action=outcome.next_action,
2606
+ error_context=outcome.error_context,
2607
+ )
2608
+ case PublishReceiptInvalidOutcome():
2609
+ return PublishReceiptInvalidEvent(
2610
+ **common,
2611
+ reason_code=outcome.reason_code,
2612
+ next_action=outcome.next_action,
2613
+ error_context=outcome.error_context,
2614
+ )
2615
+ case ExternalQuotaReportedOutcome():
2616
+ return ExternalQuotaReportedEvent(
2617
+ **common,
2618
+ quota_kind=outcome.quota_kind,
2619
+ resume_action=outcome.resume_action,
2620
+ )
2621
+ case PublishBlockerResolvedOutcome():
2622
+ return PublishBlockerResolvedEvent(
2623
+ **common,
2624
+ resolved_by=outcome.resolved_by,
2625
+ manifest_path=outcome.manifest_path,
2626
+ dry_run_receipt_path=outcome.dry_run_receipt_path,
2627
+ )
2628
+ case LinkCompletedOutcome():
2629
+ return LinkRunCompletedEvent(
2630
+ **common,
2631
+ receipt_id=outcome.receipt_id,
2632
+ changed_files=list(outcome.changed_files),
2633
+ )
2634
+ case LinkBlockedOutcome():
2635
+ return LinkRunBlockedEvent(
2636
+ **common,
2637
+ reason_code=outcome.reason_code,
2638
+ next_action=outcome.next_action,
2639
+ error_context=outcome.error_context,
2640
+ )
2641
+ case RollbackCompletedOutcome():
2642
+ return RollbackCompletedEvent(**common, rollback_receipt_id=outcome.rollback_receipt_id)
2643
+ case RollbackFailureRecordedOutcome():
2644
+ return RollbackFailureRecordedEvent(**common, error_context=outcome.error_context)
2645
+ case ContractMissingNextActionOutcome():
2646
+ return MissingNextActionEvent(
2647
+ **common,
2648
+ contract_source=row.origin_state,
2649
+ next_action_hint=outcome.next_action_hint,
2650
+ )
2651
+ case ContractMissingErrorContextOutcome():
2652
+ return MissingErrorContextEvent(
2653
+ **common,
2654
+ contract_source=row.origin_state,
2655
+ error_context_hint=outcome.error_context_hint,
2656
+ )
2657
+
2658
+
2659
+ @dataclass(frozen=True)
2660
+ class ProcessChatsEffectBuilderCase:
2661
+ origin_state: str
2662
+ outcomes: tuple[ProcessChatsEffectOutcome, ...]
2663
+ build: Callable[[], WorkflowEffect]
2664
+
2665
+
2666
+ def process_chats_effect_builders(
2667
+ *,
2668
+ workflow: str,
2669
+ run_id: str,
2670
+ ) -> tuple[ProcessChatsEffectBuilderCase, ...]:
2671
+ """Return representative effects so matrix coverage tests track all builders."""
2672
+
2673
+ if workflow != PROCESS_CHATS_WORKFLOW:
2674
+ raise ValueError(f"process-chats effect builders require workflow {PROCESS_CHATS_WORKFLOW}")
2675
+ event = _BuilderEvent(
2676
+ workflow=PROCESS_CHATS_WORKFLOW,
2677
+ run_id=run_id,
2678
+ current_state=ProcessChatsState.ENVIRONMENT_CHECKING.value,
2679
+ )
2680
+
2681
+ def effect_for(
2682
+ origin: ProcessChatsState,
2683
+ *,
2684
+ kind: WorkflowEffectKind,
2685
+ target: str,
2686
+ payload: ProcessChatsEffectPayload,
2687
+ mutates_resources: bool = False,
2688
+ rollback_declared: bool = False,
2689
+ requires_attestation: bool = False,
2690
+ model_policy: dict[str, str] | None = None,
2691
+ ) -> WorkflowEffect:
2692
+ return _workflow_effect(
2693
+ event,
2694
+ origin,
2695
+ kind=kind,
2696
+ target=target,
2697
+ payload=payload,
2698
+ mutates_resources=mutates_resources,
2699
+ rollback_declared=rollback_declared,
2700
+ requires_attestation=requires_attestation,
2701
+ model_policy=model_policy,
2702
+ )
2703
+
2704
+ err = ProcessChatsErrorContext(
2705
+ root_cause="contract_gap",
2706
+ affected_artifact="builder",
2707
+ retry_scope="single-run",
2708
+ next_action="Corrigir contrato e retomar.",
2709
+ )
2710
+ return (
2711
+ ProcessChatsEffectBuilderCase(
2712
+ origin_state=ProcessChatsState.ENVIRONMENT_PATHS_MISSING.value,
2713
+ outcomes=(
2714
+ SetupPathsConfiguredOutcome(config_path="config.toml"),
2715
+ ContractMissingNextActionOutcome(next_action_hint="Informar próxima ação de setup."),
2716
+ ),
2717
+ build=lambda: effect_for(
2718
+ ProcessChatsState.ENVIRONMENT_PATHS_MISSING,
2719
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2720
+ target="/mednotes:setup paths",
2721
+ payload=SetupPathsEffectPayload(missing_path_kind="wiki_dir"),
2722
+ ),
2723
+ ),
2724
+ ProcessChatsEffectBuilderCase(
2725
+ origin_state=ProcessChatsState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED.value,
2726
+ outcomes=(
2727
+ SetupBootstrapCompletedOutcome(bootstrap_summary="bootstrap ok"),
2728
+ ContractMissingErrorContextOutcome(error_context_hint="Informar error_context."),
2729
+ ),
2730
+ build=lambda: effect_for(
2731
+ ProcessChatsState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED,
2732
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2733
+ target="/mednotes:setup bootstrap",
2734
+ payload=SetupBootstrapEffectPayload(reason_code="environment_blocker.windows_path_or_venv"),
2735
+ ),
2736
+ ),
2737
+ ProcessChatsEffectBuilderCase(
2738
+ origin_state=ProcessChatsState.ARCHITECT_WORK_REQUESTED.value,
2739
+ outcomes=(
2740
+ ArchitectCompletedOutcome(
2741
+ receipt_id="receipt",
2742
+ attestation_hash="attestation",
2743
+ coverage_path="/tmp/coverage.json",
2744
+ manifest_path="/tmp/manifest.json",
2745
+ ),
2746
+ ArchitectCapacityBlockedOutcome(
2747
+ reason_code="specialist_model_quota_exhausted",
2748
+ resume_action="Aguardar capacidade.",
2749
+ ),
2750
+ AgentToolContractViolationOutcome(origin_event="architect_work_completed", error_context=err),
2751
+ ),
2752
+ build=lambda: effect_for(
2753
+ ProcessChatsState.ARCHITECT_WORK_REQUESTED,
2754
+ kind=WorkflowEffectKind.CALL_SPECIALIST_MODEL,
2755
+ target="med-knowledge-architect",
2756
+ payload=ArchitectSpecialistEffectPayload(note_plan_hash="note-plan", raw_file_count=1),
2757
+ requires_attestation=True,
2758
+ model_policy={"specialist": "med-knowledge-architect"},
2759
+ ),
2760
+ ),
2761
+ ProcessChatsEffectBuilderCase(
2762
+ origin_state=ProcessChatsState.ARCHITECT_AWAITING_SPECIALIST_CAPACITY.value,
2763
+ outcomes=(
2764
+ ExternalReadyOutcome(
2765
+ restored_by="quota_window",
2766
+ manifest_path="/tmp/manifest.json",
2767
+ dry_run_receipt_path="/tmp/dry-run.json",
2768
+ ),
2769
+ ContractMissingNextActionOutcome(next_action_hint="Informar retomada."),
2770
+ ),
2771
+ build=lambda: effect_for(
2772
+ ProcessChatsState.ARCHITECT_AWAITING_SPECIALIST_CAPACITY,
2773
+ kind=WorkflowEffectKind.WAIT_EXTERNAL,
2774
+ target="wait_external.specialist_capacity",
2775
+ payload=WaitExternalEffectPayload(
2776
+ wait_target="specialist_capacity",
2777
+ blocked_reason="wait_external.specialist_capacity",
2778
+ next_action="Aguardar especialista.",
2779
+ resume_supported=True,
2780
+ ),
2781
+ ),
2782
+ ),
2783
+ ProcessChatsEffectBuilderCase(
2784
+ origin_state=ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_REQUIRED.value,
2785
+ outcomes=(
2786
+ AttestationSuppliedOutcome(attestation_hash="attestation"),
2787
+ ContractMissingErrorContextOutcome(error_context_hint="Informar erro de attestation."),
2788
+ ),
2789
+ build=lambda: effect_for(
2790
+ ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_REQUIRED,
2791
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2792
+ target="agent.plan_attestation",
2793
+ payload=PlanAttestationEffectPayload(reason_code="subagent_plan_attestation_required"),
2794
+ ),
2795
+ ),
2796
+ ProcessChatsEffectBuilderCase(
2797
+ origin_state=ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_INVALID.value,
2798
+ outcomes=(
2799
+ AttestationSuppliedOutcome(attestation_hash="attestation"),
2800
+ ContractMissingErrorContextOutcome(error_context_hint="Informar erro de attestation."),
2801
+ ),
2802
+ build=lambda: effect_for(
2803
+ ProcessChatsState.SUBAGENT_PLAN_ATTESTATION_INVALID,
2804
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2805
+ target="agent.plan_attestation",
2806
+ payload=PlanAttestationEffectPayload(reason_code="subagent_plan_attestation_invalid"),
2807
+ ),
2808
+ ),
2809
+ ProcessChatsEffectBuilderCase(
2810
+ origin_state=ProcessChatsState.NOTE_VALIDATION_COVERAGE_GAP.value,
2811
+ outcomes=(
2812
+ ValidationRetryRequestedOutcome(resolved_by="architect_retry"),
2813
+ ContractMissingErrorContextOutcome(error_context_hint="Informar erro de validação."),
2814
+ ),
2815
+ build=lambda: effect_for(
2816
+ ProcessChatsState.NOTE_VALIDATION_COVERAGE_GAP,
2817
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2818
+ target="workflow.resume_architect_work",
2819
+ payload=ResumeArchitectWorkEffectPayload(resolved_by="architect_retry"),
2820
+ ),
2821
+ ),
2822
+ ProcessChatsEffectBuilderCase(
2823
+ origin_state=ProcessChatsState.NOTE_VALIDATION_MANIFEST_MISMATCH.value,
2824
+ outcomes=(
2825
+ ValidationRetryRequestedOutcome(resolved_by="architect_retry"),
2826
+ ContractMissingErrorContextOutcome(error_context_hint="Informar erro de validação."),
2827
+ ),
2828
+ build=lambda: effect_for(
2829
+ ProcessChatsState.NOTE_VALIDATION_MANIFEST_MISMATCH,
2830
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2831
+ target="workflow.resume_architect_work",
2832
+ payload=ResumeArchitectWorkEffectPayload(resolved_by="architect_retry"),
2833
+ ),
2834
+ ),
2835
+ ProcessChatsEffectBuilderCase(
2836
+ origin_state=ProcessChatsState.NOTE_VALIDATION_CONTENT_INVALID.value,
2837
+ outcomes=(
2838
+ ValidationRetryRequestedOutcome(resolved_by="architect_retry"),
2839
+ ContractMissingErrorContextOutcome(error_context_hint="Informar erro de validação."),
2840
+ ),
2841
+ build=lambda: effect_for(
2842
+ ProcessChatsState.NOTE_VALIDATION_CONTENT_INVALID,
2843
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2844
+ target="workflow.resume_architect_work",
2845
+ payload=ResumeArchitectWorkEffectPayload(resolved_by="architect_retry"),
2846
+ ),
2847
+ ),
2848
+ ProcessChatsEffectBuilderCase(
2849
+ origin_state=ProcessChatsState.VAULT_GUARD_DECISION_REQUIRED.value,
2850
+ outcomes=(HumanConfirmedOutcome(confirmed_by="human"), HumanRejectedOutcome(rejected_by="human")),
2851
+ build=lambda: effect_for(
2852
+ ProcessChatsState.VAULT_GUARD_DECISION_REQUIRED,
2853
+ kind=WorkflowEffectKind.ASK_HUMAN,
2854
+ target="human.vault_guard_decision",
2855
+ payload=VaultGuardDecisionEffectPayload(changed_file_count=1),
2856
+ ),
2857
+ ),
2858
+ ProcessChatsEffectBuilderCase(
2859
+ origin_state=ProcessChatsState.PUBLISH_AWAITING_CONFIRMATION.value,
2860
+ outcomes=(
2861
+ HumanApprovedOutcome(
2862
+ approved_by="human",
2863
+ manifest_path="/tmp/manifest.json",
2864
+ dry_run_receipt_path="/tmp/dry-run.json",
2865
+ ),
2866
+ HumanCancelledOutcome(cancelled_by="human"),
2867
+ ),
2868
+ build=lambda: effect_for(
2869
+ ProcessChatsState.PUBLISH_AWAITING_CONFIRMATION,
2870
+ kind=WorkflowEffectKind.ASK_HUMAN,
2871
+ target="human.publish_decision",
2872
+ payload=HumanPublishDecisionEffectPayload(
2873
+ manifest_path="/tmp/manifest.json",
2874
+ dry_run_receipt_path="/tmp/dry-run.json",
2875
+ ),
2876
+ ),
2877
+ ),
2878
+ ProcessChatsEffectBuilderCase(
2879
+ origin_state=ProcessChatsState.PUBLISH_APPLY_REQUESTED.value,
2880
+ outcomes=(
2881
+ PublishBatchCompletedOutcome(
2882
+ receipt_id="receipt",
2883
+ published_count=1,
2884
+ link_trigger_context_path="/tmp/link-trigger.json",
2885
+ ),
2886
+ PublishDryRunReceiptRequiredOutcome(next_action="Recriar recibo.", error_context=err),
2887
+ PublishStaleReceiptOutcome(
2888
+ reason_code="stale_receipt",
2889
+ next_action="Atualizar recibo.",
2890
+ error_context=err,
2891
+ ),
2892
+ PublishDuplicateTargetOutcome(
2893
+ reason_code="duplicate_target",
2894
+ next_action="Resolver alvo duplicado.",
2895
+ error_context=err,
2896
+ ),
2897
+ PublishProvenanceGapOutcome(next_action="Corrigir proveniência.", error_context=err),
2898
+ PublishReceiptInvalidOutcome(next_action="Recriar recibo.", error_context=err),
2899
+ ExternalQuotaReportedOutcome(
2900
+ quota_kind="publish_batch",
2901
+ resume_action="Aguardar quota.",
2902
+ ),
2903
+ ),
2904
+ build=lambda: effect_for(
2905
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
2906
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2907
+ target="publish-batch",
2908
+ payload=PublishBatchEffectPayload(
2909
+ schema_version="medical-notes-workbench.process-chats.publish-batch-effect.v1",
2910
+ kind="publish_batch",
2911
+ manifest_path="/tmp/manifest.json",
2912
+ dry_run_receipt_path="/tmp/dry-run.json",
2913
+ ),
2914
+ mutates_resources=True,
2915
+ rollback_declared=True,
2916
+ ),
2917
+ ),
2918
+ ProcessChatsEffectBuilderCase(
2919
+ origin_state=ProcessChatsState.PUBLISH_PAUSED_FOR_QUOTA.value,
2920
+ outcomes=(
2921
+ ExternalReadyOutcome(
2922
+ restored_by="quota_window",
2923
+ manifest_path="/tmp/manifest.json",
2924
+ dry_run_receipt_path="/tmp/dry-run.json",
2925
+ ),
2926
+ ContractMissingNextActionOutcome(next_action_hint="Informar retomada de quota."),
2927
+ ),
2928
+ build=lambda: effect_for(
2929
+ ProcessChatsState.PUBLISH_PAUSED_FOR_QUOTA,
2930
+ kind=WorkflowEffectKind.WAIT_EXTERNAL,
2931
+ target="wait_external.publish_quota",
2932
+ payload=WaitExternalEffectPayload(
2933
+ wait_target="publish_quota",
2934
+ blocked_reason="wait_external.publish_quota",
2935
+ next_action="Aguardar quota.",
2936
+ resume_supported=True,
2937
+ ),
2938
+ ),
2939
+ ),
2940
+ ProcessChatsEffectBuilderCase(
2941
+ origin_state=ProcessChatsState.PUBLISH_DRY_RUN_RECEIPT_REQUIRED.value,
2942
+ outcomes=(
2943
+ PublishBlockerResolvedOutcome(
2944
+ resolved_by="receipt_recreated",
2945
+ manifest_path="/tmp/manifest.json",
2946
+ dry_run_receipt_path="/tmp/dry-run.json",
2947
+ ),
2948
+ ContractMissingErrorContextOutcome(error_context_hint="Informar erro de publish."),
2949
+ ),
2950
+ build=lambda: effect_for(
2951
+ ProcessChatsState.PUBLISH_DRY_RUN_RECEIPT_REQUIRED,
2952
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2953
+ target="workflow.resume_publish_blocker",
2954
+ payload=ResumePublishBlockerEffectPayload(reason_code="dry_run_receipt_required"),
2955
+ ),
2956
+ ),
2957
+ ProcessChatsEffectBuilderCase(
2958
+ origin_state=ProcessChatsState.PUBLISH_STALE_RECEIPT.value,
2959
+ outcomes=(
2960
+ PublishBlockerResolvedOutcome(
2961
+ resolved_by="receipt_recreated",
2962
+ manifest_path="/tmp/manifest.json",
2963
+ dry_run_receipt_path="/tmp/dry-run.json",
2964
+ ),
2965
+ ContractMissingErrorContextOutcome(error_context_hint="Informar erro de publish."),
2966
+ ),
2967
+ build=lambda: effect_for(
2968
+ ProcessChatsState.PUBLISH_STALE_RECEIPT,
2969
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2970
+ target="workflow.resume_publish_blocker",
2971
+ payload=ResumePublishBlockerEffectPayload(reason_code="stale_receipt"),
2972
+ ),
2973
+ ),
2974
+ ProcessChatsEffectBuilderCase(
2975
+ origin_state=ProcessChatsState.PUBLISH_DUPLICATE_TARGET.value,
2976
+ outcomes=(
2977
+ PublishBlockerResolvedOutcome(
2978
+ resolved_by="target_deduped",
2979
+ manifest_path="/tmp/manifest.json",
2980
+ dry_run_receipt_path="/tmp/dry-run.json",
2981
+ ),
2982
+ ContractMissingErrorContextOutcome(error_context_hint="Informar erro de publish."),
2983
+ ),
2984
+ build=lambda: effect_for(
2985
+ ProcessChatsState.PUBLISH_DUPLICATE_TARGET,
2986
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
2987
+ target="workflow.resume_publish_blocker",
2988
+ payload=ResumePublishBlockerEffectPayload(reason_code="duplicate_target"),
2989
+ ),
2990
+ ),
2991
+ ProcessChatsEffectBuilderCase(
2992
+ origin_state=ProcessChatsState.PUBLISH_PROVENANCE_GAP.value,
2993
+ outcomes=(
2994
+ PublishBlockerResolvedOutcome(
2995
+ resolved_by="provenance_completed",
2996
+ manifest_path="/tmp/manifest.json",
2997
+ dry_run_receipt_path="/tmp/dry-run.json",
2998
+ ),
2999
+ ContractMissingErrorContextOutcome(error_context_hint="Informar erro de publish."),
3000
+ ),
3001
+ build=lambda: effect_for(
3002
+ ProcessChatsState.PUBLISH_PROVENANCE_GAP,
3003
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
3004
+ target="workflow.resume_publish_blocker",
3005
+ payload=ResumePublishBlockerEffectPayload(reason_code="provenance_gap"),
3006
+ ),
3007
+ ),
3008
+ ProcessChatsEffectBuilderCase(
3009
+ origin_state=ProcessChatsState.PUBLISH_RECEIPT_INVALID.value,
3010
+ outcomes=(
3011
+ PublishBlockerResolvedOutcome(
3012
+ resolved_by="receipt_recreated",
3013
+ manifest_path="/tmp/manifest.json",
3014
+ dry_run_receipt_path="/tmp/dry-run.json",
3015
+ ),
3016
+ ContractMissingErrorContextOutcome(error_context_hint="Informar erro de publish."),
3017
+ ),
3018
+ build=lambda: effect_for(
3019
+ ProcessChatsState.PUBLISH_RECEIPT_INVALID,
3020
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
3021
+ target="workflow.resume_publish_blocker",
3022
+ payload=ResumePublishBlockerEffectPayload(reason_code="publish_receipt_invalid"),
3023
+ ),
3024
+ ),
3025
+ ProcessChatsEffectBuilderCase(
3026
+ origin_state=ProcessChatsState.LINK_RUN_REQUESTED.value,
3027
+ outcomes=(
3028
+ LinkCompletedOutcome(receipt_id="link", changed_files=[]),
3029
+ LinkBlockedOutcome(
3030
+ reason_code="graph_blockers",
3031
+ next_action="Resolver linker.",
3032
+ error_context=err,
3033
+ ),
3034
+ ),
3035
+ build=lambda: effect_for(
3036
+ ProcessChatsState.LINK_RUN_REQUESTED,
3037
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
3038
+ target="/mednotes:link",
3039
+ payload=LinkWorkflowRunEffectPayload(
3040
+ kind="link_run",
3041
+ diagnose=False,
3042
+ apply=True,
3043
+ trigger_context_path="/tmp/link-trigger.json",
3044
+ no_related_notes=False,
3045
+ ),
3046
+ mutates_resources=True,
3047
+ rollback_declared=True,
3048
+ ),
3049
+ ),
3050
+ ProcessChatsEffectBuilderCase(
3051
+ origin_state=ProcessChatsState.PUBLISH_APPLY_REQUESTED.value,
3052
+ outcomes=(
3053
+ RollbackCompletedOutcome(rollback_receipt_id="rollback"),
3054
+ ContractMissingErrorContextOutcome(error_context_hint="Informar erro de rollback."),
3055
+ ),
3056
+ build=lambda: effect_for(
3057
+ ProcessChatsState.PUBLISH_APPLY_REQUESTED,
3058
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
3059
+ target="vault.rollback",
3060
+ payload=RollbackEffectPayload(failed_origin_state=ProcessChatsState.PUBLISH_APPLY_REQUESTED.value),
3061
+ mutates_resources=True,
3062
+ rollback_declared=True,
3063
+ ),
3064
+ ),
3065
+ ProcessChatsEffectBuilderCase(
3066
+ origin_state=ProcessChatsState.LINK_RUN_REQUESTED.value,
3067
+ outcomes=(
3068
+ RollbackCompletedOutcome(rollback_receipt_id="rollback"),
3069
+ ContractMissingErrorContextOutcome(error_context_hint="Informar erro de rollback."),
3070
+ ),
3071
+ build=lambda: effect_for(
3072
+ ProcessChatsState.LINK_RUN_REQUESTED,
3073
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
3074
+ target="vault.rollback",
3075
+ payload=RollbackEffectPayload(failed_origin_state=ProcessChatsState.LINK_RUN_REQUESTED.value),
3076
+ mutates_resources=True,
3077
+ rollback_declared=True,
3078
+ ),
3079
+ ),
3080
+ ProcessChatsEffectBuilderCase(
3081
+ origin_state=ProcessChatsState.ROLLBACK_RECORDED.value,
3082
+ outcomes=(
3083
+ RollbackFailureRecordedOutcome(error_context=err),
3084
+ ContractMissingErrorContextOutcome(error_context_hint="Informar finalização de falha."),
3085
+ ),
3086
+ build=lambda: effect_for(
3087
+ ProcessChatsState.ROLLBACK_RECORDED,
3088
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
3089
+ target="workflow.failure_finalization",
3090
+ payload=FailureFinalizationEffectPayload(),
3091
+ ),
3092
+ ),
3093
+ )
3094
+
3095
+
3096
+ class _BuilderEvent(ProcessChatsEvent):
3097
+ name: Literal["builder_effect_case"] = "builder_effect_case"