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,2768 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Literal
5
+
6
+ from pydantic import ConfigDict, Field, StrictStr, field_validator, model_validator
7
+ from pydantic import ValidationError as PydanticValidationError
8
+ from pydantic.json_schema import SkipJsonSchema
9
+
10
+ from mednotes.domains.wiki.contracts.effect_payloads import (
11
+ LinkWorkflowRunEffectPayload,
12
+ RelatedNotesRecoveryStateEffectPayload,
13
+ )
14
+ from mednotes.domains.wiki.contracts.related_notes_runtime import RelatedNotesRecoveryState
15
+ from mednotes.domains.wiki.contracts.workflow_guardrails import error_context as build_error_context
16
+ from mednotes.domains.wiki.contracts.workflow_outcomes import (
17
+ DecisionEvidence,
18
+ HumanDecisionOption,
19
+ RejectedAutomation,
20
+ WorkflowDecision,
21
+ )
22
+ from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_machine import (
23
+ FixWikiBoundaryEvent,
24
+ FixWikiBoundaryEventAdapter,
25
+ FixWikiMachine,
26
+ FixWikiRuntimeObservation,
27
+ RuntimeObservedEvent,
28
+ )
29
+ from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_primary_objective import fix_wiki_primary_objective_summary
30
+ from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_states import (
31
+ FIX_WIKI_WORKFLOW,
32
+ FixWikiReason,
33
+ FixWikiState,
34
+ category_for_state,
35
+ reason_for_state,
36
+ )
37
+ from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_states import (
38
+ FixWikiDiagnosisLane as FixWikiDiagnosisLane,
39
+ )
40
+ from mednotes.kernel.agent_directive import (
41
+ AgentDirective,
42
+ agent_directive_from_progress_view_model,
43
+ assert_agent_directive_matches_progress,
44
+ )
45
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
46
+ from mednotes.kernel.effects import WorkflowEffect, WorkflowEffectKind
47
+ from mednotes.kernel.fsm_model import WorkflowModel
48
+ from mednotes.kernel.progress import (
49
+ WorkflowProgressCounts,
50
+ WorkflowProgressEvent,
51
+ WorkflowProgressEventType,
52
+ WorkflowProgressState,
53
+ WorkflowProgressStatus,
54
+ WorkflowProgressViewModel,
55
+ build_progress_view_model,
56
+ progress_state_from_view_model,
57
+ )
58
+ from mednotes.kernel.public_report import (
59
+ WorkflowPublicReport,
60
+ WorkflowReports,
61
+ assert_public_report_matches_progress,
62
+ public_progress_followup_line,
63
+ )
64
+ from mednotes.kernel.state_machine import (
65
+ WorkflowStateCategory,
66
+ WorkflowStateMachineSnapshot,
67
+ WorkflowTransition,
68
+ send_workflow_event,
69
+ )
70
+ from mednotes.kernel.workflow import (
71
+ HumanDecisionPacket,
72
+ ReceiptStatus,
73
+ VersionControlSafety,
74
+ WorkflowPhaseOutcome,
75
+ WorkflowReceiptPayload,
76
+ assert_diagnostic_context_evidence_only,
77
+ diagnostic_context_evidence_only,
78
+ )
79
+
80
+ FIX_WIKI_SCHEMA = "medical-notes-workbench.fix-wiki-fsm-result.v1"
81
+ FIX_WIKI_RECEIPT_SCHEMA = "medical-notes-workbench.fix-wiki-receipt.v3"
82
+ MEDNOTES_AGENT_DIRECTIVE_SCHEMA = "medical-notes-workbench.agent-directive.v1"
83
+
84
+ FIX_WIKI_ALLOWED_ROOT_KEYS = frozenset(
85
+ {
86
+ "schema",
87
+ "workflow",
88
+ "run_id",
89
+ "state_machine_snapshot",
90
+ "progress_view_model",
91
+ "decision",
92
+ "human_decision_packet",
93
+ "receipt",
94
+ "reports",
95
+ "agent_directive",
96
+ "artifacts",
97
+ "version_control_safety",
98
+ "diagnostic_context",
99
+ "error_context",
100
+ }
101
+ )
102
+ FIX_WIKI_FORBIDDEN_ROOT_KEYS = frozenset(
103
+ {
104
+ "status",
105
+ "phase",
106
+ "blocked_reason",
107
+ "next_action",
108
+ "next_command",
109
+ "execution_gate",
110
+ "resume_after_resolution",
111
+ "orchestration_plan",
112
+ "workflow_exit_code",
113
+ "public_report",
114
+ "requested_apply",
115
+ "effective_apply",
116
+ "blocker_resolution",
117
+ "final_validation",
118
+ }
119
+ )
120
+ FIX_WIKI_DIAGNOSTIC_PARALLEL_TRUTH_KEYS = frozenset(
121
+ {
122
+ "status",
123
+ "phase",
124
+ "blocked_reason",
125
+ "next_action",
126
+ "next_command",
127
+ "workflow_exit_code",
128
+ "requested_apply",
129
+ "effective_apply",
130
+ "required_inputs",
131
+ "human_decision_required",
132
+ "human_decision_kinds",
133
+ "primary_human_decision_kind",
134
+ "human_decision_packet",
135
+ "resume_action",
136
+ "action_directives",
137
+ "pending_effects",
138
+ }
139
+ )
140
+ FIX_WIKI_DIAGNOSTIC_OPERATIONAL_PLAN_KEYS = frozenset(
141
+ {
142
+ "status",
143
+ "phase",
144
+ "route",
145
+ "blocked_reason",
146
+ "next_action",
147
+ "next_command",
148
+ "agent_instruction",
149
+ "executable_now",
150
+ "current_work_item",
151
+ "current_batch_items",
152
+ "continuation_steps",
153
+ "parent_steps",
154
+ "execution_contract",
155
+ "runtime_execution",
156
+ "resume_action",
157
+ }
158
+ )
159
+ FIX_WIKI_RELATED_RECOVERY_COUNT_FIELDS = frozenset(
160
+ {
161
+ "fresh_record_count",
162
+ "partial_record_count",
163
+ "stale_record_count",
164
+ "record_count",
165
+ "total_note_count",
166
+ "remaining_count",
167
+ "embedded_count",
168
+ "reused_count",
169
+ "attempt_count",
170
+ "next_retry_after_seconds",
171
+ }
172
+ )
173
+
174
+ _PHASE_BY_STATE = {
175
+ "diagnosis.running": "diagnosis",
176
+ "environment.paths_missing": "environment",
177
+ "environment.wiki_dir_missing": "environment",
178
+ "environment.windows_path_or_venv_blocked": "environment",
179
+ "vault_guard.running": "vault_guard",
180
+ "vault_guard.decision_required": "vault_guard",
181
+ "subagent_plan_attestation.required": "subagent_plan_attestation",
182
+ "subagent_plan_attestation.invalid": "subagent_plan_attestation",
183
+ "agent_tool_contract_violation": "agent_tool_contract",
184
+ "deterministic_repairs.running": "deterministic_repairs",
185
+ "deterministic_repairs.failed": "deterministic_repairs",
186
+ "style_rewrite.specialist_requested": "style_rewrite",
187
+ "style_rewrite.capacity_wait": "style_rewrite",
188
+ "style_rewrite.review_required": "style_rewrite",
189
+ "style_rewrite.apply_running": "style_rewrite",
190
+ "taxonomy.decision_required": "taxonomy",
191
+ "taxonomy.apply_running": "taxonomy",
192
+ "vocabulary.curator_running": "vocabulary",
193
+ "vocabulary.semantic_ingestion_pending": "vocabulary",
194
+ "vocabulary.eval_running": "vocabulary",
195
+ "vocabulary.eval_needs_review": "vocabulary",
196
+ "vocabulary.apply_running": "vocabulary",
197
+ "vocabulary.sqlite_integrity_failed": "vocabulary",
198
+ "atomicity_split.running": "atomicity_split",
199
+ "atomicity_split.review_required": "atomicity_split",
200
+ "related_notes.export_running": "related_notes",
201
+ "related_notes.quota_wait": "related_notes_recovery",
202
+ "related_notes.obsidian_not_ready": "related_notes",
203
+ "related_notes.blocked": "related_notes",
204
+ "link.run_requested": "link",
205
+ "link.graph_blocked": "link",
206
+ "link.graph_review_required": "link",
207
+ "link.linker_blocked": "link",
208
+ "merge.running": "merge",
209
+ "merge.review_required": "merge",
210
+ "contract_gap.missing_next_action": "contract_gap",
211
+ "contract_gap.missing_error_context": "contract_gap",
212
+ "rollback.running": "rollback",
213
+ "rollback.performed": "rollback",
214
+ "rollback.failed": "rollback",
215
+ "final_validation.running": "final_validation",
216
+ "final_validation.failed": "final_validation",
217
+ "preview.ready": "preview",
218
+ "completed": "final_validation",
219
+ "completed_with_warnings": "final_validation",
220
+ "waiting_agent": "style_rewrite",
221
+ "waiting_external": "external_wait",
222
+ "waiting_for_external_quota": "related_notes_recovery",
223
+ "waiting_human": "human_decision",
224
+ "blocked": "blocked",
225
+ "failed": "failure",
226
+ }
227
+
228
+
229
+
230
+ class FixWikiRuntimeFacts(ContractModel):
231
+ """Typed adapter input produced by health/runtime before entering the FSM.
232
+
233
+ This model is deliberately not the public FSM facts. It is the current
234
+ runtime boundary that turns validated health facts into one canonical
235
+ `FixWikiMachine` event, so diagnostic-only fields cannot fabricate a
236
+ public state after this point.
237
+ """
238
+
239
+ run_id: str = Field(min_length=1)
240
+ requested_apply: bool = Field(strict=True)
241
+ effective_apply: bool = Field(strict=True)
242
+ total_changed_count: int = Field(default=0, ge=0, strict=True)
243
+ vault_changed_file_count: int = Field(default=0, ge=0, strict=True)
244
+ written_count: int = Field(default=0, ge=0, strict=True)
245
+ warning_count: int = Field(default=0, ge=0, strict=True)
246
+ requires_llm_rewrite_count: int = Field(default=0, ge=0, strict=True)
247
+ final_validation: JsonObject = Field(default_factory=dict)
248
+ version_control_safety: VersionControlSafety
249
+ artifacts: JsonObject = Field(default_factory=dict)
250
+ related_notes_blocked: bool = Field(default=False, strict=True)
251
+ related_notes_recovery_state: RelatedNotesRecoveryState = Field(default_factory=RelatedNotesRecoveryState)
252
+ vocabulary_semantic_ingestion_pending: bool = Field(default=False, strict=True)
253
+ vocabulary_eval_needs_review: bool = Field(default=False, strict=True)
254
+ atomicity_split_required: bool = Field(default=False, strict=True)
255
+ merge_review_required: bool = Field(default=False, strict=True)
256
+ human_decision_required: bool = Field(default=False, strict=True)
257
+ decision: WorkflowDecision | None = None
258
+ human_decision_packet: HumanDecisionPacket | None = None
259
+ changed_files: list[str] = Field(default_factory=list)
260
+ graph_error_count: int = Field(default=0, ge=0, strict=True)
261
+ graph_blocker_count: int = Field(default=0, ge=0, strict=True)
262
+ graph_review_required: bool = Field(default=False, strict=True)
263
+ linker_blocked: bool = Field(default=False, strict=True)
264
+ linker_apply_attempted: bool = Field(default=False, strict=True)
265
+ taxonomy_action_required: bool = Field(default=False, strict=True)
266
+ failed: bool = Field(default=False, strict=True)
267
+ failed_reason_code: str = ""
268
+ vault_guard_required: bool = Field(default=False, strict=True)
269
+ environment_windows_path_or_venv_blocked: bool = Field(default=False, strict=True)
270
+ next_action: str = ""
271
+ required_inputs: list[str] = Field(default_factory=list)
272
+ resume_action: str = ""
273
+ pending_effects: list[WorkflowEffect] = Field(default_factory=list)
274
+ external_wait_reason_code: str = ""
275
+ external_wait_resume_action: str = ""
276
+ external_wait_payload: JsonObject = Field(default_factory=dict)
277
+ diagnostic_context: JsonObject = Field(default_factory=dict)
278
+ error_context: JsonObject = Field(default_factory=dict)
279
+
280
+ @model_validator(mode="before")
281
+ @classmethod
282
+ def _reject_noncanonical_pending_effects_at_boundary(cls, value: object) -> object:
283
+ """Reject noncanonical effect shims before projection logic can inspect them.
284
+
285
+ `pending_effects` is an FSM-owned contract. The projector may validate a
286
+ canonical `WorkflowEffect`, but it must not fill `phase`,
287
+ `origin_state`, `workflow`, `run_id`, or `model_policy` on behalf of a
288
+ noncanonical producer because that would make success depend on adapter
289
+ glue rather than the StateChart transition that emitted the effect.
290
+ """
291
+
292
+ if not isinstance(value, dict):
293
+ return value
294
+ if "pending_effects" not in value:
295
+ return value
296
+ for raw_effect in value["pending_effects"]:
297
+ if isinstance(raw_effect, WorkflowEffect):
298
+ data = raw_effect.to_payload()
299
+ else:
300
+ data = dict(JsonObjectAdapter.validate_python(raw_effect))
301
+ if "phase" in data:
302
+ raise ValueError("pending effect must use origin_state, not phase")
303
+ kind = str(data["kind"]) if "kind" in data else ""
304
+ origin_state = str(data["origin_state"]).strip() if "origin_state" in data else ""
305
+ specialist_origin = FixWikiState.STYLE_REWRITE_SPECIALIST_REQUESTED.value
306
+ if not origin_state:
307
+ raise ValueError("pending effect origin_state is required")
308
+ if kind == WorkflowEffectKind.CALL_SPECIALIST_MODEL.value and origin_state != specialist_origin:
309
+ raise ValueError("pending effect origin_state must match style_rewrite.specialist_requested")
310
+ if (
311
+ "kind" in data
312
+ and data["kind"] == WorkflowEffectKind.CALL_SPECIALIST_MODEL.value
313
+ and ("model_policy" not in data or not data["model_policy"])
314
+ ):
315
+ raise ValueError("call_specialist_model pending effect requires model_policy")
316
+ return value
317
+
318
+ @field_validator("related_notes_recovery_state", mode="before")
319
+ @classmethod
320
+ def _coerce_related_notes_recovery_state(cls, value: object) -> RelatedNotesRecoveryState:
321
+ if isinstance(value, RelatedNotesRecoveryState):
322
+ return value
323
+ if isinstance(value, dict):
324
+ for field_name in FIX_WIKI_RELATED_RECOVERY_COUNT_FIELDS:
325
+ if field_name not in value:
326
+ continue
327
+ raw_value = value[field_name]
328
+ if type(raw_value) is not int:
329
+ raise ValueError(f"invalid numeric recovery_state value: {raw_value}")
330
+ return RelatedNotesRecoveryState.from_payload(value)
331
+
332
+ @model_validator(mode="after")
333
+ def _human_wait_requires_closed_packet(self) -> FixWikiRuntimeFacts:
334
+ if self.human_decision_required:
335
+ if self.decision is None:
336
+ raise ValueError("human_decision_required requires decision")
337
+ if self.human_decision_packet is None:
338
+ raise ValueError("human_decision_required requires human_decision_packet")
339
+ if self.decision.kind != "ask_human":
340
+ raise ValueError("human_decision_required requires ask_human decision")
341
+ if self.human_decision_packet.to_payload() != self.decision.to_human_decision_packet():
342
+ raise ValueError("human_decision_packet must match decision")
343
+ if self._would_complete_apply() and not _final_validation_has_evidence(self.final_validation):
344
+ raise ValueError("final_validation evidence required before completed fix-wiki apply")
345
+ _assert_final_validation_graph_matches_counters(
346
+ self.final_validation,
347
+ graph_error_count=self.graph_error_count,
348
+ graph_blocker_count=self.graph_blocker_count,
349
+ )
350
+ return self
351
+
352
+ def _would_complete_apply(self) -> bool:
353
+ """Return true only for the clean apply path that would enter a terminal success state."""
354
+
355
+ return self.effective_apply and not any(
356
+ (
357
+ self.failed,
358
+ self.human_decision_required,
359
+ self.related_notes_blocked,
360
+ self.vocabulary_semantic_ingestion_pending,
361
+ self.vocabulary_eval_needs_review,
362
+ self.atomicity_split_required,
363
+ self.merge_review_required,
364
+ self.graph_review_required,
365
+ self.graph_error_count,
366
+ self.graph_blocker_count,
367
+ self.linker_blocked,
368
+ self.taxonomy_action_required,
369
+ self.requires_llm_rewrite_count,
370
+ self.pending_effects,
371
+ self.external_wait_reason_code,
372
+ self.related_notes_recovery_state.status,
373
+ )
374
+ )
375
+
376
+
377
+ def _final_validation_has_evidence(payload: JsonObject) -> bool:
378
+ """Require concrete validation counters before apply can become success."""
379
+
380
+ graph = payload["graph"] if "graph" in payload else None
381
+ if not isinstance(graph, dict):
382
+ return False
383
+ for key in ("error_count", "blocker_count"):
384
+ if key in graph and type(graph[key]) is int:
385
+ return True
386
+ return False
387
+
388
+
389
+ def _assert_final_validation_graph_matches_counters(
390
+ payload: JsonObject,
391
+ *,
392
+ graph_error_count: int,
393
+ graph_blocker_count: int,
394
+ ) -> None:
395
+ """Keep final validation as evidence for the canonical graph counters."""
396
+
397
+ graph = payload["graph"] if "graph" in payload else None
398
+ if not isinstance(graph, dict):
399
+ return
400
+ expected = {
401
+ "error_count": graph_error_count,
402
+ "blocker_count": graph_blocker_count,
403
+ }
404
+ for key, canonical_value in expected.items():
405
+ if key not in graph:
406
+ continue
407
+ observed = graph[key]
408
+ if type(observed) is not int:
409
+ continue
410
+ if observed != canonical_value:
411
+ raise ValueError(f"final_validation graph {key} must match canonical graph counter")
412
+
413
+
414
+ class FixWikiFsmFacts(ContractModel):
415
+ """Canonical public projector input: one valid StateChart edge plus context."""
416
+
417
+ run_id: str = Field(min_length=1)
418
+ initial_state: FixWikiState
419
+ event: FixWikiBoundaryEvent
420
+ runtime: FixWikiRuntimeFacts
421
+ machine_effects: list[WorkflowEffect] = Field(default_factory=list)
422
+
423
+ @model_validator(mode="after")
424
+ def _event_must_match_fsm_entry(self) -> FixWikiFsmFacts:
425
+ if self.event.workflow != FIX_WIKI_WORKFLOW:
426
+ raise ValueError(f"fix-wiki event workflow must be {FIX_WIKI_WORKFLOW}")
427
+ if self.event.run_id != self.run_id:
428
+ raise ValueError("fix-wiki event run_id must match FixWikiFsmFacts.run_id")
429
+ if self.event.current_state != self.initial_state.value:
430
+ raise ValueError("fix-wiki event current_state must match initial_state")
431
+ if self.runtime.run_id != self.run_id:
432
+ raise ValueError("runtime run_id must match FixWikiFsmFacts.run_id")
433
+ return self
434
+
435
+ def with_runtime_updates(self, update: dict[str, object]) -> FixWikiFsmFacts:
436
+ """Rebuild the canonical event after adapter/runtime facts change."""
437
+
438
+ runtime = FixWikiRuntimeFacts.model_validate({**self.runtime.model_dump(mode="python"), **update})
439
+ return _fix_wiki_fsm_facts_from_runtime_model(runtime)
440
+
441
+ @property
442
+ def requested_apply(self) -> bool:
443
+ return self.runtime.requested_apply
444
+
445
+ @property
446
+ def effective_apply(self) -> bool:
447
+ return self.runtime.effective_apply
448
+
449
+ @property
450
+ def total_changed_count(self) -> int:
451
+ return self.runtime.total_changed_count
452
+
453
+ @property
454
+ def vault_changed_file_count(self) -> int:
455
+ return self.runtime.vault_changed_file_count
456
+
457
+ @property
458
+ def written_count(self) -> int:
459
+ return self.runtime.written_count
460
+
461
+ @property
462
+ def warning_count(self) -> int:
463
+ return self.runtime.warning_count
464
+
465
+ @property
466
+ def requires_llm_rewrite_count(self) -> int:
467
+ return self.runtime.requires_llm_rewrite_count
468
+
469
+ @property
470
+ def final_validation(self) -> JsonObject:
471
+ return self.runtime.final_validation
472
+
473
+ @property
474
+ def version_control_safety(self) -> VersionControlSafety:
475
+ return self.runtime.version_control_safety
476
+
477
+ @property
478
+ def artifacts(self) -> JsonObject:
479
+ return self.runtime.artifacts
480
+
481
+ @property
482
+ def related_notes_blocked(self) -> bool:
483
+ return self.runtime.related_notes_blocked
484
+
485
+ @property
486
+ def related_notes_recovery_state(self) -> RelatedNotesRecoveryState:
487
+ return self.runtime.related_notes_recovery_state
488
+
489
+ @property
490
+ def vocabulary_semantic_ingestion_pending(self) -> bool:
491
+ return self.runtime.vocabulary_semantic_ingestion_pending
492
+
493
+ @property
494
+ def vocabulary_eval_needs_review(self) -> bool:
495
+ return self.runtime.vocabulary_eval_needs_review
496
+
497
+ @property
498
+ def atomicity_split_required(self) -> bool:
499
+ return self.runtime.atomicity_split_required
500
+
501
+ @property
502
+ def merge_review_required(self) -> bool:
503
+ return self.runtime.merge_review_required
504
+
505
+ @property
506
+ def human_decision_required(self) -> bool:
507
+ return self.runtime.human_decision_required
508
+
509
+ @property
510
+ def decision(self) -> WorkflowDecision | None:
511
+ return self.runtime.decision
512
+
513
+ @property
514
+ def human_decision_packet(self) -> HumanDecisionPacket | None:
515
+ return self.runtime.human_decision_packet
516
+
517
+ @property
518
+ def changed_files(self) -> list[str]:
519
+ return self.runtime.changed_files
520
+
521
+ @property
522
+ def graph_error_count(self) -> int:
523
+ return self.runtime.graph_error_count
524
+
525
+ @property
526
+ def graph_blocker_count(self) -> int:
527
+ return self.runtime.graph_blocker_count
528
+
529
+ @property
530
+ def graph_review_required(self) -> bool:
531
+ return self.runtime.graph_review_required
532
+
533
+ @property
534
+ def linker_blocked(self) -> bool:
535
+ return self.runtime.linker_blocked
536
+
537
+ @property
538
+ def linker_apply_attempted(self) -> bool:
539
+ return self.runtime.linker_apply_attempted
540
+
541
+ @property
542
+ def taxonomy_action_required(self) -> bool:
543
+ return self.runtime.taxonomy_action_required
544
+
545
+ @property
546
+ def failed(self) -> bool:
547
+ return self.runtime.failed
548
+
549
+ @property
550
+ def failed_reason_code(self) -> str:
551
+ return self.runtime.failed_reason_code
552
+
553
+ @property
554
+ def next_action(self) -> str:
555
+ return self.runtime.next_action
556
+
557
+ @property
558
+ def required_inputs(self) -> list[str]:
559
+ return self.runtime.required_inputs
560
+
561
+ @property
562
+ def resume_action(self) -> str:
563
+ return self.runtime.resume_action
564
+
565
+ @property
566
+ def pending_effects(self) -> list[WorkflowEffect]:
567
+ return self.runtime.pending_effects
568
+
569
+ @property
570
+ def external_wait_reason_code(self) -> str:
571
+ return self.runtime.external_wait_reason_code
572
+
573
+ @property
574
+ def external_wait_resume_action(self) -> str:
575
+ return self.runtime.external_wait_resume_action
576
+
577
+ @property
578
+ def external_wait_payload(self) -> JsonObject:
579
+ return self.runtime.external_wait_payload
580
+
581
+ @property
582
+ def diagnostic_context(self) -> JsonObject:
583
+ return self.runtime.diagnostic_context
584
+
585
+ @property
586
+ def error_context(self) -> JsonObject:
587
+ return self.runtime.error_context
588
+
589
+
590
+ class _FixWikiStateView(ContractModel):
591
+ """Display/effect view derived from FixWikiMachine state and transition."""
592
+
593
+ reason: FixWikiReason
594
+ state: FixWikiState
595
+ category: WorkflowStateCategory
596
+ status: WorkflowProgressStatus
597
+ event_type: WorkflowProgressEventType
598
+ decision: WorkflowDecision | None = None
599
+ next_action: str = ""
600
+ resume_action: str = ""
601
+ resume_supported: bool = False
602
+ can_continue_now: bool = False
603
+ message: str
604
+ trigger: str
605
+
606
+
607
+ class _FixWikiPayloadProgressView(ContractModel):
608
+ status: StrictStr
609
+
610
+
611
+ class _FixWikiPayloadSnapshot(ContractModel):
612
+ current_category: StrictStr
613
+
614
+
615
+ class _FixWikiPayloadReceipt(ContractModel):
616
+ status: StrictStr
617
+
618
+
619
+ class _FixWikiExternalWaitEffectFields(ContractModel):
620
+ origin_state: StrictStr = ""
621
+
622
+
623
+ class _FixWikiExternalWaitProgressFields(ContractModel):
624
+ model_config = ConfigDict(extra="ignore")
625
+
626
+ status: StrictStr = ""
627
+ state: StrictStr = ""
628
+
629
+
630
+ class _FixWikiExternalWaitSnapshotFields(ContractModel):
631
+ model_config = ConfigDict(extra="ignore")
632
+
633
+ current_state: StrictStr = ""
634
+
635
+
636
+ class _FixWikiExternalWaitDiagnosticFields(ContractModel):
637
+ model_config = ConfigDict(extra="ignore")
638
+
639
+ related_notes_recovery_state: JsonObject = Field(default_factory=dict)
640
+
641
+
642
+ class _FixWikiExternalWaitPayloadFields(ContractModel):
643
+ """Typed lens for child FSM payloads returned by waiting-external effects."""
644
+
645
+ model_config = ConfigDict(extra="ignore")
646
+
647
+ progress_view_model: _FixWikiExternalWaitProgressFields = Field(default_factory=_FixWikiExternalWaitProgressFields)
648
+ state_machine_snapshot: _FixWikiExternalWaitSnapshotFields = Field(default_factory=_FixWikiExternalWaitSnapshotFields)
649
+ diagnostic_context: _FixWikiExternalWaitDiagnosticFields = Field(default_factory=_FixWikiExternalWaitDiagnosticFields)
650
+
651
+
652
+ class _FixWikiExistingErrorContextFields(ContractModel):
653
+ model_config = ConfigDict(extra="ignore")
654
+
655
+ blocked_reason: StrictStr = ""
656
+ root_cause: StrictStr = ""
657
+ next_action: StrictStr = ""
658
+
659
+
660
+ class _FixWikiArtifactPathFields(ContractModel):
661
+ """Typed artifact lens used when a FSM leaf needs a concrete recovery file."""
662
+
663
+ model_config = ConfigDict(extra="ignore")
664
+
665
+ atomicity_split_plan_path: StrictStr = ""
666
+
667
+
668
+ class _FixWikiErrorRequiredInputs(ContractModel):
669
+ """Typed lens for the only error-context field that can drive UX inputs."""
670
+
671
+ model_config = ConfigDict(extra="ignore")
672
+
673
+ required_inputs: list[StrictStr] = Field(default_factory=list)
674
+
675
+
676
+ class _FixWikiPendingEffectKind(ContractModel):
677
+ model_config = ConfigDict(extra="ignore")
678
+
679
+ kind: StrictStr = ""
680
+
681
+
682
+ class _FixWikiVocabularyBootstrapDiagnostic(ContractModel):
683
+ model_config = ConfigDict(extra="ignore")
684
+
685
+ trigger: StrictStr = ""
686
+
687
+
688
+ class _FixWikiPayloadFields(ContractModel):
689
+ workflow: Literal["/mednotes:fix-wiki"]
690
+ progress_view_model: _FixWikiPayloadProgressView
691
+ state_machine_snapshot: _FixWikiPayloadSnapshot
692
+ receipt: _FixWikiPayloadReceipt
693
+
694
+
695
+ class FixWikiFsmResult(ContractModel):
696
+ schema_id: Literal["medical-notes-workbench.fix-wiki-fsm-result.v1"] = Field(
697
+ default=FIX_WIKI_SCHEMA,
698
+ alias="schema",
699
+ )
700
+ workflow: Literal["/mednotes:fix-wiki"] = FIX_WIKI_WORKFLOW
701
+ run_id: str = Field(min_length=1)
702
+ progress_state: SkipJsonSchema[WorkflowProgressState]
703
+ progress_view_model: WorkflowProgressViewModel
704
+ state_machine_snapshot: WorkflowStateMachineSnapshot
705
+ decision: WorkflowDecision | None = None
706
+ human_decision_packet: HumanDecisionPacket | None = None
707
+ receipt: WorkflowReceiptPayload
708
+ reports: WorkflowReports
709
+ agent_directive: JsonObject
710
+ artifacts: JsonObject = Field(default_factory=dict)
711
+ version_control_safety: VersionControlSafety
712
+ diagnostic_context: JsonObject = Field(default_factory=dict)
713
+ error_context: JsonObject = Field(default_factory=dict)
714
+
715
+ @model_validator(mode="before")
716
+ @classmethod
717
+ def _hydrate_progress_state_from_public_payload(cls, value: object) -> object:
718
+ """Accept public payloads where progress_state is intentionally hidden."""
719
+
720
+ if not isinstance(value, dict) or "progress_state" in value or "progress_view_model" not in value:
721
+ return value
722
+ hydrated = dict(value)
723
+ progress_view = WorkflowProgressViewModel.model_validate(value["progress_view_model"])
724
+ hydrated["progress_state"] = progress_state_from_view_model(progress_view).to_payload()
725
+ return hydrated
726
+
727
+ @model_validator(mode="after")
728
+ def _progress_view_model_matches_state(self) -> FixWikiFsmResult:
729
+ expected = build_progress_view_model(self.progress_state).to_payload()
730
+ if self.progress_view_model.to_payload() != expected:
731
+ raise ValueError("progress_view_model must match progress_state")
732
+ return self
733
+
734
+ def to_payload(self) -> JsonObject:
735
+ payload: JsonObject = {
736
+ "schema": self.schema_id,
737
+ "workflow": self.workflow,
738
+ "run_id": self.run_id,
739
+ "state_machine_snapshot": self.state_machine_snapshot.to_payload(),
740
+ "progress_view_model": self.progress_view_model.to_payload(),
741
+ "decision": self.decision.to_payload() if self.decision is not None else None,
742
+ "human_decision_packet": self.human_decision_packet.to_payload()
743
+ if self.human_decision_packet is not None
744
+ else None,
745
+ "receipt": self.receipt.to_payload(),
746
+ "reports": self.reports.to_payload(),
747
+ "agent_directive": dict(self.agent_directive),
748
+ "artifacts": dict(self.artifacts),
749
+ "version_control_safety": self.version_control_safety.to_payload(),
750
+ "error_context": dict(self.error_context),
751
+ }
752
+ if self.diagnostic_context:
753
+ payload["diagnostic_context"] = dict(self.diagnostic_context)
754
+ payload = _payload_with_primary_objective_summary(payload)
755
+ payload = JsonObjectAdapter.validate_python(payload)
756
+ assert_fix_wiki_fsm_payload(payload)
757
+ return payload
758
+
759
+
760
+ def fix_wiki_fsm_facts_from_runtime(**runtime_fields: object) -> FixWikiFsmFacts:
761
+ """Normalize existing fix-wiki runtime facts into one StateChart event."""
762
+
763
+ runtime = FixWikiRuntimeFacts.model_validate(runtime_fields)
764
+ return _fix_wiki_fsm_facts_from_runtime_model(runtime)
765
+
766
+
767
+ def _fix_wiki_fsm_facts_from_runtime_model(runtime: FixWikiRuntimeFacts) -> FixWikiFsmFacts:
768
+ initial_state = FixWikiState.DIAGNOSIS_RUNNING
769
+ event = _runtime_observation_event_from_facts(runtime)
770
+ model = _fix_wiki_model_after_event(initial_state, event)
771
+ machine_effects = list(model.last_transition.effects) if model.last_transition is not None else []
772
+ return FixWikiFsmFacts(
773
+ run_id=runtime.run_id,
774
+ initial_state=initial_state,
775
+ event=event,
776
+ runtime=runtime,
777
+ machine_effects=machine_effects,
778
+ )
779
+
780
+
781
+ def build_fix_wiki_fsm_result(facts: FixWikiFsmFacts) -> FixWikiFsmResult:
782
+ model = _fix_wiki_model_after_event(facts.initial_state, facts.event)
783
+ state_view = _state_view_from_model(facts, model)
784
+ progress_state = _progress_state(facts, state_view)
785
+ progress_view_model = build_progress_view_model(progress_state)
786
+ snapshot = _snapshot_from_model(model, state_view, progress_state)
787
+ human_decision_packet = facts.human_decision_packet or _projection_human_decision_packet(state_view)
788
+ receipt = _receipt(facts, state_view, progress_state, snapshot, human_decision_packet=human_decision_packet)
789
+ reports_model = _reports(facts, state_view)
790
+ diagnostic_context = _diagnostic_context(
791
+ facts,
792
+ state_view,
793
+ )
794
+ agent_directive = _agent_directive(
795
+ facts,
796
+ state_view,
797
+ progress_view_model=progress_view_model,
798
+ user_visible_summary=_public_report_summary_text(reports_model.public_report),
799
+ )
800
+ diagnostic_context = _problem_diagnostic_context(diagnostic_context, state_view)
801
+ reports_model = _reports_with_primary_objective_summary(
802
+ reports_model,
803
+ run_id=facts.run_id,
804
+ progress_view_model=progress_view_model,
805
+ receipt=receipt,
806
+ diagnostic_context=diagnostic_context,
807
+ )
808
+
809
+ return FixWikiFsmResult(
810
+ run_id=facts.run_id,
811
+ progress_state=progress_state,
812
+ progress_view_model=progress_view_model,
813
+ state_machine_snapshot=snapshot,
814
+ decision=state_view.decision,
815
+ human_decision_packet=human_decision_packet,
816
+ receipt=receipt,
817
+ reports=reports_model,
818
+ agent_directive=agent_directive,
819
+ artifacts=facts.artifacts,
820
+ version_control_safety=facts.version_control_safety,
821
+ diagnostic_context=diagnostic_context,
822
+ error_context=_error_context(facts, state_view),
823
+ )
824
+
825
+
826
+ def _runtime_observation_event_from_facts(facts: FixWikiRuntimeFacts) -> RuntimeObservedEvent:
827
+ """Build the only runtime bridge event; the StateChart owns leaf selection."""
828
+
829
+ return RuntimeObservedEvent(
830
+ run_id=facts.run_id,
831
+ current_state=FixWikiState.DIAGNOSIS_RUNNING.value,
832
+ observation=FixWikiRuntimeObservation(
833
+ failed=facts.failed,
834
+ failed_reason_code=facts.failed_reason_code,
835
+ vault_guard_required=facts.vault_guard_required,
836
+ environment_windows_path_or_venv_blocked=facts.environment_windows_path_or_venv_blocked,
837
+ next_action=facts.next_action,
838
+ human_decision_required=facts.human_decision_required,
839
+ external_wait_reason_code=facts.external_wait_reason_code,
840
+ related_notes_waiting_external=_related_notes_waiting_external(facts),
841
+ vocabulary_semantic_ingestion_pending=facts.vocabulary_semantic_ingestion_pending,
842
+ vocabulary_eval_needs_review=facts.vocabulary_eval_needs_review,
843
+ atomicity_split_required=facts.atomicity_split_required,
844
+ merge_review_required=facts.merge_review_required,
845
+ graph_review_required=facts.graph_review_required,
846
+ graph_blocker_count=facts.graph_blocker_count,
847
+ graph_error_count=facts.graph_error_count,
848
+ related_notes_blocked=facts.related_notes_blocked,
849
+ linker_blocked=facts.linker_blocked,
850
+ taxonomy_action_required=facts.taxonomy_action_required,
851
+ specialist_model_waiting_agent=_specialist_model_waiting_agent(facts),
852
+ requires_llm_rewrite_count=facts.requires_llm_rewrite_count,
853
+ effective_apply=facts.effective_apply,
854
+ warning_count=facts.warning_count,
855
+ style_rewrite_effect=_style_rewrite_effect_input_from_runtime(facts),
856
+ link_subworkflow_required=_link_subworkflow_required(facts),
857
+ link_effect=_link_effect_input_from_runtime(facts),
858
+ related_notes_recovery_state=RelatedNotesRecoveryStateEffectPayload.model_validate(
859
+ facts.related_notes_recovery_state.to_payload()
860
+ ),
861
+ ),
862
+ audit_evidence=_runtime_audit_evidence(facts, "runtime_observed"),
863
+ )
864
+
865
+
866
+ def _runtime_audit_evidence(facts: FixWikiRuntimeFacts, reason: str) -> JsonObject:
867
+ return JsonObjectAdapter.validate_python(
868
+ {
869
+ "runtime_reason": reason,
870
+ "requested_apply": facts.requested_apply,
871
+ "effective_apply": facts.effective_apply,
872
+ "counts": {
873
+ "total_changed_count": facts.total_changed_count,
874
+ "vault_changed_file_count": facts.vault_changed_file_count,
875
+ "written_count": facts.written_count,
876
+ "warning_count": facts.warning_count,
877
+ "requires_llm_rewrite_count": facts.requires_llm_rewrite_count,
878
+ "graph_error_count": facts.graph_error_count,
879
+ "graph_blocker_count": facts.graph_blocker_count,
880
+ },
881
+ }
882
+ )
883
+
884
+
885
+ def _style_rewrite_effect_input_from_runtime(facts: FixWikiRuntimeFacts) -> WorkflowEffect | None:
886
+ """Pass typed batch evidence into the StateChart without making it public truth."""
887
+
888
+ if not _specialist_model_waiting_agent(facts):
889
+ return None
890
+ for effect in facts.pending_effects:
891
+ if effect.kind == WorkflowEffectKind.CALL_SPECIALIST_MODEL:
892
+ return effect
893
+ return None
894
+
895
+
896
+ def _link_subworkflow_required(facts: FixWikiRuntimeFacts) -> bool:
897
+ """Return true when a fix-wiki mutation must be followed by `/mednotes:link`."""
898
+
899
+ if facts.linker_apply_attempted or not facts.effective_apply:
900
+ return False
901
+ if _has_unresolved_work_before_link(facts):
902
+ return False
903
+ return bool(facts.changed_files or facts.vault_changed_file_count or facts.written_count)
904
+
905
+
906
+ def _has_unresolved_work_before_link(facts: FixWikiRuntimeFacts) -> bool:
907
+ """Keep link execution behind higher-priority blockers and human choices."""
908
+
909
+ return bool(
910
+ facts.failed
911
+ or facts.human_decision_required
912
+ or facts.decision is not None
913
+ or facts.human_decision_packet is not None
914
+ or facts.external_wait_reason_code
915
+ or _related_notes_waiting_external(facts)
916
+ or facts.vocabulary_semantic_ingestion_pending
917
+ or facts.vocabulary_eval_needs_review
918
+ or facts.atomicity_split_required
919
+ or facts.merge_review_required
920
+ or facts.graph_review_required
921
+ or facts.graph_blocker_count
922
+ or facts.graph_error_count
923
+ or facts.related_notes_blocked
924
+ or facts.linker_blocked
925
+ or facts.taxonomy_action_required
926
+ or _specialist_model_waiting_agent(facts)
927
+ or facts.requires_llm_rewrite_count
928
+ )
929
+
930
+
931
+ def _link_effect_input_from_runtime(facts: FixWikiRuntimeFacts) -> WorkflowEffect | None:
932
+ """Build the private link effect payload consumed by the StateChart action."""
933
+
934
+ if not _link_subworkflow_required(facts):
935
+ return None
936
+ link_artifacts = facts.artifacts
937
+ diagnosis_path = _artifact_text_field(link_artifacts, "linker_diagnosis_path")
938
+ receipt_path = _artifact_text_field(link_artifacts, "linker_receipt_path") or _link_receipt_path(diagnosis_path)
939
+ trigger_context_path = _artifact_text_field(link_artifacts, "link_trigger_context_path")
940
+ return WorkflowEffect(
941
+ workflow=FIX_WIKI_WORKFLOW,
942
+ run_id=facts.run_id,
943
+ effect_id="fix-wiki-link-run",
944
+ origin_state=FixWikiState.LINK_RUN_REQUESTED.value,
945
+ kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
946
+ target="/mednotes:link",
947
+ payload=LinkWorkflowRunEffectPayload(
948
+ kind="link_run",
949
+ diagnose=False,
950
+ apply=True,
951
+ diagnosis_path=diagnosis_path,
952
+ receipt_path=receipt_path,
953
+ trigger_context_path=trigger_context_path,
954
+ no_related_notes=False,
955
+ version_control_safety=facts.version_control_safety,
956
+ ).to_payload(),
957
+ mutates_resources=True,
958
+ rollback_declared=True,
959
+ requires_receipt=False,
960
+ )
961
+
962
+
963
+ def _artifact_text_field(artifacts: JsonObject, key: str) -> str:
964
+ if key not in artifacts:
965
+ return ""
966
+ value = artifacts[key]
967
+ return value if isinstance(value, str) else ""
968
+
969
+
970
+ def _link_receipt_path(diagnosis_path: str) -> str:
971
+ if not diagnosis_path.strip():
972
+ return ""
973
+ return str(Path(diagnosis_path).with_name("link-run-receipt.json"))
974
+
975
+
976
+ def _fix_wiki_model_after_event(initial_state: FixWikiState, event: FixWikiBoundaryEvent) -> WorkflowModel:
977
+ event = FixWikiBoundaryEventAdapter.validate_python(event.to_payload())
978
+ model = WorkflowModel.start(
979
+ workflow=FIX_WIKI_WORKFLOW,
980
+ run_id=event.run_id,
981
+ initial_state=initial_state.value,
982
+ )
983
+ send_workflow_event(
984
+ FixWikiMachine(model=model, state_field=WorkflowModel.STATECHART_STATE_FIELD),
985
+ event,
986
+ )
987
+ return model
988
+
989
+
990
+ def _state_view_from_model(facts: FixWikiFsmFacts, model: WorkflowModel) -> _FixWikiStateView:
991
+ """Derive public presentation from the canonical StateChart result."""
992
+
993
+ state = FixWikiState(model.state)
994
+ category = category_for_state(state)
995
+ status = WorkflowProgressStatus(category.value)
996
+ reason = _state_reason_from_model(model, state)
997
+ trigger = model.last_transition.trigger if model.last_transition is not None else reason.value
998
+ if category == WorkflowStateCategory.WAITING_HUMAN:
999
+ has_runtime_decision = facts.decision is not None
1000
+ decision = facts.decision or (model.last_transition.decision if model.last_transition is not None else None)
1001
+ if decision is None:
1002
+ raise ValueError("waiting_human state requires decision")
1003
+ if has_runtime_decision and decision.reason_code != reason.value:
1004
+ decision = _decision_with_leaf_reason(decision, reason_code=reason.value, phase=state.value)
1005
+ next_action = _default_next_action(facts, reason) if not has_runtime_decision else ""
1006
+ if not has_runtime_decision and next_action and next_action != decision.next_action:
1007
+ decision = _decision_with_recovery_action(decision, next_action=next_action)
1008
+ return _state_view(
1009
+ reason=reason,
1010
+ state=state,
1011
+ category=category,
1012
+ status=status,
1013
+ event_type=WorkflowProgressEventType.DECISION_EMITTED,
1014
+ decision=decision,
1015
+ next_action=decision.next_action,
1016
+ resume_action=decision.resume_action,
1017
+ resume_supported=bool(decision.resume_action),
1018
+ can_continue_now=False,
1019
+ message="Fix-wiki aguardando decisao humana antes de continuar.",
1020
+ trigger=decision.reason_code or trigger,
1021
+ )
1022
+ match reason:
1023
+ case FixWikiReason.COMPLETED:
1024
+ return _state_view(
1025
+ reason=reason,
1026
+ state=state,
1027
+ category=category,
1028
+ status=status,
1029
+ event_type=WorkflowProgressEventType.WORKFLOW_COMPLETED,
1030
+ message="Wiki corrigida e conferida.",
1031
+ trigger=trigger,
1032
+ )
1033
+ case FixWikiReason.COMPLETED_WITH_WARNINGS:
1034
+ return _state_view(
1035
+ reason=reason,
1036
+ state=state,
1037
+ category=category,
1038
+ status=status,
1039
+ event_type=WorkflowProgressEventType.WORKFLOW_COMPLETED,
1040
+ next_action=_default_next_action(facts, reason),
1041
+ message="Wiki corrigida com avisos pendentes.",
1042
+ trigger=trigger,
1043
+ )
1044
+ case FixWikiReason.PREVIEW_READY:
1045
+ return _state_view(
1046
+ reason=reason,
1047
+ state=state,
1048
+ category=category,
1049
+ status=status,
1050
+ event_type=WorkflowProgressEventType.VALIDATION_COMPLETED,
1051
+ message="Previa do fix-wiki pronta.",
1052
+ trigger=trigger,
1053
+ )
1054
+ case (
1055
+ FixWikiReason.ENVIRONMENT_PATHS_MISSING
1056
+ | FixWikiReason.ENVIRONMENT_WIKI_DIR_MISSING
1057
+ | FixWikiReason.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED
1058
+ ):
1059
+ next_action = _default_next_action(facts, reason)
1060
+ return _blocked_state_view(
1061
+ facts=facts,
1062
+ state=state,
1063
+ reason=reason,
1064
+ phase="environment",
1065
+ reason_code=reason.value,
1066
+ public_summary="O ambiente precisa ser preparado antes de continuar o fix-wiki.",
1067
+ developer_summary=(
1068
+ "Fix-wiki entered a recoverable environment leaf and emitted the typed "
1069
+ "/mednotes:setup recovery effect."
1070
+ ),
1071
+ message="Fix-wiki bloqueado por preparacao de ambiente.",
1072
+ trigger=trigger,
1073
+ next_action=next_action,
1074
+ resume_action=next_action,
1075
+ resume_supported=True,
1076
+ )
1077
+ case FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
1078
+ next_action = _default_next_action(facts, reason)
1079
+ return _state_view(
1080
+ reason=reason,
1081
+ state=state,
1082
+ category=category,
1083
+ status=status,
1084
+ event_type=WorkflowProgressEventType.EXTERNAL_WAIT_STARTED,
1085
+ next_action=next_action,
1086
+ resume_action=facts.resume_action or next_action,
1087
+ resume_supported=facts.related_notes_recovery_state.resume_supported,
1088
+ can_continue_now=False,
1089
+ message=_related_notes_external_wait_message(facts),
1090
+ trigger=trigger,
1091
+ )
1092
+ case FixWikiReason.WAITING_EXTERNAL:
1093
+ next_action = facts.next_action or facts.external_wait_resume_action
1094
+ return _state_view(
1095
+ reason=reason,
1096
+ state=state,
1097
+ category=category,
1098
+ status=status,
1099
+ event_type=WorkflowProgressEventType.EXTERNAL_WAIT_STARTED,
1100
+ next_action=next_action,
1101
+ resume_action=next_action,
1102
+ resume_supported=True,
1103
+ can_continue_now=False,
1104
+ message="Workflow aguardando condicao externa para retomar pela rota oficial.",
1105
+ trigger=trigger,
1106
+ )
1107
+ case FixWikiReason.STYLE_REWRITE_READY:
1108
+ next_action = _default_next_action(facts, reason)
1109
+ decision = WorkflowDecision(
1110
+ kind="auto_plan",
1111
+ phase="style_rewrite",
1112
+ reason_code=reason.value,
1113
+ public_summary="A reescrita especializada esta pronta para continuacao assistida.",
1114
+ developer_summary=(
1115
+ "Fix-wiki generated a typed call_specialist_model effect and expects the agent "
1116
+ "to continue with the official agent_directive effect route."
1117
+ ),
1118
+ evidence=[
1119
+ DecisionEvidence(
1120
+ summary="O workflow emitiu efeito tipado para modelo especialista.",
1121
+ technical_code="call_specialist_model",
1122
+ source="fix_wiki_fsm",
1123
+ )
1124
+ ],
1125
+ next_action=next_action,
1126
+ )
1127
+ return _state_view(
1128
+ reason=reason,
1129
+ state=state,
1130
+ category=category,
1131
+ status=status,
1132
+ event_type=WorkflowProgressEventType.STATE_ENTERED,
1133
+ decision=decision,
1134
+ next_action=next_action,
1135
+ resume_action=next_action,
1136
+ resume_supported=False,
1137
+ can_continue_now=True,
1138
+ message="Fix-wiki pronto para continuar com reescrita especializada.",
1139
+ trigger=trigger,
1140
+ )
1141
+ case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
1142
+ next_action = _default_next_action(facts, reason)
1143
+ decision = WorkflowDecision(
1144
+ kind="auto_plan",
1145
+ phase="vocabulary",
1146
+ reason_code=reason.value,
1147
+ public_summary="A curadoria semantica do vocabulario esta pronta para continuacao assistida.",
1148
+ developer_summary=(
1149
+ "Fix-wiki generated a typed run_subworkflow effect and expects the agent "
1150
+ "to continue vocabulary semantic ingestion through agent_directive.control.effects."
1151
+ ),
1152
+ evidence=[
1153
+ DecisionEvidence(
1154
+ summary="O workflow emitiu efeito tipado para curadoria semantica do vocabulario.",
1155
+ technical_code=WorkflowEffectKind.RUN_SUBWORKFLOW.value,
1156
+ source="fix_wiki_fsm",
1157
+ )
1158
+ ],
1159
+ next_action=next_action,
1160
+ )
1161
+ return _state_view(
1162
+ reason=reason,
1163
+ state=state,
1164
+ category=category,
1165
+ status=status,
1166
+ event_type=WorkflowProgressEventType.STATE_ENTERED,
1167
+ decision=decision,
1168
+ next_action=next_action,
1169
+ resume_action=next_action,
1170
+ resume_supported=False,
1171
+ can_continue_now=True,
1172
+ message="Fix-wiki pronto para continuar com curadoria semantica do vocabulario.",
1173
+ trigger=trigger,
1174
+ )
1175
+ case FixWikiReason.GRAPH_BLOCKED:
1176
+ return _blocked_state_view(
1177
+ facts=facts,
1178
+ state=state,
1179
+ reason=reason,
1180
+ phase="graph_validation",
1181
+ reason_code="graph_blockers",
1182
+ public_summary="A Wiki ainda tem bloqueios de grafo antes de concluir.",
1183
+ developer_summary="Graph validation found blockers after fix-wiki StateChart.",
1184
+ message="Fix-wiki bloqueado por problemas de grafo.",
1185
+ trigger=trigger,
1186
+ )
1187
+ case FixWikiReason.ATOMICITY_SPLIT_REQUIRED:
1188
+ return _blocked_state_view(
1189
+ facts=facts,
1190
+ state=state,
1191
+ reason=reason,
1192
+ phase="atomicity_split",
1193
+ reason_code="atomicity_split_required",
1194
+ public_summary="Ha split de atomicidade pendente antes de concluir.",
1195
+ developer_summary="Fix-wiki found pending atomicity split work that must run by the official route.",
1196
+ message="Fix-wiki bloqueado por split de atomicidade pendente.",
1197
+ trigger=trigger,
1198
+ )
1199
+ case FixWikiReason.RELATED_NOTES_BLOCKED:
1200
+ return _blocked_state_view(
1201
+ facts=facts,
1202
+ state=state,
1203
+ reason=reason,
1204
+ phase="related_notes",
1205
+ reason_code="related_notes_blocked",
1206
+ public_summary="As Notas Relacionadas ainda precisam ser atualizadas antes de concluir.",
1207
+ developer_summary="Fix-wiki could not close because Related Notes sync/export is still blocked.",
1208
+ message="Fix-wiki bloqueado por Notas Relacionadas pendentes.",
1209
+ trigger=trigger,
1210
+ )
1211
+ case FixWikiReason.LINKER_BLOCKED:
1212
+ return _blocked_state_view(
1213
+ facts=facts,
1214
+ state=state,
1215
+ reason=reason,
1216
+ phase="linker",
1217
+ reason_code="linker_blocked",
1218
+ public_summary="O pacote de links ainda esta bloqueado.",
1219
+ developer_summary="Fix-wiki could not complete because the linker package reported a blocker.",
1220
+ message="Fix-wiki bloqueado pelo pacote de links.",
1221
+ trigger=trigger,
1222
+ )
1223
+ case FixWikiReason.TAXONOMY_BLOCKED:
1224
+ return _blocked_state_view(
1225
+ facts=facts,
1226
+ state=state,
1227
+ reason=reason,
1228
+ phase="taxonomy",
1229
+ reason_code="taxonomy_blocked",
1230
+ public_summary="A taxonomia exige acao antes de concluir.",
1231
+ developer_summary="Fix-wiki found a taxonomy action/block before final health could close.",
1232
+ message="Fix-wiki bloqueado por acao de taxonomia.",
1233
+ trigger=trigger,
1234
+ )
1235
+ case FixWikiReason.VAULT_GUARD_REQUIRED:
1236
+ return _blocked_state_view(
1237
+ facts=facts,
1238
+ state=state,
1239
+ reason=reason,
1240
+ phase="vault_guard",
1241
+ reason_code="vault_guard_required",
1242
+ public_summary="A protecao do vault precisa ser aberta antes de alterar a Wiki.",
1243
+ developer_summary="Fix-wiki apply was blocked because the official vault guard was not active.",
1244
+ message="Fix-wiki bloqueado pela protecao do vault.",
1245
+ trigger=trigger,
1246
+ )
1247
+ case FixWikiReason.STYLE_REWRITE_REQUIRED:
1248
+ return _blocked_state_view(
1249
+ facts=facts,
1250
+ state=state,
1251
+ reason=reason,
1252
+ phase="style_rewrite",
1253
+ reason_code="style_rewrite_required",
1254
+ public_summary="Ha reescrita semantica pendente antes de concluir.",
1255
+ developer_summary="Fix-wiki found notes that require the official semantic rewrite route.",
1256
+ message="Fix-wiki bloqueado por reescrita semantica pendente.",
1257
+ trigger=trigger,
1258
+ )
1259
+ case FixWikiReason.FAILED:
1260
+ next_action = _default_next_action(facts, reason)
1261
+ reason_code = facts.failed_reason_code or (
1262
+ state.value if state != FixWikiState.FAILED else "fix_wiki_failed"
1263
+ )
1264
+ decision = WorkflowDecision(
1265
+ kind="failed",
1266
+ phase="failure",
1267
+ reason_code=reason_code,
1268
+ public_summary="O fix-wiki falhou antes de concluir a conferencia.",
1269
+ developer_summary="Fix-wiki emitted a failed StateChart state.",
1270
+ evidence=[
1271
+ DecisionEvidence(
1272
+ summary="A execucao informou falha operacional.",
1273
+ technical_code=reason_code,
1274
+ source="fix_wiki_fsm",
1275
+ )
1276
+ ],
1277
+ next_action=next_action,
1278
+ )
1279
+ return _state_view(
1280
+ reason=reason,
1281
+ state=state,
1282
+ category=category,
1283
+ status=status,
1284
+ event_type=WorkflowProgressEventType.WORKFLOW_FAILED,
1285
+ decision=decision,
1286
+ next_action=next_action,
1287
+ message="Fix-wiki falhou antes de concluir.",
1288
+ trigger=trigger,
1289
+ )
1290
+ case _:
1291
+ return _state_view(
1292
+ reason=reason,
1293
+ state=state,
1294
+ category=category,
1295
+ status=status,
1296
+ event_type=_default_event_type_for_status(status),
1297
+ decision=model.last_transition.decision if model.last_transition is not None else None,
1298
+ next_action=_default_next_action(facts, reason),
1299
+ resume_action=model.last_transition.resume_action if model.last_transition is not None else "",
1300
+ resume_supported=bool(model.last_transition and model.last_transition.resume_action),
1301
+ can_continue_now=status == WorkflowProgressStatus.WAITING_AGENT,
1302
+ message=_default_message_for_state(state),
1303
+ trigger=trigger,
1304
+ )
1305
+
1306
+
1307
+ def _state_reason_from_model(model: WorkflowModel, state: FixWikiState) -> FixWikiReason:
1308
+ """Derive public reason from the canonical leaf state, not transition metadata."""
1309
+
1310
+ return reason_for_state(state)
1311
+
1312
+
1313
+ def _default_event_type_for_status(status: WorkflowProgressStatus) -> WorkflowProgressEventType:
1314
+ match status:
1315
+ case WorkflowProgressStatus.RUNNING | WorkflowProgressStatus.WAITING_AGENT:
1316
+ return WorkflowProgressEventType.STATE_ENTERED
1317
+ case WorkflowProgressStatus.WAITING_EXTERNAL:
1318
+ return WorkflowProgressEventType.EXTERNAL_WAIT_STARTED
1319
+ case WorkflowProgressStatus.WAITING_HUMAN | WorkflowProgressStatus.BLOCKED:
1320
+ return WorkflowProgressEventType.DECISION_EMITTED
1321
+ case WorkflowProgressStatus.FAILED:
1322
+ return WorkflowProgressEventType.WORKFLOW_FAILED
1323
+ case WorkflowProgressStatus.COMPLETED | WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
1324
+ return WorkflowProgressEventType.WORKFLOW_COMPLETED
1325
+ raise AssertionError(f"unsupported workflow progress status: {status}")
1326
+
1327
+
1328
+ def _default_message_for_state(state: FixWikiState) -> str:
1329
+ phase = _PHASE_BY_STATE[state.value]
1330
+ return f"Fix-wiki em {phase}."
1331
+
1332
+
1333
+ def _state_view(
1334
+ *,
1335
+ reason: FixWikiReason,
1336
+ state: FixWikiState,
1337
+ category: WorkflowStateCategory,
1338
+ status: WorkflowProgressStatus,
1339
+ event_type: WorkflowProgressEventType,
1340
+ message: str,
1341
+ trigger: str,
1342
+ decision: WorkflowDecision | None = None,
1343
+ next_action: str = "",
1344
+ resume_action: str = "",
1345
+ resume_supported: bool = False,
1346
+ can_continue_now: bool = False,
1347
+ ) -> _FixWikiStateView:
1348
+ return _FixWikiStateView(
1349
+ reason=reason,
1350
+ state=state,
1351
+ category=category,
1352
+ status=status,
1353
+ event_type=event_type,
1354
+ decision=decision,
1355
+ next_action=next_action,
1356
+ resume_action=resume_action,
1357
+ resume_supported=resume_supported,
1358
+ can_continue_now=can_continue_now,
1359
+ message=message,
1360
+ trigger=trigger,
1361
+ )
1362
+
1363
+
1364
+ def _blocked_state_view(
1365
+ *,
1366
+ facts: FixWikiFsmFacts,
1367
+ state: FixWikiState,
1368
+ reason: FixWikiReason,
1369
+ phase: str,
1370
+ reason_code: str,
1371
+ public_summary: str,
1372
+ developer_summary: str,
1373
+ message: str,
1374
+ trigger: str,
1375
+ next_action: str | None = None,
1376
+ resume_action: str = "",
1377
+ resume_supported: bool = False,
1378
+ ) -> _FixWikiStateView:
1379
+ next_action = next_action if next_action is not None else _default_next_action(facts, reason)
1380
+ effective_resume_action = resume_action or next_action
1381
+ category = category_for_state(state)
1382
+ status = WorkflowProgressStatus(category.value)
1383
+ if category == WorkflowStateCategory.WAITING_HUMAN:
1384
+ decision = _ask_human_decision(
1385
+ phase=phase,
1386
+ reason_code=reason_code,
1387
+ public_summary=public_summary,
1388
+ developer_summary=developer_summary,
1389
+ next_action=next_action,
1390
+ required_inputs=_required_inputs_for_block(facts, reason),
1391
+ )
1392
+ else:
1393
+ decision = _hard_block_decision(
1394
+ phase=phase,
1395
+ reason_code=reason_code,
1396
+ public_summary=public_summary,
1397
+ developer_summary=developer_summary,
1398
+ next_action=next_action,
1399
+ required_inputs=_required_inputs_for_block(facts, reason),
1400
+ )
1401
+ return _state_view(
1402
+ reason=reason,
1403
+ state=state,
1404
+ category=category,
1405
+ status=status,
1406
+ event_type=WorkflowProgressEventType.DECISION_EMITTED,
1407
+ decision=decision,
1408
+ next_action=next_action,
1409
+ resume_action=effective_resume_action,
1410
+ resume_supported=resume_supported,
1411
+ message=message,
1412
+ trigger=trigger,
1413
+ )
1414
+
1415
+
1416
+ def _external_wait_state(facts: FixWikiFsmFacts) -> FixWikiState:
1417
+ """Select the concrete waiting leaf from the canonical external-wait envelope."""
1418
+
1419
+ return _external_wait_state_from_payload(facts.external_wait_payload)
1420
+
1421
+
1422
+ def _external_wait_state_from_payload(payload: JsonObject) -> FixWikiState:
1423
+ """Map a waiting-external effect envelope to the concrete fix-wiki leaf."""
1424
+
1425
+ child_payload = _optional_json_object_field(payload, "payload") or payload
1426
+ child = _FixWikiExternalWaitPayloadFields.model_validate(child_payload)
1427
+ child_state = child.state_machine_snapshot.current_state or child.progress_view_model.state
1428
+ if child.progress_view_model.status == "waiting_external" and child_state == "waiting_external_related_notes_quota":
1429
+ return FixWikiState.RELATED_NOTES_QUOTA_WAIT
1430
+ recovery = child.diagnostic_context.related_notes_recovery_state
1431
+ if _related_notes_recovery_waiting_external(recovery):
1432
+ return FixWikiState.RELATED_NOTES_QUOTA_WAIT
1433
+ effect = _FixWikiExternalWaitEffectFields.model_validate(
1434
+ _optional_json_object_subset(payload, "effect", ("origin_state",))
1435
+ )
1436
+ if effect.origin_state:
1437
+ try:
1438
+ state = FixWikiState(effect.origin_state)
1439
+ except ValueError:
1440
+ state = FixWikiState.STYLE_REWRITE_CAPACITY_WAIT
1441
+ if category_for_state(state) == WorkflowStateCategory.WAITING_EXTERNAL:
1442
+ return state
1443
+ return FixWikiState.STYLE_REWRITE_CAPACITY_WAIT
1444
+
1445
+
1446
+ def _related_notes_recovery_waiting_external(recovery: JsonObject) -> bool:
1447
+ typed = RelatedNotesRecoveryState.from_payload(recovery)
1448
+ return (
1449
+ typed.status == "waiting_for_retry"
1450
+ and typed.resume_supported
1451
+ and typed.blocked_reason
1452
+ in {
1453
+ "related_notes_headless_quota_exhausted",
1454
+ "related_notes_headless_time_budget_exhausted",
1455
+ }
1456
+ )
1457
+
1458
+
1459
+ def _hard_block_decision(
1460
+ *,
1461
+ phase: str,
1462
+ reason_code: str,
1463
+ public_summary: str,
1464
+ developer_summary: str,
1465
+ next_action: str,
1466
+ required_inputs: list[str] | None = None,
1467
+ ) -> WorkflowDecision:
1468
+ return WorkflowDecision(
1469
+ kind="hard_block",
1470
+ phase=phase,
1471
+ reason_code=reason_code,
1472
+ public_summary=public_summary,
1473
+ developer_summary=developer_summary,
1474
+ evidence=[
1475
+ DecisionEvidence(
1476
+ summary="O FSM classificou o resultado como bloqueado.",
1477
+ technical_code=reason_code,
1478
+ source="fix_wiki_fsm",
1479
+ )
1480
+ ],
1481
+ next_action=next_action,
1482
+ required_inputs=list(required_inputs or []),
1483
+ )
1484
+
1485
+
1486
+ def _ask_human_decision(
1487
+ *,
1488
+ phase: str,
1489
+ reason_code: str,
1490
+ public_summary: str,
1491
+ developer_summary: str,
1492
+ next_action: str,
1493
+ required_inputs: list[str] | None = None,
1494
+ ) -> WorkflowDecision:
1495
+ return WorkflowDecision(
1496
+ kind="ask_human",
1497
+ phase=phase,
1498
+ reason_code=reason_code,
1499
+ public_summary=public_summary,
1500
+ developer_summary=developer_summary,
1501
+ evidence=[
1502
+ DecisionEvidence(
1503
+ summary="O FSM entrou em uma folha que exige escolha ou revisão humana.",
1504
+ technical_code=reason_code,
1505
+ source="fix_wiki_fsm",
1506
+ )
1507
+ ],
1508
+ next_action=next_action,
1509
+ required_inputs=list(required_inputs or []),
1510
+ resume_action=next_action,
1511
+ recommended_option_id="continue_official_route",
1512
+ human_decision_kind=reason_code,
1513
+ options=[
1514
+ HumanDecisionOption(
1515
+ id="continue_official_route",
1516
+ label="Continuar",
1517
+ description=next_action,
1518
+ ),
1519
+ HumanDecisionOption(
1520
+ id="stop_here",
1521
+ label="Parar",
1522
+ description="Encerrar este workflow sem aplicar a próxima etapa agora.",
1523
+ ),
1524
+ ],
1525
+ rejected_automations=[
1526
+ RejectedAutomation(
1527
+ kind="auto_fix",
1528
+ reason_code=reason_code,
1529
+ reason="A próxima ação depende de revisão ou escolha humana antes de mutar a Wiki.",
1530
+ ),
1531
+ RejectedAutomation(
1532
+ kind="auto_defer",
1533
+ reason_code=reason_code,
1534
+ reason="Adiar manteria a folha waiting_human aberta sem decisão registrada.",
1535
+ ),
1536
+ RejectedAutomation(
1537
+ kind="auto_plan",
1538
+ reason_code=reason_code,
1539
+ reason="O plano precisa da decisão humana para escolher a próxima rota segura.",
1540
+ ),
1541
+ ],
1542
+ )
1543
+
1544
+
1545
+ def _projection_human_decision_packet(projection: _FixWikiStateView) -> JsonObject | None:
1546
+ if projection.status != WorkflowProgressStatus.WAITING_HUMAN:
1547
+ return None
1548
+ if projection.decision is None:
1549
+ return None
1550
+ return projection.decision.to_human_decision_packet()
1551
+
1552
+
1553
+ def _decision_with_recovery_action(decision: WorkflowDecision, *, next_action: str) -> WorkflowDecision:
1554
+ """Revalidate a StateChart decision after adding artifact-specific recovery text."""
1555
+
1556
+ updated = decision.model_copy(update={"next_action": next_action, "resume_action": next_action})
1557
+ return WorkflowDecision.model_validate(updated.to_payload())
1558
+
1559
+
1560
+ def _decision_with_leaf_reason(decision: WorkflowDecision, *, reason_code: str, phase: str) -> WorkflowDecision:
1561
+ """Make the public decision reason follow the reached leaf, not runtime metadata."""
1562
+
1563
+ human_kind = decision.human_decision_kind or decision.reason_code
1564
+ updated = decision.model_copy(update={"reason_code": reason_code, "phase": phase, "human_decision_kind": human_kind})
1565
+ return WorkflowDecision.model_validate(updated.to_payload())
1566
+
1567
+
1568
+ def _required_inputs_for_block(facts: FixWikiFsmFacts, reason: FixWikiReason) -> list[str]:
1569
+ match reason:
1570
+ case (
1571
+ FixWikiReason.GRAPH_BLOCKED
1572
+ | FixWikiReason.LINKER_BLOCKED
1573
+ | FixWikiReason.RELATED_NOTES_BLOCKED
1574
+ | FixWikiReason.TAXONOMY_BLOCKED
1575
+ | FixWikiReason.VAULT_GUARD_REQUIRED
1576
+ ):
1577
+ return _clean_required_inputs(facts.required_inputs)
1578
+ case FixWikiReason.FAILED:
1579
+ error_fields = _FixWikiErrorRequiredInputs.model_validate(facts.error_context)
1580
+ return _clean_required_inputs(error_fields.required_inputs)
1581
+ case _:
1582
+ return []
1583
+
1584
+
1585
+ def _clean_required_inputs(value: object) -> list[str]:
1586
+ if value is None:
1587
+ return []
1588
+ raw_items = JsonObjectAdapter.validate_python({"items": value})["items"]
1589
+ if not isinstance(raw_items, list):
1590
+ raise ValueError("required_inputs must be a list of strings")
1591
+ cleaned: list[str] = []
1592
+ for item in raw_items:
1593
+ if not isinstance(item, str):
1594
+ raise ValueError("required_inputs must contain only strings")
1595
+ text = item.strip()
1596
+ if text:
1597
+ cleaned.append(text)
1598
+ return cleaned
1599
+
1600
+
1601
+ def _default_next_action(facts: FixWikiFsmFacts, reason: FixWikiReason) -> str:
1602
+ candidate = facts.next_action.strip()
1603
+ match reason:
1604
+ case FixWikiReason.ENVIRONMENT_PATHS_MISSING | FixWikiReason.ENVIRONMENT_WIKI_DIR_MISSING:
1605
+ return candidate or "setup:set-paths"
1606
+ case FixWikiReason.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED:
1607
+ return candidate or "setup:bootstrap-python"
1608
+ case FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
1609
+ return candidate or "Aguardar a cota externa e retomar pela rota oficial."
1610
+ case FixWikiReason.WAITING_EXTERNAL:
1611
+ return facts.external_wait_resume_action or "Aguardar a condicao externa e retomar pela rota oficial."
1612
+ case FixWikiReason.WAITING_HUMAN:
1613
+ return candidate or "Responder a decisao solicitada para continuar."
1614
+ case FixWikiReason.STYLE_REWRITE_REVIEW_REQUIRED:
1615
+ return "Executar a reescrita semantica oficial antes de concluir."
1616
+ case FixWikiReason.TAXONOMY_DECISION_REQUIRED:
1617
+ return "Resolver a acao de taxonomia pela rota oficial antes de concluir."
1618
+ case FixWikiReason.VOCABULARY_EVAL_NEEDS_REVIEW:
1619
+ return "Revisar a avaliacao do vocabulario e retomar pela rota oficial."
1620
+ case FixWikiReason.GRAPH_REVIEW_REQUIRED:
1621
+ return candidate or "Revisar os bloqueios de grafo e retomar pela rota oficial."
1622
+ case FixWikiReason.ATOMICITY_SPLIT_REVIEW_REQUIRED:
1623
+ return _atomicity_split_recovery_action(facts)
1624
+ case FixWikiReason.MERGE_REVIEW_REQUIRED:
1625
+ return "Revisar o merge de notas e retomar pela rota oficial."
1626
+ case FixWikiReason.SUBAGENT_PLAN_ATTESTATION_REQUIRED | FixWikiReason.SUBAGENT_PLAN_ATTESTATION_INVALID:
1627
+ return "Reemitir o plano de subagente pela rota oficial com atestacao valida."
1628
+ case FixWikiReason.STYLE_REWRITE_READY:
1629
+ return "Continuar pela reescrita especializada, aplicar a versao validada e repetir a conferencia da Wiki."
1630
+ case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
1631
+ return candidate or "Executar a curadoria semantica do vocabulario e repetir a conferencia da Wiki."
1632
+ case FixWikiReason.GRAPH_BLOCKED:
1633
+ return candidate or "Executar /mednotes:link para reparar WikiLinks e grafo pela rota oficial."
1634
+ case FixWikiReason.LINK_RUN_REQUESTED:
1635
+ return candidate or "Executar /mednotes:link pela rota oficial antes de concluir o fix-wiki."
1636
+ case FixWikiReason.ATOMICITY_SPLIT_REQUIRED:
1637
+ return candidate or _atomicity_split_recovery_action(facts)
1638
+ case FixWikiReason.RELATED_NOTES_BLOCKED:
1639
+ return candidate or (
1640
+ "Conferir o export do Related Notes: o workflow não conseguiu provar que o export cobre esta Wiki; "
1641
+ "atualize as Notas Relacionadas e repita a conferência."
1642
+ )
1643
+ case FixWikiReason.LINKER_BLOCKED:
1644
+ return candidate or "Retomar o pacote de links pela rota oficial antes de concluir."
1645
+ case FixWikiReason.TAXONOMY_BLOCKED:
1646
+ return candidate or "Resolver a acao de taxonomia pela rota oficial antes de concluir."
1647
+ case FixWikiReason.VAULT_GUARD_REQUIRED:
1648
+ return candidate or "Abrir a protecao do vault pela rota oficial e repetir o apply."
1649
+ case FixWikiReason.STYLE_REWRITE_REQUIRED:
1650
+ return "Executar a reescrita semantica oficial antes de concluir."
1651
+ case FixWikiReason.FAILED:
1652
+ return candidate or "Revisar o erro e retomar pela rota oficial indicada."
1653
+ case FixWikiReason.COMPLETED_WITH_WARNINGS:
1654
+ return candidate or "Revisar os avisos pendentes quando possivel."
1655
+ case FixWikiReason.PREVIEW_READY | FixWikiReason.COMPLETED:
1656
+ return ""
1657
+
1658
+
1659
+ def _atomicity_split_recovery_action(facts: FixWikiFsmFacts) -> str:
1660
+ """Render the official recovery route from the FSM artifact snapshot."""
1661
+
1662
+ plan_path = _FixWikiArtifactPathFields.model_validate(facts.artifacts).atomicity_split_plan_path.strip()
1663
+ command = "apply-atomicity-split"
1664
+ if plan_path:
1665
+ return (
1666
+ f"Revisar {plan_path}, executar {command} para os bundles aprovados "
1667
+ "e repetir /mednotes:fix-wiki."
1668
+ )
1669
+ return f"Executar {command} para os bundles aprovados e repetir /mednotes:fix-wiki."
1670
+
1671
+
1672
+ def _error_context(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> JsonObject:
1673
+ existing = JsonObjectAdapter.validate_python(facts.error_context or {})
1674
+ if projection.status not in {
1675
+ WorkflowProgressStatus.BLOCKED,
1676
+ WorkflowProgressStatus.FAILED,
1677
+ WorkflowProgressStatus.WAITING_HUMAN,
1678
+ }:
1679
+ return existing
1680
+ expected_reason = projection.decision.reason_code if projection.decision is not None else projection.reason.value
1681
+ if _existing_error_context_matches(existing, expected_reason, projection.next_action):
1682
+ return existing
1683
+ return build_error_context(
1684
+ phase=projection.decision.phase if projection.decision is not None else _progress_phase(facts, projection),
1685
+ blocked_reason=expected_reason,
1686
+ root_cause=expected_reason,
1687
+ affected_artifact=_affected_artifact_for_reason(projection.reason),
1688
+ error_summary=projection.message,
1689
+ suggested_fix=projection.next_action,
1690
+ next_action=projection.next_action,
1691
+ retry_scope=_retry_scope_for_reason(projection.reason),
1692
+ human_decision_required=projection.status == WorkflowProgressStatus.WAITING_HUMAN,
1693
+ )
1694
+
1695
+
1696
+ def _existing_error_context_matches(context: JsonObject, expected_reason: str, expected_next_action: str) -> bool:
1697
+ if not context:
1698
+ return False
1699
+ fields = _FixWikiExistingErrorContextFields.model_validate(context)
1700
+ return expected_reason in {fields.blocked_reason, fields.root_cause} and fields.next_action == expected_next_action
1701
+
1702
+
1703
+ def _affected_artifact_for_reason(reason: FixWikiReason) -> str:
1704
+ match reason:
1705
+ case FixWikiReason.ENVIRONMENT_PATHS_MISSING | FixWikiReason.ENVIRONMENT_WIKI_DIR_MISSING:
1706
+ return "workbench_paths_config"
1707
+ case FixWikiReason.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED:
1708
+ return "python_environment"
1709
+ case (
1710
+ FixWikiReason.STYLE_REWRITE_REQUIRED
1711
+ | FixWikiReason.STYLE_REWRITE_READY
1712
+ | FixWikiReason.STYLE_REWRITE_REVIEW_REQUIRED
1713
+ ):
1714
+ return "style_rewrite_plan"
1715
+ case FixWikiReason.SUBAGENT_PLAN_ATTESTATION_REQUIRED | FixWikiReason.SUBAGENT_PLAN_ATTESTATION_INVALID:
1716
+ return "subagent_plan"
1717
+ case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
1718
+ return "vocabulary_semantic_repair"
1719
+ case FixWikiReason.VOCABULARY_EVAL_NEEDS_REVIEW:
1720
+ return "vocabulary_eval_report"
1721
+ case FixWikiReason.RELATED_NOTES_BLOCKED | FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
1722
+ return "related_notes_export"
1723
+ case FixWikiReason.LINKER_BLOCKED | FixWikiReason.GRAPH_BLOCKED | FixWikiReason.GRAPH_REVIEW_REQUIRED:
1724
+ return "linker_diagnosis"
1725
+ case FixWikiReason.TAXONOMY_BLOCKED | FixWikiReason.TAXONOMY_DECISION_REQUIRED:
1726
+ return "taxonomy_plan"
1727
+ case FixWikiReason.ATOMICITY_SPLIT_REQUIRED | FixWikiReason.ATOMICITY_SPLIT_REVIEW_REQUIRED:
1728
+ return "atomicity_split_plan"
1729
+ case FixWikiReason.MERGE_REVIEW_REQUIRED:
1730
+ return "note_merge_plan"
1731
+ case _:
1732
+ return "fix_wiki_plan"
1733
+
1734
+
1735
+ def _retry_scope_for_reason(reason: FixWikiReason) -> str:
1736
+ match reason:
1737
+ case (
1738
+ FixWikiReason.ENVIRONMENT_PATHS_MISSING
1739
+ | FixWikiReason.ENVIRONMENT_WIKI_DIR_MISSING
1740
+ | FixWikiReason.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED
1741
+ ):
1742
+ return "setup_then_rerun_fix_wiki"
1743
+ case (
1744
+ FixWikiReason.STYLE_REWRITE_REQUIRED
1745
+ | FixWikiReason.STYLE_REWRITE_READY
1746
+ | FixWikiReason.STYLE_REWRITE_REVIEW_REQUIRED
1747
+ ):
1748
+ return "style_rewrite_official_route"
1749
+ case FixWikiReason.SUBAGENT_PLAN_ATTESTATION_REQUIRED | FixWikiReason.SUBAGENT_PLAN_ATTESTATION_INVALID:
1750
+ return "subagent_plan_attestation"
1751
+ case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
1752
+ return "vocabulary_semantic_ingestion_then_rerun_fix_wiki"
1753
+ case FixWikiReason.VOCABULARY_EVAL_NEEDS_REVIEW:
1754
+ return "vocabulary_eval_review"
1755
+ case FixWikiReason.GRAPH_REVIEW_REQUIRED:
1756
+ return "link_review_then_rerun_fix_wiki"
1757
+ case FixWikiReason.ATOMICITY_SPLIT_REQUIRED | FixWikiReason.ATOMICITY_SPLIT_REVIEW_REQUIRED:
1758
+ return "atomicity_split_then_rerun_fix_wiki"
1759
+ case FixWikiReason.MERGE_REVIEW_REQUIRED:
1760
+ return "note_merge_review"
1761
+ case FixWikiReason.RELATED_NOTES_BLOCKED | FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
1762
+ return "related_notes_then_rerun_fix_wiki"
1763
+ case FixWikiReason.LINKER_BLOCKED | FixWikiReason.GRAPH_BLOCKED:
1764
+ return "link_then_rerun_fix_wiki"
1765
+ case FixWikiReason.TAXONOMY_DECISION_REQUIRED:
1766
+ return "taxonomy_official_route"
1767
+ case _:
1768
+ return "fix_wiki_official_route"
1769
+
1770
+
1771
+ def _related_notes_waiting_external(facts: FixWikiRuntimeFacts) -> bool:
1772
+ state = facts.related_notes_recovery_state
1773
+ return (
1774
+ facts.related_notes_blocked
1775
+ and state.status == "waiting_for_retry"
1776
+ and state.resume_supported
1777
+ and state.blocked_reason
1778
+ in {
1779
+ "related_notes_headless_quota_exhausted",
1780
+ "related_notes_headless_time_budget_exhausted",
1781
+ }
1782
+ )
1783
+
1784
+
1785
+ def _specialist_model_waiting_agent(facts: FixWikiRuntimeFacts) -> bool:
1786
+ if facts.requires_llm_rewrite_count <= 0:
1787
+ return False
1788
+ for effect in facts.pending_effects:
1789
+ if effect.kind == WorkflowEffectKind.CALL_SPECIALIST_MODEL:
1790
+ return True
1791
+ return False
1792
+
1793
+
1794
+ def _related_notes_external_wait_message(facts: FixWikiFsmFacts) -> str:
1795
+ blocked_reason = facts.related_notes_recovery_state.blocked_reason
1796
+ if blocked_reason == "related_notes_headless_time_budget_exhausted":
1797
+ return "Related Notes pausou a indexação para evitar uma execução longa; a próxima tentativa retoma do índice parcial."
1798
+ if blocked_reason == "related_notes_headless_quota_exhausted":
1799
+ return "Related Notes aguardando cota externa para retomar pela rota oficial."
1800
+ return "Related Notes aguardando condição externa para retomar pela rota oficial."
1801
+
1802
+
1803
+ def _progress_user_action(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> str:
1804
+ """Domain-owned user action text; kernel progress stays domain-agnostic."""
1805
+
1806
+ if projection.status != WorkflowProgressStatus.WAITING_EXTERNAL:
1807
+ return ""
1808
+ if projection.reason == FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
1809
+ blocked_reason = facts.related_notes_recovery_state.blocked_reason
1810
+ if blocked_reason == "related_notes_headless_time_budget_exhausted":
1811
+ return (
1812
+ "A indexacao pausou para evitar uma execucao longa; "
1813
+ "o progresso foi preservado para retomar pela acao oficial."
1814
+ )
1815
+ if blocked_reason == "related_notes_headless_quota_exhausted":
1816
+ return "Aguarde a cota externa; o progresso foi preservado para retomar pela acao oficial."
1817
+ if (
1818
+ projection.reason == FixWikiReason.WAITING_EXTERNAL
1819
+ and facts.external_wait_reason_code == "specialist_model_capacity_unavailable"
1820
+ ):
1821
+ return "Aguarde o modelo especializado antes de retomar pela rota oficial."
1822
+ return ""
1823
+
1824
+
1825
+ def _progress_state(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> WorkflowProgressState:
1826
+ current = _related_current(facts.related_notes_recovery_state)
1827
+ total = _related_total(facts.related_notes_recovery_state)
1828
+ remaining = _related_remaining(facts.related_notes_recovery_state, current=current, total=total)
1829
+ if total and current > total:
1830
+ current = max(0, total - remaining) if remaining else total
1831
+ if total == 0:
1832
+ current = max(0, facts.total_changed_count)
1833
+ total = max(current, facts.total_changed_count)
1834
+ remaining = max(0, total - current)
1835
+ counts = WorkflowProgressCounts(
1836
+ planned_items=total,
1837
+ processed_items=current,
1838
+ warnings=facts.warning_count,
1839
+ mutated_files=_applied_mutation_file_count(facts),
1840
+ written_files=_applied_written_file_count(facts),
1841
+ remaining_items=remaining,
1842
+ blocked_items=_blocked_item_count(facts),
1843
+ )
1844
+ return WorkflowProgressState(
1845
+ workflow=FIX_WIKI_WORKFLOW,
1846
+ run_id=facts.run_id,
1847
+ state=projection.state.value,
1848
+ phase=_progress_phase(facts, projection),
1849
+ event_type=projection.event_type,
1850
+ message=projection.message,
1851
+ status=projection.status,
1852
+ current=current,
1853
+ total=total,
1854
+ counts=counts,
1855
+ resume_action=projection.resume_action,
1856
+ resume_supported=projection.resume_supported,
1857
+ can_continue_now=projection.can_continue_now,
1858
+ user_action=_progress_user_action(facts, projection),
1859
+ decision=projection.decision.decision_summary() if projection.decision is not None else None,
1860
+ technical_context={
1861
+ "reason": projection.reason.value,
1862
+ "trigger": projection.trigger,
1863
+ "related_notes_blocked_reason": facts.related_notes_recovery_state.blocked_reason,
1864
+ },
1865
+ )
1866
+
1867
+
1868
+ def _fsm_directive_instructions(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> list[str]:
1869
+ common = [
1870
+ (
1871
+ "agent_instruction: use progress_view_model, state_machine_snapshot, receipt, "
1872
+ "reports.public_report.lines and reports.details.primary_objective_summary as source of truth."
1873
+ ),
1874
+ "agent_instruction: ignore diagnostic-only fields when they contradict FSM fields.",
1875
+ ]
1876
+ match projection.reason:
1877
+ case FixWikiReason.PREVIEW_READY:
1878
+ specific = [
1879
+ "agent_instruction: report that this was a preview and no vault files were changed.",
1880
+ "agent_instruction: do not run extra probes or repeat fix-wiki before the user requests apply.",
1881
+ ]
1882
+ case FixWikiReason.COMPLETED:
1883
+ specific = [
1884
+ "agent_instruction: write the final report now using reports.public_report.lines and reports.details.primary_objective_summary.",
1885
+ "agent_instruction: do not run additional diagnostics after completed status.",
1886
+ ]
1887
+ case FixWikiReason.COMPLETED_WITH_WARNINGS:
1888
+ specific = [
1889
+ "agent_instruction: write the final report now and explicitly include the remaining warnings.",
1890
+ "agent_instruction: do not describe completed_with_warnings as a clean completion.",
1891
+ ]
1892
+ case FixWikiReason.STYLE_REWRITE_READY:
1893
+ specific = [
1894
+ "agent_instruction: do not write the final report yet.",
1895
+ (
1896
+ "agent_instruction: execute agent_directive.control.effects through the official "
1897
+ "harness route before final report."
1898
+ ),
1899
+ (
1900
+ "agent_instruction: do not repeat /mednotes:fix-wiki preview before the executable "
1901
+ "effects are applied or blocked."
1902
+ ),
1903
+ "agent_instruction: do not invent specialist prompts or substitute packaged specialists.",
1904
+ "agent_instruction: do not edit receipts, manifests, attestations or specialist outputs by hand.",
1905
+ (
1906
+ "agent_instruction: follow agent_directive.control.effects returned by each official apply; "
1907
+ "only rerun /mednotes:fix-wiki when the style queue is empty."
1908
+ ),
1909
+ ]
1910
+ case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
1911
+ specific = [
1912
+ "agent_instruction: do not write the final report yet.",
1913
+ (
1914
+ "agent_instruction: execute agent_directive.control.effects through the official "
1915
+ "harness route before final report."
1916
+ ),
1917
+ (
1918
+ "agent_instruction: do not classify vocabulary semantic ingestion as linker failure; "
1919
+ "this is an executable waiting_agent state."
1920
+ ),
1921
+ "agent_instruction: rerun /mednotes:fix-wiki only after the vocabulary effect completes or blocks.",
1922
+ ]
1923
+ case FixWikiReason.WAITING_EXTERNAL | FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
1924
+ specific = [
1925
+ "agent_instruction: report the external wait and preserved progress; do not claim the Wiki is fixed.",
1926
+ "agent_instruction: do not manually call external APIs or regenerate external indexes outside the official route.",
1927
+ ]
1928
+ if projection.resume_action:
1929
+ specific.append(f"agent_instruction: resume only through resume_action: {projection.resume_action}.")
1930
+ case (
1931
+ FixWikiReason.WAITING_HUMAN
1932
+ | FixWikiReason.SUBAGENT_PLAN_ATTESTATION_REQUIRED
1933
+ | FixWikiReason.SUBAGENT_PLAN_ATTESTATION_INVALID
1934
+ | FixWikiReason.STYLE_REWRITE_REVIEW_REQUIRED
1935
+ | FixWikiReason.TAXONOMY_DECISION_REQUIRED
1936
+ | FixWikiReason.VOCABULARY_EVAL_NEEDS_REVIEW
1937
+ | FixWikiReason.GRAPH_REVIEW_REQUIRED
1938
+ | FixWikiReason.ATOMICITY_SPLIT_REVIEW_REQUIRED
1939
+ | FixWikiReason.MERGE_REVIEW_REQUIRED
1940
+ ):
1941
+ specific = [
1942
+ "agent_instruction: present human_decision_packet options; do not choose on behalf of the user.",
1943
+ "agent_instruction: do not mutate the vault until the human decision is provided.",
1944
+ ]
1945
+ case (
1946
+ FixWikiReason.ENVIRONMENT_PATHS_MISSING
1947
+ | FixWikiReason.ENVIRONMENT_WIKI_DIR_MISSING
1948
+ | FixWikiReason.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED
1949
+ ):
1950
+ specific = [
1951
+ "agent_instruction: do not claim fix-wiki failed; this is a recoverable setup blocker.",
1952
+ "agent_instruction: execute only the typed /mednotes:setup effect before rerunning fix-wiki.",
1953
+ "agent_instruction: do not patch scripts or prompts as a workaround for environment setup.",
1954
+ ]
1955
+ case (
1956
+ FixWikiReason.GRAPH_BLOCKED
1957
+ | FixWikiReason.RELATED_NOTES_BLOCKED
1958
+ | FixWikiReason.LINKER_BLOCKED
1959
+ | FixWikiReason.TAXONOMY_BLOCKED
1960
+ | FixWikiReason.STYLE_REWRITE_REQUIRED
1961
+ | FixWikiReason.ATOMICITY_SPLIT_REQUIRED
1962
+ ):
1963
+ specific = [
1964
+ "agent_instruction: report the blocker and next action; do not treat tool success as workflow success.",
1965
+ "agent_instruction: do not mutate the vault or launch alternate commands outside the FSM next action.",
1966
+ ]
1967
+ case FixWikiReason.FAILED:
1968
+ specific = [
1969
+ "agent_instruction: report the failure root cause and next action; do not claim success.",
1970
+ "agent_instruction: do not retry with ad hoc commands outside the official route.",
1971
+ ]
1972
+ case _:
1973
+ specific = [
1974
+ "agent_instruction: follow the StateChart status and report only after the FSM reaches a terminal state.",
1975
+ ]
1976
+ if facts.pending_effects and projection.reason != FixWikiReason.STYLE_REWRITE_READY:
1977
+ specific.append("agent_instruction: pending_effects exist; do not ignore them in the final report.")
1978
+ return [*common, *specific]
1979
+
1980
+
1981
+ def _progress_phase(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> str:
1982
+ if projection.reason == FixWikiReason.WAITING_EXTERNAL:
1983
+ if projection.state == FixWikiState.RELATED_NOTES_QUOTA_WAIT:
1984
+ return _PHASE_BY_STATE[projection.state.value]
1985
+ payload = facts.external_wait_payload
1986
+ effect = _FixWikiExternalWaitEffectFields.model_validate(
1987
+ _optional_json_object_subset(payload, "effect", ("origin_state",))
1988
+ )
1989
+ phase = effect.origin_state.strip()
1990
+ if phase:
1991
+ return _PHASE_BY_STATE.get(phase, phase)
1992
+ if facts.external_wait_reason_code == "specialist_model_capacity_unavailable":
1993
+ return "style_rewrite"
1994
+ if projection.reason == FixWikiReason.STYLE_REWRITE_READY:
1995
+ return "style_rewrite"
1996
+ return _PHASE_BY_STATE[projection.state.value]
1997
+
1998
+
1999
+ def _snapshot_from_model(
2000
+ model: WorkflowModel,
2001
+ projection: _FixWikiStateView,
2002
+ progress_state: WorkflowProgressState,
2003
+ ) -> WorkflowStateMachineSnapshot:
2004
+ if model.state != projection.state.value:
2005
+ raise ValueError("FixWikiMachine state must match public projection state")
2006
+ progress_event = WorkflowProgressEvent(
2007
+ workflow=FIX_WIKI_WORKFLOW,
2008
+ run_id=model.run_id,
2009
+ state=projection.state.value,
2010
+ phase=progress_state.phase,
2011
+ event_type=projection.event_type,
2012
+ message=projection.message,
2013
+ status=projection.status,
2014
+ current=progress_state.current,
2015
+ total=progress_state.total,
2016
+ counts=progress_state.counts,
2017
+ resume_action=projection.resume_action,
2018
+ resume_supported=projection.resume_supported,
2019
+ can_continue_now=projection.can_continue_now,
2020
+ decision=progress_state.decision,
2021
+ technical_context=progress_state.technical_context,
2022
+ )
2023
+ transitions: list[WorkflowTransition] = []
2024
+ for index, transition in enumerate(model.transition_log):
2025
+ progress_events = [progress_event] if index == len(model.transition_log) - 1 else []
2026
+ transitions.append(
2027
+ WorkflowTransition(
2028
+ workflow=transition.workflow,
2029
+ from_state=transition.from_state,
2030
+ to_state=transition.to_state,
2031
+ to_category=category_for_state(transition.to_state),
2032
+ trigger=transition.trigger,
2033
+ effects=list(transition.effects),
2034
+ progress_events=progress_events,
2035
+ decision=transition.decision,
2036
+ resume_action=transition.resume_action,
2037
+ )
2038
+ )
2039
+ return WorkflowStateMachineSnapshot(
2040
+ workflow=FIX_WIKI_WORKFLOW,
2041
+ run_id=model.run_id,
2042
+ current_state=model.state,
2043
+ current_category=category_for_state(model.state),
2044
+ transitions=transitions,
2045
+ metadata={"reason": projection.reason.value, "source": "FixWikiMachine"},
2046
+ )
2047
+
2048
+
2049
+ def _transition_effects(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> list[WorkflowEffect]:
2050
+ allowed_kinds = _allowed_effect_kinds_for_fix_wiki_state(
2051
+ category=projection.category,
2052
+ current_state=projection.state.value,
2053
+ )
2054
+ if not allowed_kinds:
2055
+ return []
2056
+ return [effect for effect in facts.machine_effects if effect.kind in allowed_kinds]
2057
+
2058
+
2059
+ def _allowed_effect_kinds_for_fix_wiki_state(
2060
+ *,
2061
+ category: WorkflowStateCategory,
2062
+ current_state: str,
2063
+ ) -> set[WorkflowEffectKind]:
2064
+ """Return effect kinds executable from a concrete fix-wiki StateChart leaf."""
2065
+
2066
+ allowed_kinds = _allowed_effect_kinds_for_category(category)
2067
+ if current_state in {
2068
+ FixWikiState.ENVIRONMENT_PATHS_MISSING.value,
2069
+ FixWikiState.ENVIRONMENT_WIKI_DIR_MISSING.value,
2070
+ FixWikiState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED.value,
2071
+ FixWikiState.LINK_RUN_REQUESTED.value,
2072
+ }:
2073
+ allowed_kinds = allowed_kinds | {WorkflowEffectKind.RUN_SUBWORKFLOW}
2074
+ return allowed_kinds
2075
+
2076
+
2077
+ def _allowed_effect_kinds_for_category(category: WorkflowStateCategory) -> set[WorkflowEffectKind]:
2078
+ match category:
2079
+ case WorkflowStateCategory.WAITING_AGENT:
2080
+ return {WorkflowEffectKind.CALL_SPECIALIST_MODEL, WorkflowEffectKind.RUN_SUBWORKFLOW}
2081
+ case WorkflowStateCategory.WAITING_EXTERNAL:
2082
+ return {WorkflowEffectKind.WAIT_EXTERNAL}
2083
+ case WorkflowStateCategory.WAITING_HUMAN:
2084
+ return {WorkflowEffectKind.ASK_HUMAN}
2085
+ case _:
2086
+ return set()
2087
+
2088
+
2089
+ def _receipt(
2090
+ facts: FixWikiFsmFacts,
2091
+ projection: _FixWikiStateView,
2092
+ progress_state: WorkflowProgressState,
2093
+ snapshot: WorkflowStateMachineSnapshot,
2094
+ *,
2095
+ human_decision_packet: JsonObject | HumanDecisionPacket | None = None,
2096
+ ) -> WorkflowReceiptPayload:
2097
+ view_model = build_progress_view_model(progress_state)
2098
+ return WorkflowReceiptPayload(
2099
+ schema=FIX_WIKI_RECEIPT_SCHEMA,
2100
+ workflow=FIX_WIKI_WORKFLOW,
2101
+ run_id=facts.run_id,
2102
+ status=_receipt_status(projection),
2103
+ mutated=_applied_mutation_file_count(facts) > 0,
2104
+ next_action=_receipt_next_action(projection),
2105
+ human_decision_required=projection.status == WorkflowProgressStatus.WAITING_HUMAN,
2106
+ human_decision_packet=human_decision_packet,
2107
+ phase_outcomes=_receipt_phase_outcomes(projection, human_decision_packet=human_decision_packet),
2108
+ artifacts=_receipt_artifacts(facts.artifacts),
2109
+ changed_files=list(facts.changed_files),
2110
+ version_control_safety=facts.version_control_safety,
2111
+ progress_state=progress_state,
2112
+ progress_view_model=view_model,
2113
+ state_machine_snapshot=snapshot,
2114
+ )
2115
+
2116
+
2117
+ def _receipt_phase_outcomes(
2118
+ projection: _FixWikiStateView,
2119
+ *,
2120
+ human_decision_packet: JsonObject | HumanDecisionPacket | None,
2121
+ ) -> list[WorkflowPhaseOutcome]:
2122
+ """Embed the FSM decision in the receipt instead of duplicating blocker fields."""
2123
+
2124
+ if projection.decision is None:
2125
+ return []
2126
+ packet = None
2127
+ if human_decision_packet is not None:
2128
+ packet = HumanDecisionPacket.model_validate(human_decision_packet)
2129
+ return [
2130
+ WorkflowPhaseOutcome(
2131
+ phase=projection.decision.phase,
2132
+ decision_summary=projection.decision.decision_summary(),
2133
+ human_decision_packet=packet,
2134
+ )
2135
+ ]
2136
+
2137
+
2138
+ def _applied_mutation_file_count(facts: FixWikiFsmFacts) -> int:
2139
+ if not facts.effective_apply:
2140
+ return 0
2141
+ if facts.vault_changed_file_count > 0:
2142
+ return facts.vault_changed_file_count
2143
+ if facts.total_changed_count > 0:
2144
+ return facts.total_changed_count
2145
+ if facts.written_count > 0:
2146
+ return facts.written_count
2147
+ return len(_non_backup_changed_files(facts.changed_files))
2148
+
2149
+
2150
+ def _applied_written_file_count(facts: FixWikiFsmFacts) -> int:
2151
+ if not facts.effective_apply:
2152
+ return 0
2153
+ if facts.written_count > 0:
2154
+ return facts.written_count
2155
+ if facts.total_changed_count > 0:
2156
+ return facts.total_changed_count
2157
+ if facts.vault_changed_file_count > 0:
2158
+ return facts.vault_changed_file_count
2159
+ return len(_non_backup_changed_files(facts.changed_files))
2160
+
2161
+
2162
+ def _non_backup_changed_files(paths: list[str]) -> list[str]:
2163
+ return [path for path in paths if path.strip() and not path.endswith(".bak")]
2164
+
2165
+
2166
+ def _receipt_status(projection: _FixWikiStateView) -> ReceiptStatus:
2167
+ match projection.status:
2168
+ case WorkflowProgressStatus.COMPLETED:
2169
+ return "completed"
2170
+ case WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
2171
+ return "completed_with_warnings"
2172
+ case WorkflowProgressStatus.WAITING_AGENT:
2173
+ return "waiting_agent"
2174
+ case WorkflowProgressStatus.WAITING_EXTERNAL:
2175
+ return "waiting_external"
2176
+ case WorkflowProgressStatus.WAITING_HUMAN:
2177
+ return "waiting_human"
2178
+ case WorkflowProgressStatus.RUNNING:
2179
+ return "running"
2180
+ case WorkflowProgressStatus.BLOCKED:
2181
+ return "blocked"
2182
+ case WorkflowProgressStatus.FAILED:
2183
+ return "failed"
2184
+ case _:
2185
+ return "blocked"
2186
+
2187
+
2188
+ def _receipt_next_action(projection: _FixWikiStateView) -> str:
2189
+ if projection.status == WorkflowProgressStatus.COMPLETED:
2190
+ return ""
2191
+ return projection.next_action
2192
+
2193
+
2194
+ def _receipt_artifacts(artifacts: JsonObject) -> list[dict[str, str]]:
2195
+ receipt_artifacts: list[dict[str, str]] = []
2196
+ for key, value in artifacts.items():
2197
+ if value is None:
2198
+ continue
2199
+ if isinstance(value, dict):
2200
+ for nested_key, nested_value in value.items():
2201
+ if nested_value is None:
2202
+ continue
2203
+ nested_path = str(nested_value).strip()
2204
+ if nested_path:
2205
+ receipt_artifacts.append({"kind": f"{key}.{nested_key}", "path": nested_path})
2206
+ continue
2207
+ receipt_artifacts.append({"kind": str(key), "path": str(value)})
2208
+ return receipt_artifacts
2209
+
2210
+
2211
+ def _reports(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> WorkflowReports:
2212
+ match projection.reason:
2213
+ case FixWikiReason.COMPLETED:
2214
+ summary = "Corrigi e conferi a Wiki."
2215
+ case FixWikiReason.COMPLETED_WITH_WARNINGS:
2216
+ summary = "Corrigi a Wiki com avisos pendentes."
2217
+ case FixWikiReason.PREVIEW_READY:
2218
+ summary = "Conferi a Wiki; nada foi alterado."
2219
+ case FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
2220
+ summary = "Pausei a etapa do Related Notes porque ela depende de cota externa."
2221
+ case FixWikiReason.WAITING_EXTERNAL:
2222
+ if (
2223
+ facts.external_wait_reason_code == "specialist_model_capacity_unavailable"
2224
+ and facts.requires_llm_rewrite_count > 0
2225
+ ):
2226
+ rewrite_label = (
2227
+ "reescrita especializada"
2228
+ if facts.requires_llm_rewrite_count == 1
2229
+ else "reescritas especializadas"
2230
+ )
2231
+ summary = (
2232
+ "Apliquei os reparos seguros e pausei porque "
2233
+ f"{facts.requires_llm_rewrite_count} {rewrite_label} dependem do modelo especializado."
2234
+ )
2235
+ else:
2236
+ summary = "Pausei o workflow porque ele depende de uma condicao externa."
2237
+ case FixWikiReason.STYLE_REWRITE_READY:
2238
+ summary = "Apliquei os reparos seguros e vou continuar pela reescrita especializada."
2239
+ case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
2240
+ summary = "Apliquei os reparos seguros e vou continuar pela curadoria semantica do vocabulario."
2241
+ case (
2242
+ FixWikiReason.WAITING_HUMAN
2243
+ | FixWikiReason.SUBAGENT_PLAN_ATTESTATION_REQUIRED
2244
+ | FixWikiReason.SUBAGENT_PLAN_ATTESTATION_INVALID
2245
+ | FixWikiReason.STYLE_REWRITE_REVIEW_REQUIRED
2246
+ | FixWikiReason.TAXONOMY_DECISION_REQUIRED
2247
+ | FixWikiReason.VOCABULARY_EVAL_NEEDS_REVIEW
2248
+ | FixWikiReason.ATOMICITY_SPLIT_REVIEW_REQUIRED
2249
+ | FixWikiReason.MERGE_REVIEW_REQUIRED
2250
+ ):
2251
+ summary = "Preciso de uma escolha sua antes de continuar."
2252
+ case FixWikiReason.ATOMICITY_SPLIT_REQUIRED:
2253
+ summary = "O fix-wiki parou porque ha split de atomicidade pendente."
2254
+ case FixWikiReason.GRAPH_BLOCKED:
2255
+ summary = "A Wiki ainda precisa de reparo de grafo pela rota oficial."
2256
+ case FixWikiReason.RELATED_NOTES_BLOCKED:
2257
+ summary = "O fix-wiki parou porque as Notas Relacionadas ainda precisam ser atualizadas."
2258
+ case FixWikiReason.LINKER_BLOCKED:
2259
+ summary = "O pacote de links ainda esta bloqueado."
2260
+ case FixWikiReason.TAXONOMY_BLOCKED:
2261
+ summary = "A taxonomia exige acao antes de concluir."
2262
+ case FixWikiReason.STYLE_REWRITE_REQUIRED:
2263
+ summary = "Ha reescrita semantica pendente antes de concluir."
2264
+ case FixWikiReason.FAILED:
2265
+ summary = "O fix-wiki falhou antes de concluir."
2266
+ case _:
2267
+ summary = projection.message
2268
+ return WorkflowReports(
2269
+ summary=summary,
2270
+ public_report=_public_report(facts, projection, summary),
2271
+ )
2272
+
2273
+
2274
+ def _reports_with_primary_objective_summary(
2275
+ reports: WorkflowReports,
2276
+ *,
2277
+ run_id: str,
2278
+ progress_view_model: WorkflowProgressViewModel,
2279
+ receipt: WorkflowReceiptPayload,
2280
+ diagnostic_context: JsonObject,
2281
+ ) -> WorkflowReports:
2282
+ """Attach the typed validator summary under the shared reports envelope."""
2283
+
2284
+ objective = fix_wiki_primary_objective_summary(
2285
+ JsonObjectAdapter.validate_python(
2286
+ {
2287
+ "schema": FIX_WIKI_SCHEMA,
2288
+ "workflow": FIX_WIKI_WORKFLOW,
2289
+ "run_id": run_id,
2290
+ "progress_view_model": progress_view_model.to_payload(),
2291
+ "receipt": receipt.to_payload(),
2292
+ "diagnostic_context": dict(diagnostic_context),
2293
+ }
2294
+ )
2295
+ )
2296
+ if objective is None:
2297
+ return reports
2298
+ details = dict(reports.details)
2299
+ details["primary_objective_summary"] = objective.to_payload()
2300
+ return reports.model_copy(update={"details": JsonObjectAdapter.validate_python(details)})
2301
+
2302
+
2303
+ def _payload_with_primary_objective_summary(payload: JsonObject) -> JsonObject:
2304
+ """Refresh the structured validator summary on the final serialized payload."""
2305
+
2306
+ objective = fix_wiki_primary_objective_summary(payload)
2307
+ if objective is None:
2308
+ return payload
2309
+ reports = JsonObjectAdapter.validate_python(payload["reports"] if "reports" in payload else {})
2310
+ details = JsonObjectAdapter.validate_python(reports["details"] if "details" in reports else {})
2311
+ details["primary_objective_summary"] = objective.to_payload()
2312
+ reports["details"] = details
2313
+ payload["reports"] = reports
2314
+ return payload
2315
+
2316
+
2317
+ def _public_report(facts: FixWikiFsmFacts, projection: _FixWikiStateView, summary: str) -> WorkflowPublicReport:
2318
+ """Human UX projection owned by the same FSM result as the machine state."""
2319
+
2320
+ changed_count = _applied_mutation_file_count(facts)
2321
+ vault_has_changes = changed_count > 0
2322
+ human_decision_required = (
2323
+ projection.status == WorkflowProgressStatus.WAITING_HUMAN
2324
+ or (projection.decision is not None and projection.decision.kind == "ask_human")
2325
+ )
2326
+ can_continue_without_human = projection.status == WorkflowProgressStatus.WAITING_AGENT and projection.can_continue_now
2327
+
2328
+ if can_continue_without_human:
2329
+ headline = (
2330
+ "Apliquei reparos iniciais e vou continuar automaticamente."
2331
+ if vault_has_changes
2332
+ else "Preparei a próxima etapa e vou continuar automaticamente."
2333
+ )
2334
+ elif not facts.requested_apply and not vault_has_changes:
2335
+ headline = "Conferi a Wiki; nada foi alterado."
2336
+ elif vault_has_changes and projection.status == WorkflowProgressStatus.COMPLETED:
2337
+ headline = "Corrigi a Wiki."
2338
+ elif vault_has_changes and projection.status == WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
2339
+ headline = "Corrigi a Wiki; restaram avisos não bloqueantes."
2340
+ elif vault_has_changes:
2341
+ headline = "Apliquei reparos na Wiki, mas ainda falta concluir."
2342
+ else:
2343
+ headline = "Não alterei a Wiki."
2344
+
2345
+ lines: list[str] = [headline]
2346
+ if vault_has_changes:
2347
+ lines.append(f"Alterei {changed_count} arquivo(s) da Wiki nesta etapa.")
2348
+ elif not facts.requested_apply:
2349
+ lines.append("Esta foi uma conferência: nenhum arquivo da Wiki foi alterado.")
2350
+ else:
2351
+ lines.append("Nenhum arquivo da Wiki foi alterado nesta etapa.")
2352
+
2353
+ if can_continue_without_human:
2354
+ lines.append(_public_followup_line(facts, projection))
2355
+
2356
+ blockers = _public_blockers(facts, projection, human_decision_required=human_decision_required)
2357
+ if blockers:
2358
+ lines.append("Ainda falta concluir; " + "; ".join(blockers) + ".")
2359
+ elif projection.status == WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
2360
+ lines.append("A Wiki ficou sem bloqueios técnicos, mas ainda há avisos para revisar.")
2361
+ elif projection.status == WorkflowProgressStatus.COMPLETED:
2362
+ lines.append("Não encontrei bloqueios técnicos restantes.")
2363
+
2364
+ if human_decision_required:
2365
+ question = ""
2366
+ if projection.decision is not None and projection.decision.public_summary:
2367
+ question = projection.decision.public_summary.strip()
2368
+ if question:
2369
+ lines.append(f"Preciso da sua decisão: {question}")
2370
+ else:
2371
+ lines.append(_public_followup_line(facts, projection))
2372
+ elif projection.status == WorkflowProgressStatus.WAITING_EXTERNAL:
2373
+ lines.append(_waiting_external_public_line(facts, projection))
2374
+ elif not can_continue_without_human and projection.status not in {
2375
+ WorkflowProgressStatus.COMPLETED,
2376
+ WorkflowProgressStatus.COMPLETED_WITH_WARNINGS,
2377
+ }:
2378
+ followup_line = _public_followup_line(facts, projection)
2379
+ if followup_line:
2380
+ lines.append(followup_line)
2381
+
2382
+ return WorkflowPublicReport(
2383
+ workflow=FIX_WIKI_WORKFLOW,
2384
+ run_id=facts.run_id,
2385
+ headline=headline,
2386
+ lines=lines,
2387
+ )
2388
+
2389
+
2390
+ def _public_blockers(
2391
+ facts: FixWikiFsmFacts,
2392
+ projection: _FixWikiStateView,
2393
+ *,
2394
+ human_decision_required: bool,
2395
+ ) -> list[str]:
2396
+ blockers: list[str] = []
2397
+ match projection.reason:
2398
+ case (
2399
+ FixWikiReason.STYLE_REWRITE_READY
2400
+ | FixWikiReason.STYLE_REWRITE_REQUIRED
2401
+ | FixWikiReason.STYLE_REWRITE_REVIEW_REQUIRED
2402
+ ):
2403
+ blockers.append("há nota(s) que precisam de reescrita assistida antes de concluir")
2404
+ case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
2405
+ blockers.append("a curadoria semântica do vocabulário ainda precisa ser aplicada")
2406
+ case FixWikiReason.GRAPH_BLOCKED | FixWikiReason.GRAPH_REVIEW_REQUIRED:
2407
+ blockers.append("o grafo de links ainda tem referência(s) que precisam ser corrigidas")
2408
+ case FixWikiReason.RELATED_NOTES_BLOCKED | FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
2409
+ blockers.append("as Notas Relacionadas ainda precisam ser atualizadas")
2410
+ case FixWikiReason.LINKER_BLOCKED:
2411
+ blockers.append("a atualização de links ainda não pôde ser concluída")
2412
+ case FixWikiReason.TAXONOMY_BLOCKED | FixWikiReason.TAXONOMY_DECISION_REQUIRED:
2413
+ blockers.append("a organização por pastas ainda precisa de revisão")
2414
+ case FixWikiReason.ATOMICITY_SPLIT_REQUIRED | FixWikiReason.ATOMICITY_SPLIT_REVIEW_REQUIRED:
2415
+ blockers.append("há nota(s) que precisam ser divididas antes de concluir")
2416
+ case _:
2417
+ pass
2418
+ if human_decision_required:
2419
+ blockers.append("há uma decisão humana pendente antes de continuar")
2420
+ return blockers
2421
+
2422
+
2423
+ def _waiting_external_public_line(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> str:
2424
+ if projection.reason == FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES or facts.related_notes_recovery_state.status:
2425
+ return "As Notas Relacionadas dependem de quota externa antes de retomar com segurança."
2426
+ return _public_followup_line(facts, projection)
2427
+
2428
+
2429
+ def _public_followup_line(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> str:
2430
+ """Render a safe next-step sentence without copying technical next_action text."""
2431
+
2432
+ if projection.reason in {
2433
+ FixWikiReason.GRAPH_BLOCKED,
2434
+ FixWikiReason.GRAPH_REVIEW_REQUIRED,
2435
+ FixWikiReason.LINKER_BLOCKED,
2436
+ } and _graph_curator_followup(facts.next_action):
2437
+ return "Retomar a curadoria do grafo pela rota oficial antes de concluir."
2438
+ return public_progress_followup_line(_progress_state(facts, projection))
2439
+
2440
+
2441
+ def _graph_curator_followup(next_action: str) -> bool:
2442
+ """Recognize graph-curator workflow hints only to choose closed public wording."""
2443
+
2444
+ normalized = next_action.casefold()
2445
+ return any(marker in normalized for marker in ("med-link-graph-curator", "collect-curator-outputs", "curator-batch"))
2446
+
2447
+
2448
+ def _public_report_summary_text(public_report: WorkflowPublicReport) -> str:
2449
+ """Use the public report lines as the single human-visible summary channel."""
2450
+
2451
+ return public_report.summary_text()
2452
+
2453
+
2454
+ def _diagnostic_context(
2455
+ facts: FixWikiFsmFacts,
2456
+ projection: _FixWikiStateView,
2457
+ ) -> JsonObject:
2458
+ """Build explanatory diagnostics without carrying executable control."""
2459
+
2460
+ context: JsonObject = dict(facts.diagnostic_context)
2461
+ for key in FIX_WIKI_DIAGNOSTIC_PARALLEL_TRUTH_KEYS:
2462
+ if key in context:
2463
+ context.pop(key)
2464
+ apply_context = _optional_json_object_field(context, "apply")
2465
+ context.update(
2466
+ {
2467
+ "schema": "medical-notes-workbench.fix-wiki-fsm-diagnostic-context.v1",
2468
+ "reason": projection.reason.value,
2469
+ "state": projection.state.value,
2470
+ "apply": {
2471
+ **apply_context,
2472
+ "requested_apply": facts.requested_apply,
2473
+ "effective_apply": facts.effective_apply,
2474
+ },
2475
+ "counts": {
2476
+ "total_changed_count": facts.total_changed_count,
2477
+ "vault_changed_file_count": facts.vault_changed_file_count,
2478
+ "written_count": facts.written_count,
2479
+ "warning_count": facts.warning_count,
2480
+ "requires_llm_rewrite_count": facts.requires_llm_rewrite_count,
2481
+ "graph_error_count": facts.graph_error_count,
2482
+ "graph_blocker_count": facts.graph_blocker_count,
2483
+ },
2484
+ "final_validation": dict(facts.final_validation),
2485
+ "linker_blocked": facts.linker_blocked,
2486
+ "related_notes_blocked": facts.related_notes_blocked,
2487
+ "atomicity_split_required": facts.atomicity_split_required,
2488
+ "graph_review_required": facts.graph_review_required,
2489
+ "taxonomy_action_required": facts.taxonomy_action_required,
2490
+ }
2491
+ )
2492
+ if facts.failed_reason_code:
2493
+ context["root_cause"] = facts.failed_reason_code
2494
+ if facts.related_notes_recovery_state:
2495
+ context["related_notes_recovery_state"] = facts.related_notes_recovery_state.operation_payload
2496
+ if facts.external_wait_payload:
2497
+ context["external_wait_payload"] = dict(facts.external_wait_payload)
2498
+ return diagnostic_context_evidence_only(context)
2499
+
2500
+
2501
+ def _agent_directive(
2502
+ facts: FixWikiFsmFacts,
2503
+ projection: _FixWikiStateView,
2504
+ *,
2505
+ progress_view_model: WorkflowProgressViewModel,
2506
+ user_visible_summary: str,
2507
+ ) -> JsonObject:
2508
+ """Build the root executable agent contract directly from FSM state."""
2509
+
2510
+ typed = agent_directive_from_progress_view_model(
2511
+ progress_view_model,
2512
+ schema=MEDNOTES_AGENT_DIRECTIVE_SCHEMA,
2513
+ reason=projection.reason.value,
2514
+ effects=_transition_effects(facts, projection),
2515
+ blockers=_blocked_by_for_guidance(projection),
2516
+ resume=projection.resume_action,
2517
+ report_requires=["primary_objective", "mutations", "graph", "related_notes"],
2518
+ summary=user_visible_summary,
2519
+ instructions=_plain_agent_directive_instructions(_fsm_directive_instructions(facts, projection)),
2520
+ )
2521
+ return JsonObjectAdapter.validate_python(typed.to_payload())
2522
+
2523
+
2524
+ def _problem_diagnostic_context(context: JsonObject, projection: _FixWikiStateView) -> JsonObject:
2525
+ vocabulary_bootstrap = _FixWikiVocabularyBootstrapDiagnostic.model_validate(
2526
+ context["vocabulary_bootstrap"]
2527
+ if "vocabulary_bootstrap" in context and isinstance(context["vocabulary_bootstrap"], dict)
2528
+ else {}
2529
+ )
2530
+ explicit_vocabulary_reset = vocabulary_bootstrap.trigger == "explicit_vocabulary_reset"
2531
+ if projection.status == WorkflowProgressStatus.COMPLETED:
2532
+ if explicit_vocabulary_reset:
2533
+ return JsonObjectAdapter.validate_python(context)
2534
+ return {}
2535
+ return JsonObjectAdapter.validate_python(context)
2536
+
2537
+
2538
+ def _blocked_by_for_guidance(projection: _FixWikiStateView) -> list[str]:
2539
+ if projection.status not in {
2540
+ WorkflowProgressStatus.BLOCKED,
2541
+ WorkflowProgressStatus.FAILED,
2542
+ WorkflowProgressStatus.WAITING_EXTERNAL,
2543
+ WorkflowProgressStatus.WAITING_HUMAN,
2544
+ }:
2545
+ return []
2546
+ if projection.decision is not None:
2547
+ return [projection.decision.reason_code]
2548
+ return [projection.trigger or projection.reason.value]
2549
+
2550
+
2551
+ def _plain_agent_directive_instructions(lines: list[str]) -> list[str]:
2552
+ cleaned: list[str] = []
2553
+ for line in lines:
2554
+ text = line.strip()
2555
+ prefix = "agent_instruction:"
2556
+ if text.casefold().startswith(prefix):
2557
+ text = text[len(prefix):].strip()
2558
+ if text:
2559
+ cleaned.append(text)
2560
+ return cleaned
2561
+
2562
+
2563
+ def fix_wiki_cli_exit_code(payload: JsonObject) -> int:
2564
+ progress = _FixWikiPayloadProgressView.model_validate(
2565
+ _json_object_subset(payload, "progress_view_model", ("status",))
2566
+ )
2567
+ status = progress.status
2568
+ match status:
2569
+ case "completed" | "completed_with_warnings":
2570
+ return 0
2571
+ case "waiting_agent" | "waiting_external" | "waiting_human" | "blocked":
2572
+ return 3
2573
+ case "failed":
2574
+ return 5
2575
+ case _:
2576
+ return 1
2577
+
2578
+
2579
+ def assert_fix_wiki_fsm_payload(payload: JsonObject) -> None:
2580
+ forbidden_root_keys = set(payload) & FIX_WIKI_FORBIDDEN_ROOT_KEYS
2581
+ if forbidden_root_keys:
2582
+ raise ValueError(f"fix-wiki FSM payload contains noncanonical root fields: {sorted(forbidden_root_keys)}")
2583
+ required_root_keys = FIX_WIKI_ALLOWED_ROOT_KEYS - {"diagnostic_context"}
2584
+ missing_keys = required_root_keys - set(payload)
2585
+ if missing_keys:
2586
+ raise ValueError(f"fix-wiki FSM payload missing canonical root fields: {sorted(missing_keys)}")
2587
+ unexpected_keys = set(payload) - FIX_WIKI_ALLOWED_ROOT_KEYS
2588
+ if unexpected_keys:
2589
+ raise ValueError(f"fix-wiki FSM payload contains unexpected root fields: {sorted(unexpected_keys)}")
2590
+ diagnostic_context = payload["diagnostic_context"] if "diagnostic_context" in payload else {}
2591
+ assert_diagnostic_context_evidence_only(diagnostic_context)
2592
+ fields = _fix_wiki_payload_fields(payload)
2593
+ reports_model = WorkflowReports.model_validate(payload["reports"])
2594
+ if fields.progress_view_model.status != fields.state_machine_snapshot.current_category:
2595
+ raise ValueError("fix-wiki FSM status must match state_machine_snapshot category")
2596
+ if fields.receipt.status != fields.progress_view_model.status:
2597
+ raise ValueError("fix-wiki FSM receipt status must match progress view status")
2598
+ snapshot = WorkflowStateMachineSnapshot.model_validate(payload["state_machine_snapshot"])
2599
+ progress_view_model = WorkflowProgressViewModel.model_validate(payload["progress_view_model"])
2600
+ assert_public_report_matches_progress(
2601
+ reports_model.public_report,
2602
+ workflow=FIX_WIKI_WORKFLOW,
2603
+ run_id=str(payload["run_id"]),
2604
+ progress_view_model=progress_view_model,
2605
+ label="fix-wiki FSM",
2606
+ )
2607
+ _assert_fix_wiki_machine_snapshot(snapshot)
2608
+ _assert_fix_wiki_agent_directive_matches_snapshot(payload, snapshot, progress_view_model)
2609
+ diagnostic_context = payload["diagnostic_context"] if "diagnostic_context" in payload else None
2610
+ if isinstance(diagnostic_context, dict) and "agent_directive" in diagnostic_context:
2611
+ raise ValueError("fix-wiki FSM diagnostic_context must not contain agent_directive")
2612
+ if isinstance(diagnostic_context, dict):
2613
+ _assert_fix_wiki_diagnostic_context_is_non_operational(diagnostic_context)
2614
+
2615
+
2616
+ def _assert_fix_wiki_diagnostic_context_is_non_operational(diagnostic_context: JsonObject) -> None:
2617
+ """Keep diagnostics as evidence only; executable work belongs in agent_directive."""
2618
+
2619
+ for key in FIX_WIKI_DIAGNOSTIC_PARALLEL_TRUTH_KEYS:
2620
+ if key in diagnostic_context:
2621
+ raise ValueError(f"fix-wiki FSM diagnostic_context contains parallel truth field: {key}")
2622
+ for key in diagnostic_context:
2623
+ if str(key).startswith("human_decision"):
2624
+ raise ValueError(f"fix-wiki FSM diagnostic_context contains parallel truth field: {key}")
2625
+ for plan_key in ("orchestration_plan", "continuation_plan"):
2626
+ plan = diagnostic_context[plan_key] if plan_key in diagnostic_context else None
2627
+ if not isinstance(plan, dict):
2628
+ continue
2629
+ plan_payload = JsonObjectAdapter.validate_python(plan)
2630
+ operational_keys = sorted(set(plan_payload) & FIX_WIKI_DIAGNOSTIC_OPERATIONAL_PLAN_KEYS)
2631
+ if operational_keys:
2632
+ raise ValueError(
2633
+ f"fix-wiki FSM diagnostic_context.{plan_key} contains operational fields: {operational_keys}"
2634
+ )
2635
+
2636
+
2637
+ def _assert_fix_wiki_machine_snapshot(snapshot: WorkflowStateMachineSnapshot) -> None:
2638
+ if snapshot.workflow != FIX_WIKI_WORKFLOW:
2639
+ raise ValueError("fix-wiki FSM snapshot has invalid workflow")
2640
+ if snapshot.current_category != category_for_state(snapshot.current_state):
2641
+ raise ValueError("fix-wiki FSM snapshot category does not match StateChart state")
2642
+ edges = _fix_wiki_machine_edges()
2643
+ for transition in snapshot.transitions:
2644
+ if transition.to_category != category_for_state(transition.to_state):
2645
+ raise ValueError("fix-wiki FSM transition category does not match StateChart state")
2646
+ for effect in transition.effects:
2647
+ if effect.origin_state != transition.to_state:
2648
+ raise ValueError("fix-wiki FSM transition effect origin_state must match transition target")
2649
+ if effect.kind == WorkflowEffectKind.ASK_HUMAN and transition.to_category != WorkflowStateCategory.WAITING_HUMAN:
2650
+ raise ValueError("fix-wiki ask_human effects are only allowed for waiting_human states")
2651
+ if effect.kind == WorkflowEffectKind.WAIT_EXTERNAL and transition.to_category != WorkflowStateCategory.WAITING_EXTERNAL:
2652
+ raise ValueError("fix-wiki wait_external effects are only allowed for waiting_external states")
2653
+ if (
2654
+ effect.kind == WorkflowEffectKind.CALL_SPECIALIST_MODEL
2655
+ and transition.to_category != WorkflowStateCategory.WAITING_AGENT
2656
+ ):
2657
+ raise ValueError("fix-wiki specialist effects are only allowed for waiting_agent states")
2658
+ edge = (transition.trigger, transition.from_state, transition.to_state)
2659
+ if edge not in edges:
2660
+ raise ValueError(f"unauthorized FSM transition: {edge}")
2661
+
2662
+
2663
+ def _assert_fix_wiki_agent_directive_matches_snapshot(
2664
+ payload: JsonObject,
2665
+ snapshot: WorkflowStateMachineSnapshot,
2666
+ progress_view_model: WorkflowProgressViewModel,
2667
+ ) -> None:
2668
+ """Keep the public agent route anchored to the current StateChart leaf."""
2669
+
2670
+ try:
2671
+ directive = AgentDirective.model_validate(_optional_json_object_field(payload, "agent_directive"))
2672
+ except PydanticValidationError as exc:
2673
+ raise ValueError("fix-wiki FSM payload invalid: agent_directive") from exc
2674
+ if directive.workflow != FIX_WIKI_WORKFLOW:
2675
+ raise ValueError("fix-wiki FSM agent_directive workflow must match workflow")
2676
+ if directive.schema_ != MEDNOTES_AGENT_DIRECTIVE_SCHEMA:
2677
+ raise ValueError("fix-wiki FSM agent_directive schema must match public MedNotes contract")
2678
+ category = WorkflowStateCategory(snapshot.current_category)
2679
+ allowed_effect_kinds = _allowed_effect_kinds_for_fix_wiki_state(
2680
+ category=category,
2681
+ current_state=snapshot.current_state,
2682
+ )
2683
+ assert_agent_directive_matches_progress(
2684
+ directive,
2685
+ workflow=FIX_WIKI_WORKFLOW,
2686
+ run_id=str(payload["run_id"]),
2687
+ progress_view_model=progress_view_model,
2688
+ snapshot=snapshot,
2689
+ allowed_effect_kinds=allowed_effect_kinds,
2690
+ label="fix-wiki FSM",
2691
+ )
2692
+
2693
+
2694
+ def _fix_wiki_machine_edges() -> set[tuple[str, str, str]]:
2695
+ from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_machine import FixWikiMachine
2696
+
2697
+ edges: set[tuple[str, str, str]] = set()
2698
+ for event in FixWikiMachine.events:
2699
+ for transition in event._transitions:
2700
+ for target in transition._targets:
2701
+ edges.add((event.id, str(transition.source.value), str(target.value)))
2702
+ return edges
2703
+
2704
+
2705
+ def _fix_wiki_payload_fields(payload: JsonObject) -> _FixWikiPayloadFields:
2706
+ raw_fields: JsonObject = {
2707
+ "workflow": payload["workflow"],
2708
+ "progress_view_model": _json_object_subset(payload, "progress_view_model", ("status",)),
2709
+ "state_machine_snapshot": _json_object_subset(payload, "state_machine_snapshot", ("current_category",)),
2710
+ "receipt": _json_object_subset(payload, "receipt", ("status",)),
2711
+ }
2712
+ try:
2713
+ return _FixWikiPayloadFields.model_validate(raw_fields)
2714
+ except PydanticValidationError as exc:
2715
+ first = exc.errors()[0] if exc.errors() else {}
2716
+ loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
2717
+ msg = str(first.get("msg") or str(exc))
2718
+ raise ValueError(f"fix-wiki FSM payload invalid: {loc}: {msg}") from exc
2719
+
2720
+
2721
+ def _json_object_subset(payload: JsonObject, field_name: str, keys: tuple[str, ...]) -> JsonObject:
2722
+ try:
2723
+ source = JsonObjectAdapter.validate_python(payload[field_name])
2724
+ except PydanticValidationError as exc:
2725
+ raise ValueError(f"fix-wiki FSM payload invalid: {field_name} must be an object") from exc
2726
+ return {key: source[key] for key in keys if key in source}
2727
+
2728
+
2729
+ def _optional_json_object_field(payload: JsonObject, field_name: str) -> JsonObject:
2730
+ if field_name not in payload:
2731
+ return {}
2732
+ try:
2733
+ return JsonObjectAdapter.validate_python(payload[field_name])
2734
+ except PydanticValidationError as exc:
2735
+ raise ValueError(f"fix-wiki FSM payload invalid: {field_name} must be an object") from exc
2736
+
2737
+
2738
+ def _optional_json_object_subset(payload: JsonObject, field_name: str, keys: tuple[str, ...]) -> JsonObject:
2739
+ if field_name not in payload:
2740
+ return {}
2741
+ return _json_object_subset(payload, field_name, keys)
2742
+
2743
+
2744
+ def _related_current(recovery_state: RelatedNotesRecoveryState) -> int:
2745
+ return recovery_state.fresh_record_count or recovery_state.partial_record_count
2746
+
2747
+
2748
+ def _related_total(recovery_state: RelatedNotesRecoveryState) -> int:
2749
+ return recovery_state.total_note_count
2750
+
2751
+
2752
+ def _related_remaining(recovery_state: RelatedNotesRecoveryState, *, current: int, total: int) -> int:
2753
+ remaining = recovery_state.remaining_count
2754
+ if remaining:
2755
+ return min(remaining, total) if total else remaining
2756
+ return max(0, total - current)
2757
+
2758
+
2759
+ def _blocked_item_count(facts: FixWikiFsmFacts) -> int:
2760
+ return (
2761
+ facts.graph_blocker_count
2762
+ + int(facts.linker_blocked)
2763
+ + int(facts.related_notes_blocked)
2764
+ + int(facts.taxonomy_action_required)
2765
+ + int(facts.atomicity_split_required)
2766
+ + int(facts.graph_review_required)
2767
+ + facts.requires_llm_rewrite_count
2768
+ )