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,1119 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, cast
4
+
5
+ from pydantic import ConfigDict, Field, StrictStr, field_validator, model_validator
6
+ from pydantic import ValidationError as PydanticValidationError
7
+ from pydantic.json_schema import SkipJsonSchema
8
+
9
+ from mednotes.domains.wiki.contracts.workflow_outcomes import WorkflowDecision
10
+ from mednotes.domains.wiki.flows.link.link_machine import (
11
+ LINK_BODY_WORKFLOW,
12
+ LINK_PUBLIC_WORKFLOWS,
13
+ LinkBoundaryEvent,
14
+ LinkMachine,
15
+ LinkMode,
16
+ category_for_link_state,
17
+ )
18
+ from mednotes.domains.wiki.flows.link.link_machine import (
19
+ LinkState as MachineLinkState,
20
+ )
21
+ from mednotes.kernel.agent_directive import (
22
+ AgentDirective,
23
+ agent_directive_from_progress_view_model,
24
+ assert_agent_directive_matches_progress,
25
+ )
26
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
27
+ from mednotes.kernel.effects import WorkflowEffectKind
28
+ from mednotes.kernel.errors import EXIT_IO, EXIT_MISSING, EXIT_OK, EXIT_USAGE, EXIT_VALIDATION
29
+ from mednotes.kernel.fsm_event import WorkflowEventLike
30
+ from mednotes.kernel.fsm_model import WorkflowModel
31
+ from mednotes.kernel.fsm_transition_result import WorkflowTransitionResult
32
+ from mednotes.kernel.progress import (
33
+ WorkflowProgressCounts,
34
+ WorkflowProgressEventType,
35
+ WorkflowProgressState,
36
+ WorkflowProgressStatus,
37
+ WorkflowProgressViewModel,
38
+ build_progress_view_model,
39
+ progress_state_from_view_model,
40
+ )
41
+ from mednotes.kernel.public_report import (
42
+ WorkflowPrimaryObjectiveSummary,
43
+ WorkflowPublicReport,
44
+ WorkflowReports,
45
+ assert_public_report_matches_progress,
46
+ public_progress_followup_line,
47
+ )
48
+ from mednotes.kernel.state_machine import (
49
+ WorkflowStateCategory,
50
+ WorkflowStateMachineSnapshot,
51
+ WorkflowTransition,
52
+ send_workflow_event,
53
+ )
54
+ from mednotes.kernel.workflow import (
55
+ HumanDecisionPacket,
56
+ ReceiptStatus,
57
+ VersionControlSafety,
58
+ WorkflowReceiptPayload,
59
+ assert_diagnostic_context_evidence_only,
60
+ diagnostic_context_evidence_only,
61
+ )
62
+
63
+ LINK_WORKFLOW = "/mednotes:link"
64
+ LINK_BODY_PUBLIC_WORKFLOW = LINK_BODY_WORKFLOW
65
+
66
+
67
+ class _MachineAuditEvidence(ContractModel):
68
+ """Typed event evidence consumed by parent workflow FSMs."""
69
+
70
+ model_config = ConfigDict(extra="ignore")
71
+
72
+ adapter_schema: str = ""
73
+ adapter_phase: str = ""
74
+ adapter_status: str = ""
75
+ adapter_reason: str = ""
76
+ operation: JsonObject = Field(default_factory=dict)
77
+ mode: str = ""
78
+ include_related_notes: bool = False
79
+ counts: JsonObject = Field(default_factory=dict)
80
+ required_inputs: list[str] = Field(default_factory=list)
81
+ related_notes_recovery_state: JsonObject = Field(default_factory=dict)
82
+ stale_reason: str = ""
83
+ expected_git_status_hash: str = ""
84
+ actual_git_status_hash: str = ""
85
+ expected_git_head: str = ""
86
+ actual_git_head: str = ""
87
+
88
+ @field_validator("operation", mode="before")
89
+ @classmethod
90
+ def _coerce_operation(cls, value: object) -> JsonObject:
91
+ if not isinstance(value, dict):
92
+ return {}
93
+ return JsonObjectAdapter.validate_python(value)
94
+
95
+ @field_validator("counts", "related_notes_recovery_state", mode="before")
96
+ @classmethod
97
+ def _coerce_json_object(cls, value: object) -> JsonObject:
98
+ if not isinstance(value, dict):
99
+ return {}
100
+ return JsonObjectAdapter.validate_python(value)
101
+
102
+ def to_payload(self) -> JsonObject:
103
+ return JsonObjectAdapter.validate_python(self.model_dump(mode="json"))
104
+
105
+
106
+ class _MachineEventEvidence(ContractModel):
107
+ """Typed lens over persisted machine event evidence."""
108
+
109
+ model_config = ConfigDict(extra="ignore")
110
+
111
+ audit_evidence: _MachineAuditEvidence = Field(default_factory=_MachineAuditEvidence)
112
+ LINK_SCHEMA = "medical-notes-workbench.link-fsm-result.v1"
113
+ LINK_RECEIPT_SCHEMA = "medical-notes-workbench.link-receipt.v1"
114
+ LINK_AGENT_DIRECTIVE_FIELD = "agent_directive"
115
+
116
+ LINK_ALLOWED_ROOT_KEYS = frozenset(
117
+ {
118
+ "schema",
119
+ "workflow",
120
+ "run_id",
121
+ "state_machine_snapshot",
122
+ "progress_view_model",
123
+ "decision",
124
+ "human_decision_packet",
125
+ "receipt",
126
+ "reports",
127
+ "agent_directive",
128
+ "artifacts",
129
+ "version_control_safety",
130
+ "diagnostic_context",
131
+ "error_context",
132
+ }
133
+ )
134
+ LINK_FORBIDDEN_ROOT_KEYS = frozenset(
135
+ {
136
+ "status",
137
+ "phase",
138
+ "blocked_reason",
139
+ "next_action",
140
+ "required_inputs",
141
+ "human_decision_required",
142
+ "returncode",
143
+ "workflow_exit_code",
144
+ "body_term_linker",
145
+ "related_notes_sync",
146
+ "reference_repair",
147
+ "graph_audit_after",
148
+ "vocabulary_bootstrap",
149
+ "vocabulary_curator_batch_plan",
150
+ }
151
+ )
152
+
153
+ def category_for_state(state: str) -> WorkflowStateCategory:
154
+ """Map link leaf states through the canonical LinkMachine enum only."""
155
+
156
+ return category_for_link_state(MachineLinkState(state))
157
+
158
+
159
+ class LinkFsmFacts(ContractModel):
160
+ workflow: Literal["/mednotes:link", "/mednotes:link-body"] = LINK_WORKFLOW
161
+ run_id: str = Field(min_length=1)
162
+ mode: LinkMode = LinkMode.FULL
163
+ initial_state: MachineLinkState
164
+ event: LinkBoundaryEvent
165
+ changed_files: list[str] = Field(default_factory=list)
166
+ mutated: bool = False
167
+ artifacts: JsonObject = Field(default_factory=dict)
168
+ version_control_safety: VersionControlSafety
169
+ error_context: JsonObject = Field(default_factory=dict)
170
+
171
+ @model_validator(mode="after")
172
+ def _event_must_match_fsm_entry(self) -> LinkFsmFacts:
173
+ if self.event.workflow != self.workflow:
174
+ raise ValueError("link event workflow must match LinkFsmFacts.workflow")
175
+ if self.event.run_id != self.run_id:
176
+ raise ValueError("link event run_id must match LinkFsmFacts.run_id")
177
+ if self.event.current_state != self.initial_state.value:
178
+ raise ValueError("link event current_state must match initial_state")
179
+ if self.workflow == LINK_BODY_PUBLIC_WORKFLOW and self.mode != LinkMode.BODY_ONLY:
180
+ raise ValueError("link-body facts require body_only mode")
181
+ if self.workflow == LINK_WORKFLOW and self.mode != LinkMode.FULL:
182
+ raise ValueError("full link facts require full mode")
183
+ return self
184
+
185
+
186
+ class _LinkPayloadProgressViewFields(ContractModel):
187
+ status: StrictStr
188
+
189
+
190
+ class _LinkPayloadSnapshotFields(ContractModel):
191
+ current_category: StrictStr
192
+
193
+
194
+ class _LinkPayloadReceiptFields(ContractModel):
195
+ status: StrictStr
196
+
197
+
198
+ class _LinkPayloadFields(ContractModel):
199
+ workflow: StrictStr
200
+ progress_view_model: _LinkPayloadProgressViewFields
201
+ state_machine_snapshot: _LinkPayloadSnapshotFields
202
+ receipt: _LinkPayloadReceiptFields
203
+
204
+
205
+ class _LinkCliExitCodeFields(ContractModel):
206
+ progress_view_model: _LinkPayloadProgressViewFields
207
+
208
+
209
+ class _LinkErrorContextFields(ContractModel):
210
+ missing_inputs: list[StrictStr] = Field(default_factory=list)
211
+
212
+
213
+ class LinkFsmResult(ContractModel):
214
+ schema_id: Literal["medical-notes-workbench.link-fsm-result.v1"] = Field(default=LINK_SCHEMA, alias="schema")
215
+ workflow: Literal["/mednotes:link", "/mednotes:link-body"] = LINK_WORKFLOW
216
+ run_id: str = Field(min_length=1)
217
+ progress_state: SkipJsonSchema[WorkflowProgressState]
218
+ progress_view_model: WorkflowProgressViewModel
219
+ state_machine_snapshot: WorkflowStateMachineSnapshot
220
+ decision: WorkflowDecision | None = None
221
+ human_decision_packet: HumanDecisionPacket | None = None
222
+ receipt: WorkflowReceiptPayload
223
+ reports: WorkflowReports
224
+ agent_directive: JsonObject
225
+ artifacts: JsonObject = Field(default_factory=dict)
226
+ version_control_safety: VersionControlSafety
227
+ diagnostic_context: JsonObject = Field(default_factory=dict)
228
+ error_context: JsonObject = Field(default_factory=dict)
229
+
230
+ @model_validator(mode="before")
231
+ @classmethod
232
+ def _hydrate_progress_state_from_public_payload(cls, value: object) -> object:
233
+ """Accept public payloads where progress_state is intentionally hidden."""
234
+
235
+ if not isinstance(value, dict) or "progress_state" in value or "progress_view_model" not in value:
236
+ return value
237
+ hydrated = dict(value)
238
+ progress_view = WorkflowProgressViewModel.model_validate(value["progress_view_model"])
239
+ hydrated["progress_state"] = progress_state_from_view_model(progress_view).to_payload()
240
+ return hydrated
241
+
242
+ @model_validator(mode="after")
243
+ def _progress_view_model_matches_state(self) -> LinkFsmResult:
244
+ expected = build_progress_view_model(self.progress_state).to_payload()
245
+ if self.progress_view_model.to_payload() != expected:
246
+ raise ValueError("progress_view_model must match progress_state")
247
+ return self
248
+
249
+ def to_payload(self) -> JsonObject:
250
+ payload: JsonObject = {
251
+ "schema": self.schema_id,
252
+ "workflow": self.workflow,
253
+ "run_id": self.run_id,
254
+ "state_machine_snapshot": self.state_machine_snapshot.to_payload(),
255
+ "progress_view_model": self.progress_view_model.to_payload(),
256
+ "decision": self.decision.to_payload() if self.decision is not None else None,
257
+ "human_decision_packet": self.human_decision_packet.to_payload()
258
+ if self.human_decision_packet is not None
259
+ else None,
260
+ "receipt": self.receipt.to_payload(),
261
+ "reports": self.reports.to_payload(),
262
+ "agent_directive": dict(self.agent_directive),
263
+ "artifacts": dict(self.artifacts),
264
+ "version_control_safety": self.version_control_safety.to_payload(),
265
+ "error_context": dict(self.error_context),
266
+ }
267
+ if self.diagnostic_context:
268
+ payload["diagnostic_context"] = dict(self.diagnostic_context)
269
+ payload = JsonObjectAdapter.validate_python(payload)
270
+ assert_link_fsm_payload(payload)
271
+ return payload
272
+
273
+
274
+ def build_link_fsm_result(facts: LinkFsmFacts) -> LinkFsmResult:
275
+ """Project one typed LinkMachine event into the public link FSM payload."""
276
+
277
+ return build_link_fsm_result_from_model(
278
+ _link_model_after_event(facts.initial_state, facts.event),
279
+ version_control_safety=facts.version_control_safety,
280
+ error_context=facts.error_context,
281
+ artifacts=facts.artifacts,
282
+ changed_files=facts.changed_files,
283
+ mutated=facts.mutated,
284
+ )
285
+
286
+
287
+ def _link_model_after_event(initial_state: MachineLinkState, event: WorkflowEventLike) -> WorkflowModel:
288
+ model = WorkflowModel.start(
289
+ workflow=event.workflow,
290
+ run_id=event.run_id,
291
+ initial_state=initial_state.value,
292
+ )
293
+ send_workflow_event(
294
+ LinkMachine(model=model, state_field=WorkflowModel.STATECHART_STATE_FIELD),
295
+ event,
296
+ )
297
+ return model
298
+
299
+
300
+ def build_link_fsm_result_from_model(
301
+ model: WorkflowModel,
302
+ *,
303
+ version_control_safety: VersionControlSafety | dict[str, object],
304
+ error_context: JsonObject | None = None,
305
+ artifacts: JsonObject | None = None,
306
+ changed_files: list[str] | None = None,
307
+ mutated: bool | None = None,
308
+ ) -> LinkFsmResult:
309
+ """Project a real LinkMachine model without reading adapter reports."""
310
+
311
+ _validate_link_machine_model(model)
312
+ state = MachineLinkState(model.state)
313
+ category = category_for_link_state(state)
314
+ progress_state = _progress_state_from_model(model, state, category)
315
+ progress_view_model = build_progress_view_model(progress_state)
316
+ snapshot = _snapshot_from_model(model, state, category)
317
+ safety = _version_control_safety(version_control_safety)
318
+ receipt = _receipt_from_model(
319
+ model,
320
+ progress_state=progress_state,
321
+ progress_view_model=progress_view_model,
322
+ snapshot=snapshot,
323
+ version_control_safety=safety,
324
+ changed_files=changed_files or [],
325
+ mutated=mutated,
326
+ )
327
+ reports_model = _reports_from_model(model, state, progress_state)
328
+ public_report = reports_model.public_report
329
+ diagnostic_context = _diagnostic_context_from_model(model, state, category)
330
+ agent_directive = agent_directive_from_progress_view_model(
331
+ progress_view_model,
332
+ schema="medical-notes-workbench.agent-directive.v1",
333
+ reason=_machine_reason_code(model, state),
334
+ effects=model.pending_effects,
335
+ blockers=_machine_blockers(category, model, state),
336
+ resume=progress_state.resume_action,
337
+ report_requires=["graph", "body_links", "related_notes"],
338
+ summary=public_report.summary_text(),
339
+ instructions=_machine_agent_instructions(category),
340
+ ).to_payload()
341
+ machine_error_context = error_context or _error_context_from_model(model, state, category)
342
+ return LinkFsmResult(
343
+ workflow=cast(Literal["/mednotes:link", "/mednotes:link-body"], model.workflow),
344
+ run_id=model.run_id,
345
+ progress_state=progress_state,
346
+ progress_view_model=progress_view_model,
347
+ state_machine_snapshot=snapshot,
348
+ decision=model.last_transition.decision if model.last_transition is not None else None,
349
+ human_decision_packet=model.last_transition.human_decision_packet if model.last_transition is not None else None,
350
+ receipt=receipt,
351
+ reports=reports_model,
352
+ agent_directive=JsonObjectAdapter.validate_python(agent_directive),
353
+ artifacts=artifacts or {},
354
+ version_control_safety=safety,
355
+ diagnostic_context=diagnostic_context,
356
+ error_context=machine_error_context,
357
+ )
358
+
359
+
360
+ def link_fsm_payload_from_model(
361
+ model: WorkflowModel,
362
+ *,
363
+ version_control_safety: VersionControlSafety | dict[str, object],
364
+ ) -> JsonObject:
365
+ """JSON boundary for the machine-driven link FSM projection."""
366
+
367
+ return build_link_fsm_result_from_model(model, version_control_safety=version_control_safety).to_payload()
368
+
369
+
370
+ def _validate_link_machine_model(model: WorkflowModel) -> None:
371
+ if model.workflow not in LINK_PUBLIC_WORKFLOWS:
372
+ raise ValueError(f"link FSM projector requires workflow in {sorted(LINK_PUBLIC_WORKFLOWS)}")
373
+ MachineLinkState(model.state)
374
+
375
+
376
+ def _progress_state_from_model(
377
+ model: WorkflowModel,
378
+ state: MachineLinkState,
379
+ category: WorkflowStateCategory,
380
+ ) -> WorkflowProgressState:
381
+ status = _machine_progress_status(category)
382
+ current, total, counts = _machine_progress_numbers(model, state, status)
383
+ return WorkflowProgressState(
384
+ workflow=model.workflow,
385
+ run_id=model.run_id,
386
+ state=state.value,
387
+ phase=_machine_phase_for_state(state),
388
+ event_type=_machine_event_type(status),
389
+ message=_machine_message_for_state(state),
390
+ status=status,
391
+ current=current,
392
+ total=total,
393
+ counts=counts,
394
+ resume_action=_machine_resume_action(model, state),
395
+ resume_supported=status
396
+ in {
397
+ WorkflowProgressStatus.WAITING_AGENT,
398
+ WorkflowProgressStatus.WAITING_EXTERNAL,
399
+ WorkflowProgressStatus.WAITING_HUMAN,
400
+ WorkflowProgressStatus.BLOCKED,
401
+ },
402
+ can_continue_now=status
403
+ in {
404
+ WorkflowProgressStatus.RUNNING,
405
+ WorkflowProgressStatus.WAITING_AGENT,
406
+ },
407
+ decision=model.last_transition.decision.decision_summary()
408
+ if model.last_transition is not None and model.last_transition.decision is not None
409
+ else None,
410
+ technical_context={
411
+ "reason": _machine_reason_code(model, state),
412
+ "category": category.value,
413
+ "source": "LinkMachine",
414
+ },
415
+ )
416
+
417
+
418
+ def _machine_progress_numbers(
419
+ model: WorkflowModel,
420
+ state: MachineLinkState,
421
+ status: WorkflowProgressStatus,
422
+ ) -> tuple[int, int, WorkflowProgressCounts]:
423
+ changed = _machine_event_int(model, "changed_file_count") or _machine_audit_count(model, "files_changed")
424
+ planned = max(_machine_event_int(model, "planned_link_count"), _machine_audit_count(model, "links_planned"))
425
+ rewritten = _machine_audit_count(model, "links_rewritten")
426
+ blocker_count = max(_machine_event_int(model, "blocker_count"), _machine_audit_count(model, "blocker_count"))
427
+ fresh = _machine_audit_count(model, "fresh_record_count")
428
+ remaining = _machine_audit_count(model, "remaining_count")
429
+ total_notes = _machine_audit_count(model, "total_note_count")
430
+ cache_hits = _machine_audit_count(model, "reused_count")
431
+ api_calls = _machine_audit_count(model, "embedded_count")
432
+
433
+ if state == MachineLinkState.WAITING_EXTERNAL_RELATED_NOTES_QUOTA:
434
+ total = total_notes or fresh + remaining
435
+ return (
436
+ fresh,
437
+ total,
438
+ WorkflowProgressCounts(
439
+ planned_items=total,
440
+ processed_items=fresh,
441
+ cache_hits=cache_hits,
442
+ api_calls=api_calls,
443
+ remaining_items=remaining,
444
+ blocked_items=remaining,
445
+ deferred_items=remaining,
446
+ mutated_files=changed,
447
+ written_files=changed,
448
+ ),
449
+ )
450
+ if state == MachineLinkState.COMPLETED:
451
+ total = max(changed, rewritten, planned)
452
+ return (
453
+ total,
454
+ total,
455
+ WorkflowProgressCounts(
456
+ planned_items=total,
457
+ processed_items=total,
458
+ mutated_files=changed,
459
+ written_files=changed,
460
+ ),
461
+ )
462
+ if state == MachineLinkState.COMPLETED_WITH_LINK_BLOCKERS:
463
+ blocked = max(blocker_count, 1)
464
+ return (
465
+ 0,
466
+ blocked,
467
+ WorkflowProgressCounts(
468
+ planned_items=max(planned, blocked),
469
+ warnings=blocked,
470
+ remaining_items=blocked,
471
+ blocked_items=blocked,
472
+ mutated_files=changed,
473
+ written_files=changed,
474
+ ),
475
+ )
476
+ if state == MachineLinkState.WAITING_HUMAN_CONFIRMATION:
477
+ total = max(planned, rewritten)
478
+ return (
479
+ 0,
480
+ total,
481
+ WorkflowProgressCounts(
482
+ planned_items=total,
483
+ remaining_items=total,
484
+ blocked_items=total,
485
+ ),
486
+ )
487
+ if status in {WorkflowProgressStatus.WAITING_AGENT, WorkflowProgressStatus.BLOCKED, WorkflowProgressStatus.FAILED}:
488
+ blocked = max(blocker_count, remaining, 1)
489
+ return (
490
+ 0,
491
+ blocked,
492
+ WorkflowProgressCounts(
493
+ planned_items=max(planned, blocked),
494
+ remaining_items=blocked,
495
+ blocked_items=blocked,
496
+ mutated_files=changed,
497
+ written_files=changed,
498
+ ),
499
+ )
500
+ total = max(planned, rewritten)
501
+ return 0, total, WorkflowProgressCounts(planned_items=total)
502
+
503
+
504
+ def _machine_event_int(model: WorkflowModel, field_name: str) -> int:
505
+ if not model.event_log:
506
+ return 0
507
+ event = model.event_log[-1]
508
+ value = event[field_name] if field_name in event else 0
509
+ if isinstance(value, bool):
510
+ return 0
511
+ if isinstance(value, int) and value > 0:
512
+ return value
513
+ return 0
514
+
515
+
516
+ def _machine_audit_count(model: WorkflowModel, field_name: str) -> int:
517
+ evidence = _machine_audit_evidence(model)
518
+ try:
519
+ raw_counts = evidence["counts"]
520
+ except KeyError:
521
+ return 0
522
+ if not isinstance(raw_counts, dict):
523
+ return 0
524
+ try:
525
+ value = raw_counts[field_name]
526
+ except KeyError:
527
+ return 0
528
+ if isinstance(value, bool):
529
+ return 0
530
+ if isinstance(value, int) and value > 0:
531
+ return value
532
+ return 0
533
+
534
+
535
+ def _snapshot_from_model(
536
+ model: WorkflowModel,
537
+ state: MachineLinkState,
538
+ category: WorkflowStateCategory,
539
+ ) -> WorkflowStateMachineSnapshot:
540
+ return WorkflowStateMachineSnapshot(
541
+ workflow=model.workflow,
542
+ run_id=model.run_id,
543
+ current_state=state.value,
544
+ current_category=category,
545
+ transitions=[_machine_snapshot_transition(transition) for transition in model.transition_log],
546
+ metadata={
547
+ "reason": _machine_reason_code(model, state),
548
+ "source": "LinkMachine",
549
+ "link_mode": _link_mode_for_model(model).value,
550
+ },
551
+ )
552
+
553
+
554
+ def _link_mode_for_model(model: WorkflowModel) -> LinkMode:
555
+ """Recover the invariant execution mode from the observed event or workflow."""
556
+
557
+ if model.workflow == LINK_BODY_PUBLIC_WORKFLOW:
558
+ return LinkMode.BODY_ONLY
559
+ for raw_event in reversed(model.event_log):
560
+ try:
561
+ observation = raw_event["observation"]
562
+ except KeyError:
563
+ continue
564
+ if not isinstance(observation, dict):
565
+ continue
566
+ try:
567
+ raw_mode = observation["mode"]
568
+ except KeyError:
569
+ continue
570
+ if isinstance(raw_mode, str) and raw_mode:
571
+ return LinkMode(raw_mode)
572
+ return LinkMode.FULL
573
+
574
+
575
+ def _machine_snapshot_transition(transition: WorkflowTransitionResult) -> WorkflowTransition:
576
+ return WorkflowTransition(
577
+ workflow=transition.workflow,
578
+ from_state=transition.from_state,
579
+ to_state=transition.to_state,
580
+ to_category=category_for_link_state(MachineLinkState(transition.to_state)),
581
+ trigger=transition.trigger,
582
+ effects=list(transition.effects),
583
+ decision=transition.decision,
584
+ resume_action=transition.resume_action,
585
+ )
586
+
587
+
588
+ def _receipt_from_model(
589
+ model: WorkflowModel,
590
+ *,
591
+ progress_state: WorkflowProgressState,
592
+ progress_view_model: WorkflowProgressViewModel,
593
+ snapshot: WorkflowStateMachineSnapshot,
594
+ version_control_safety: VersionControlSafety,
595
+ changed_files: list[str],
596
+ mutated: bool | None,
597
+ ) -> WorkflowReceiptPayload:
598
+ return WorkflowReceiptPayload(
599
+ schema=LINK_RECEIPT_SCHEMA,
600
+ workflow=model.workflow,
601
+ run_id=model.run_id,
602
+ status=_machine_receipt_status(progress_state.status),
603
+ mutated=mutated if mutated is not None else version_control_safety.changed_file_count > 0,
604
+ next_action="" if progress_state.status == WorkflowProgressStatus.COMPLETED else progress_state.resume_action,
605
+ human_decision_required=progress_state.status == WorkflowProgressStatus.WAITING_HUMAN,
606
+ human_decision_packet=model.last_transition.human_decision_packet if model.last_transition is not None else None,
607
+ changed_files=changed_files,
608
+ version_control_safety=version_control_safety,
609
+ progress_state=progress_state,
610
+ progress_view_model=progress_view_model,
611
+ state_machine_snapshot=snapshot,
612
+ )
613
+
614
+
615
+ def _reports_from_model(
616
+ model: WorkflowModel,
617
+ state: MachineLinkState,
618
+ progress_state: WorkflowProgressState,
619
+ ) -> WorkflowReports:
620
+ summary = _machine_message_for_state(state)
621
+ public_lines = [summary]
622
+ followup_line = public_progress_followup_line(progress_state)
623
+ if followup_line:
624
+ public_lines.append(followup_line)
625
+ public_report = WorkflowPublicReport(
626
+ workflow=progress_state.workflow,
627
+ run_id=model.run_id,
628
+ headline=summary,
629
+ lines=public_lines,
630
+ )
631
+ details: JsonObject = {
632
+ "primary_objective_summary": _primary_objective_summary(
633
+ run_id=model.run_id,
634
+ workflow=progress_state.workflow,
635
+ state=state,
636
+ progress_state=progress_state,
637
+ ).to_payload()
638
+ }
639
+ operation_details = _operation_details_from_model(model)
640
+ if operation_details:
641
+ details.update(operation_details)
642
+ return WorkflowReports(
643
+ summary=summary,
644
+ public_report=public_report,
645
+ details=details,
646
+ )
647
+
648
+
649
+ def _operation_details_from_model(model: WorkflowModel) -> JsonObject:
650
+ """Expose typed child-operation evidence for parent FSMs without root state."""
651
+
652
+ for event in reversed(model.event_log):
653
+ event_evidence = _MachineEventEvidence.model_validate(event)
654
+ if event_evidence.audit_evidence.operation:
655
+ return event_evidence.audit_evidence.operation
656
+ return {}
657
+
658
+
659
+ def _primary_objective_summary(
660
+ *,
661
+ run_id: str,
662
+ workflow: str,
663
+ state: MachineLinkState,
664
+ progress_state: WorkflowProgressState,
665
+ ) -> WorkflowPrimaryObjectiveSummary:
666
+ """State-owned answer to whether `/mednotes:link` completed its job."""
667
+
668
+ completed = state == MachineLinkState.COMPLETED
669
+ changed_count = max(progress_state.counts.mutated_files, progress_state.counts.written_files)
670
+ return WorkflowPrimaryObjectiveSummary(
671
+ workflow=workflow,
672
+ run_id=run_id,
673
+ objective=_link_objective_for_workflow(workflow),
674
+ completed=completed,
675
+ status=state.value,
676
+ mutation_state="changed" if changed_count > 0 else "unchanged",
677
+ mutation_summary=_link_mutation_summary(changed_count),
678
+ remaining_work_summary=_link_remaining_work_summary(state, completed),
679
+ next_step_summary=_link_next_step_summary(progress_state, completed),
680
+ blocked_reason="" if completed else state.value,
681
+ )
682
+
683
+
684
+ def _link_mutation_summary(changed_count: int) -> str:
685
+ if changed_count > 0:
686
+ return f"{changed_count} arquivo(s) de links foram alterados."
687
+ return "Nenhum arquivo de links foi alterado nesta etapa."
688
+
689
+
690
+ def _link_objective_for_workflow(workflow: str) -> str:
691
+ if workflow == LINK_BODY_PUBLIC_WORKFLOW:
692
+ return "Atualizar somente WikiLinks no corpo das notas, sem Notas Relacionadas."
693
+ return "Atualizar grafo, links de corpo e Notas Relacionadas quando aplicável."
694
+
695
+
696
+ def _link_remaining_work_summary(state: MachineLinkState, completed: bool) -> str:
697
+ if completed:
698
+ return "Grafo, links de corpo e Notas Relacionadas ficaram concluídos."
699
+ if state == MachineLinkState.COMPLETED_WITH_LINK_BLOCKERS:
700
+ return "O link terminou com pendências explícitas de grafo ou Notas Relacionadas."
701
+ return _machine_message_for_state(state)
702
+
703
+
704
+ def _link_next_step_summary(progress_state: WorkflowProgressState, completed: bool) -> str:
705
+ if completed:
706
+ return "Nenhuma ação pendente para o pacote de links."
707
+ return progress_state.resume_action or "Retomar /mednotes:link pela rota oficial."
708
+
709
+
710
+ def _diagnostic_context_from_model(
711
+ model: WorkflowModel,
712
+ state: MachineLinkState,
713
+ category: WorkflowStateCategory,
714
+ ) -> JsonObject:
715
+ if category in {WorkflowStateCategory.COMPLETED, WorkflowStateCategory.COMPLETED_WITH_WARNINGS}:
716
+ return {}
717
+ context: JsonObject = {
718
+ "schema": "medical-notes-workbench.link-fsm-diagnostic-context.v2",
719
+ "state": state.value,
720
+ "category": category.value,
721
+ "reason": _machine_reason_code(model, state),
722
+ "source": "LinkMachine",
723
+ }
724
+ evidence = _machine_audit_evidence(model)
725
+ for key, value in evidence.items():
726
+ if key not in context:
727
+ context[key] = value
728
+ return diagnostic_context_evidence_only(context)
729
+
730
+
731
+ def _machine_audit_evidence(model: WorkflowModel) -> JsonObject:
732
+ if not model.event_log:
733
+ return {}
734
+ event = _MachineEventEvidence.model_validate(model.event_log[-1])
735
+ return event.audit_evidence.to_payload()
736
+
737
+
738
+ def _machine_progress_status(category: WorkflowStateCategory) -> WorkflowProgressStatus:
739
+ match category:
740
+ case WorkflowStateCategory.PREPARING | WorkflowStateCategory.RUNNING:
741
+ return WorkflowProgressStatus.RUNNING
742
+ case WorkflowStateCategory.WAITING_AGENT:
743
+ return WorkflowProgressStatus.WAITING_AGENT
744
+ case WorkflowStateCategory.WAITING_EXTERNAL:
745
+ return WorkflowProgressStatus.WAITING_EXTERNAL
746
+ case WorkflowStateCategory.WAITING_HUMAN:
747
+ return WorkflowProgressStatus.WAITING_HUMAN
748
+ case WorkflowStateCategory.BLOCKED:
749
+ return WorkflowProgressStatus.BLOCKED
750
+ case WorkflowStateCategory.FAILED:
751
+ return WorkflowProgressStatus.FAILED
752
+ case WorkflowStateCategory.COMPLETED:
753
+ return WorkflowProgressStatus.COMPLETED
754
+ case WorkflowStateCategory.COMPLETED_WITH_WARNINGS:
755
+ return WorkflowProgressStatus.COMPLETED_WITH_WARNINGS
756
+
757
+
758
+ def _machine_receipt_status(status: WorkflowProgressStatus) -> ReceiptStatus:
759
+ match status:
760
+ case WorkflowProgressStatus.RUNNING:
761
+ return "running"
762
+ case WorkflowProgressStatus.COMPLETED:
763
+ return "completed"
764
+ case WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
765
+ return "completed_with_warnings"
766
+ case WorkflowProgressStatus.WAITING_AGENT:
767
+ return "waiting_agent"
768
+ case WorkflowProgressStatus.WAITING_EXTERNAL:
769
+ return "waiting_external"
770
+ case WorkflowProgressStatus.WAITING_HUMAN:
771
+ return "waiting_human"
772
+ case WorkflowProgressStatus.FAILED:
773
+ return "failed"
774
+ case WorkflowProgressStatus.BLOCKED:
775
+ return "blocked"
776
+ case _:
777
+ return "blocked"
778
+
779
+
780
+ def _machine_event_type(status: WorkflowProgressStatus) -> WorkflowProgressEventType:
781
+ match status:
782
+ case WorkflowProgressStatus.COMPLETED | WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
783
+ return WorkflowProgressEventType.WORKFLOW_COMPLETED
784
+ case WorkflowProgressStatus.FAILED:
785
+ return WorkflowProgressEventType.WORKFLOW_FAILED
786
+ case WorkflowProgressStatus.WAITING_EXTERNAL:
787
+ return WorkflowProgressEventType.EXTERNAL_WAIT_STARTED
788
+ case WorkflowProgressStatus.WAITING_HUMAN:
789
+ return WorkflowProgressEventType.DECISION_EMITTED
790
+ case _:
791
+ return WorkflowProgressEventType.STATE_ENTERED
792
+
793
+
794
+ def _machine_phase_for_state(state: MachineLinkState) -> str:
795
+ match state:
796
+ case MachineLinkState.CHECKING_TRIGGER_CONTEXT:
797
+ return "trigger_context"
798
+ case MachineLinkState.DIAGNOSING_GRAPH | MachineLinkState.STALE_DIAGNOSIS:
799
+ return "diagnosis"
800
+ case MachineLinkState.VOCABULARY_BOOTSTRAP_REQUIRED:
801
+ return "vocabulary_bootstrap"
802
+ case MachineLinkState.PLANNING_BODY_LINKS | MachineLinkState.APPLYING_BODY_LINKS:
803
+ return "body_links"
804
+ case MachineLinkState.PLANNING_RELATED_NOTES | MachineLinkState.APPLYING_RELATED_NOTES:
805
+ return "related_notes"
806
+ case (
807
+ MachineLinkState.PLANNING_VOCABULARY_SEMANTIC_REPAIR
808
+ | MachineLinkState.APPLYING_VOCABULARY_SEMANTIC_REPAIR
809
+ ):
810
+ return "vocabulary_semantic_repair"
811
+ case MachineLinkState.WAITING_AGENT_DISAMBIGUATION:
812
+ return "agent_disambiguation"
813
+ case MachineLinkState.WAITING_AGENT_RELATED_NOTES_EXPORT_RECOVERY:
814
+ return "related_notes_export_recovery"
815
+ case MachineLinkState.WAITING_AGENT_VOCABULARY_CURATOR:
816
+ return "vocabulary_curator"
817
+ case MachineLinkState.WAITING_EXTERNAL_RELATED_NOTES_QUOTA:
818
+ return "related_notes_recovery"
819
+ case MachineLinkState.WAITING_HUMAN_CONFIRMATION:
820
+ return "human_confirmation"
821
+ case MachineLinkState.COMPLETED | MachineLinkState.COMPLETED_WITH_LINK_BLOCKERS:
822
+ return "completed"
823
+ case MachineLinkState.APPLY_CANCELLED:
824
+ return "apply_cancelled"
825
+ case MachineLinkState.GRAPH_DIAGNOSIS_BLOCKED:
826
+ return "graph_diagnosis_blocked"
827
+ case MachineLinkState.BODY_LINKS_BLOCKED:
828
+ return "body_links_blocked"
829
+ case MachineLinkState.RELATED_NOTES_BLOCKED:
830
+ return "related_notes_blocked"
831
+ case MachineLinkState.VOCABULARY_SEMANTIC_REPAIR_BLOCKED:
832
+ return "vocabulary_semantic_repair_blocked"
833
+ case MachineLinkState.FAILED:
834
+ return "failed"
835
+
836
+
837
+ def _machine_message_for_state(state: MachineLinkState) -> str:
838
+ match state:
839
+ case MachineLinkState.STALE_DIAGNOSIS:
840
+ return "O diagnostico de links ficou desatualizado."
841
+ case MachineLinkState.VOCABULARY_BOOTSTRAP_REQUIRED:
842
+ return "O vocabulario precisa ser preparado antes dos links."
843
+ case MachineLinkState.WAITING_EXTERNAL_RELATED_NOTES_QUOTA:
844
+ return "Notas Relacionadas aguardam cota externa para continuar."
845
+ case MachineLinkState.WAITING_AGENT_RELATED_NOTES_EXPORT_RECOVERY:
846
+ return "O export do Related Notes precisa ser recuperado pela rota oficial."
847
+ case MachineLinkState.WAITING_HUMAN_CONFIRMATION:
848
+ return "Preciso de confirmacao antes de aplicar links."
849
+ case MachineLinkState.GRAPH_DIAGNOSIS_BLOCKED:
850
+ return "Diagnóstico do grafo de links bloqueado."
851
+ case MachineLinkState.BODY_LINKS_BLOCKED:
852
+ return "Planejamento ou aplicação dos links de corpo bloqueada."
853
+ case MachineLinkState.RELATED_NOTES_BLOCKED:
854
+ return "Atualização de Notas Relacionadas bloqueada."
855
+ case MachineLinkState.VOCABULARY_SEMANTIC_REPAIR_BLOCKED:
856
+ return "Reparo semântico do vocabulário bloqueado."
857
+ case MachineLinkState.COMPLETED:
858
+ return "Links atualizados e conferidos."
859
+ case MachineLinkState.COMPLETED_WITH_LINK_BLOCKERS:
860
+ return "Links concluidos com bloqueios pendentes."
861
+ case MachineLinkState.FAILED:
862
+ return "O workflow de links falhou antes de concluir."
863
+ case _:
864
+ return "Workflow de links em andamento."
865
+
866
+
867
+ def _machine_resume_action(model: WorkflowModel, state: MachineLinkState) -> str:
868
+ if state in {MachineLinkState.COMPLETED, MachineLinkState.COMPLETED_WITH_LINK_BLOCKERS}:
869
+ return ""
870
+ if model.last_transition is not None and model.last_transition.resume_action:
871
+ return model.last_transition.resume_action
872
+ match state:
873
+ case MachineLinkState.STALE_DIAGNOSIS:
874
+ return "link:diagnose"
875
+ case MachineLinkState.VOCABULARY_BOOTSTRAP_REQUIRED:
876
+ return "link:bootstrap-vocabulary"
877
+ case MachineLinkState.WAITING_AGENT_DISAMBIGUATION:
878
+ return "link:run-agent-disambiguation"
879
+ case MachineLinkState.WAITING_AGENT_RELATED_NOTES_EXPORT_RECOVERY:
880
+ return "link:recover-related-notes-export"
881
+ case MachineLinkState.WAITING_AGENT_VOCABULARY_CURATOR:
882
+ return "link:run-vocabulary-curator"
883
+ case MachineLinkState.WAITING_EXTERNAL_RELATED_NOTES_QUOTA:
884
+ return "link:retry-related-notes-export"
885
+ case MachineLinkState.WAITING_HUMAN_CONFIRMATION:
886
+ return "link:confirm-apply"
887
+ case MachineLinkState.APPLYING_BODY_LINKS:
888
+ return "link:apply-body-links"
889
+ case MachineLinkState.APPLYING_RELATED_NOTES:
890
+ return "link:apply-related-notes"
891
+ case MachineLinkState.APPLYING_VOCABULARY_SEMANTIC_REPAIR:
892
+ return "link:apply-vocabulary-semantic-repair"
893
+ case MachineLinkState.GRAPH_DIAGNOSIS_BLOCKED:
894
+ return "link:diagnose"
895
+ case MachineLinkState.BODY_LINKS_BLOCKED:
896
+ return "link:repair-body-links"
897
+ case MachineLinkState.RELATED_NOTES_BLOCKED:
898
+ return "link:repair-related-notes"
899
+ case MachineLinkState.VOCABULARY_SEMANTIC_REPAIR_BLOCKED:
900
+ return "link:repair-vocabulary-semantics"
901
+ case _:
902
+ return "link:diagnose"
903
+
904
+
905
+ def _machine_reason_code(model: WorkflowModel, state: MachineLinkState) -> str:
906
+ if model.last_transition is not None:
907
+ return model.last_transition.reason_code
908
+ return state.value
909
+
910
+
911
+ def _machine_blockers(
912
+ category: WorkflowStateCategory,
913
+ model: WorkflowModel,
914
+ state: MachineLinkState,
915
+ ) -> list[str]:
916
+ if category in {
917
+ WorkflowStateCategory.WAITING_AGENT,
918
+ WorkflowStateCategory.WAITING_EXTERNAL,
919
+ WorkflowStateCategory.WAITING_HUMAN,
920
+ WorkflowStateCategory.BLOCKED,
921
+ WorkflowStateCategory.FAILED,
922
+ }:
923
+ return [_machine_reason_code(model, state)]
924
+ return []
925
+
926
+
927
+ def _error_context_from_model(
928
+ model: WorkflowModel,
929
+ state: MachineLinkState,
930
+ category: WorkflowStateCategory,
931
+ ) -> JsonObject:
932
+ """Synthesize the minimal recovery context owned by the LinkMachine state."""
933
+
934
+ if category not in {WorkflowStateCategory.BLOCKED, WorkflowStateCategory.FAILED}:
935
+ return {}
936
+ reason = _machine_reason_code(model, state) or state.value
937
+ return JsonObjectAdapter.validate_python(
938
+ {
939
+ "blocked_reason": reason,
940
+ "root_cause": reason,
941
+ "affected_artifact": state.value,
942
+ "next_action": _machine_resume_action(model, state) or "link:diagnose",
943
+ "retry_scope": "link",
944
+ }
945
+ )
946
+
947
+
948
+ def _machine_agent_instructions(category: WorkflowStateCategory) -> list[str]:
949
+ if category == WorkflowStateCategory.WAITING_AGENT:
950
+ return ["Execute somente os efeitos em agent_directive.control.effects e retome /mednotes:link pelo resultado tipado."]
951
+ if category == WorkflowStateCategory.WAITING_EXTERNAL:
952
+ return ["Aguarde a condicao externa indicada antes de retomar /mednotes:link."]
953
+ if category == WorkflowStateCategory.WAITING_HUMAN:
954
+ return ["Peca a decisao humana fechada antes de aplicar links."]
955
+ if category in {WorkflowStateCategory.BLOCKED, WorkflowStateCategory.FAILED}:
956
+ return ["Use a decisao e o resume_action da FSM para recuperar o workflow de links."]
957
+ return ["Use a LinkMachine como fonte de verdade do estado de links."]
958
+
959
+
960
+ def _version_control_safety(value: VersionControlSafety | dict[str, object]) -> VersionControlSafety:
961
+ if isinstance(value, VersionControlSafety):
962
+ return value
963
+ return VersionControlSafety.model_validate(value)
964
+
965
+
966
+ def assert_link_fsm_payload(payload: JsonObject) -> None:
967
+ payload = JsonObjectAdapter.validate_python(payload)
968
+ legacy_keys = set(payload) & LINK_FORBIDDEN_ROOT_KEYS
969
+ if legacy_keys:
970
+ raise ValueError(f"link FSM payload contains adapter root fields: {sorted(legacy_keys)}")
971
+ required_root_keys = LINK_ALLOWED_ROOT_KEYS - {"diagnostic_context"}
972
+ missing_keys = required_root_keys - set(payload)
973
+ if missing_keys:
974
+ raise ValueError(f"link FSM payload missing canonical root fields: {sorted(missing_keys)}")
975
+ unexpected_keys = set(payload) - LINK_ALLOWED_ROOT_KEYS
976
+ if unexpected_keys:
977
+ raise ValueError(f"link FSM payload contains unexpected root fields: {sorted(unexpected_keys)}")
978
+ try:
979
+ diagnostic_context = payload["diagnostic_context"]
980
+ except KeyError:
981
+ diagnostic_context = {}
982
+ assert_diagnostic_context_evidence_only(diagnostic_context)
983
+ if isinstance(diagnostic_context, dict) and "agent_directive" in diagnostic_context:
984
+ raise ValueError("link FSM diagnostic_context must not contain agent_directive")
985
+ fields = _link_payload_fields(payload)
986
+ if fields.workflow not in LINK_PUBLIC_WORKFLOWS:
987
+ raise ValueError("link FSM payload has invalid workflow")
988
+ if fields.progress_view_model.status != fields.state_machine_snapshot.current_category:
989
+ raise ValueError("link FSM status must match state_machine_snapshot category")
990
+ if fields.receipt.status != fields.progress_view_model.status:
991
+ raise ValueError("link FSM receipt status must match progress view status")
992
+ if fields.progress_view_model.status in {
993
+ WorkflowStateCategory.BLOCKED.value,
994
+ WorkflowStateCategory.FAILED.value,
995
+ } and not payload["error_context"]:
996
+ raise ValueError("link FSM blocked/failed payload requires error_context")
997
+ reports_model = WorkflowReports.model_validate(payload["reports"])
998
+ snapshot = WorkflowStateMachineSnapshot.model_validate(payload["state_machine_snapshot"])
999
+ progress_view_model = WorkflowProgressViewModel.model_validate(payload["progress_view_model"])
1000
+ assert_public_report_matches_progress(
1001
+ reports_model.public_report,
1002
+ workflow=fields.workflow,
1003
+ run_id=str(payload["run_id"]),
1004
+ progress_view_model=progress_view_model,
1005
+ label="link FSM",
1006
+ )
1007
+ assert_agent_directive_matches_progress(
1008
+ AgentDirective.model_validate(payload[LINK_AGENT_DIRECTIVE_FIELD]),
1009
+ workflow=fields.workflow,
1010
+ run_id=str(payload["run_id"]),
1011
+ progress_view_model=progress_view_model,
1012
+ snapshot=snapshot,
1013
+ allowed_effect_kinds=_allowed_agent_effect_kinds_for_category(snapshot.current_category),
1014
+ label="link FSM",
1015
+ )
1016
+ _assert_link_snapshot(snapshot)
1017
+
1018
+
1019
+ def _allowed_agent_effect_kinds_for_category(category: WorkflowStateCategory) -> set[WorkflowEffectKind]:
1020
+ """Keep executable linker effects tied to the current FSM lane."""
1021
+
1022
+ match category:
1023
+ case WorkflowStateCategory.WAITING_AGENT:
1024
+ return {WorkflowEffectKind.RUN_SUBWORKFLOW, WorkflowEffectKind.CALL_SPECIALIST_MODEL}
1025
+ case WorkflowStateCategory.WAITING_EXTERNAL:
1026
+ return {WorkflowEffectKind.WAIT_EXTERNAL}
1027
+ case WorkflowStateCategory.WAITING_HUMAN:
1028
+ return {WorkflowEffectKind.ASK_HUMAN}
1029
+ case _:
1030
+ return set()
1031
+
1032
+
1033
+ def _assert_link_snapshot(snapshot: WorkflowStateMachineSnapshot) -> None:
1034
+ if snapshot.workflow not in LINK_PUBLIC_WORKFLOWS:
1035
+ raise ValueError("link FSM snapshot has invalid workflow")
1036
+ if snapshot.current_category != category_for_state(snapshot.current_state):
1037
+ raise ValueError("link FSM snapshot category does not match state")
1038
+ edges = _link_machine_edges()
1039
+ for transition in snapshot.transitions:
1040
+ if transition.to_category != category_for_state(transition.to_state):
1041
+ raise ValueError("link FSM transition category does not match state")
1042
+ edge = (transition.trigger, transition.from_state, transition.to_state)
1043
+ if edge not in edges:
1044
+ raise ValueError(f"unauthorized FSM transition: {edge}")
1045
+
1046
+
1047
+ def _link_machine_edges() -> set[tuple[str, str, str]]:
1048
+ """Return every transition edge declared by the canonical LinkMachine."""
1049
+
1050
+ edges: set[tuple[str, str, str]] = set()
1051
+ for event in LinkMachine.events:
1052
+ for transition in event._transitions:
1053
+ for target in transition._targets:
1054
+ edges.add((event.id, str(transition.source.value), str(target.value)))
1055
+ return edges
1056
+
1057
+
1058
+ def _link_payload_fields(payload: JsonObject) -> _LinkPayloadFields:
1059
+ raw_fields: JsonObject = {
1060
+ "workflow": payload["workflow"],
1061
+ "progress_view_model": _json_object_subset(payload, "progress_view_model", ("status",)),
1062
+ "state_machine_snapshot": _json_object_subset(payload, "state_machine_snapshot", ("current_category",)),
1063
+ "receipt": _json_object_subset(payload, "receipt", ("status",)),
1064
+ }
1065
+ try:
1066
+ return _LinkPayloadFields.model_validate(raw_fields)
1067
+ except PydanticValidationError as exc:
1068
+ first = exc.errors()[0] if exc.errors() else {}
1069
+ loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
1070
+ msg = str(first.get("msg") or str(exc))
1071
+ raise ValueError(f"link FSM payload invalid: {loc}: {msg}") from exc
1072
+
1073
+
1074
+ def _json_object_subset(payload: JsonObject, field_name: str, keys: tuple[str, ...]) -> JsonObject:
1075
+ try:
1076
+ source = JsonObjectAdapter.validate_python(payload[field_name])
1077
+ except PydanticValidationError as exc:
1078
+ raise ValueError(f"link FSM payload invalid: {field_name} must be an object") from exc
1079
+ return {key: source[key] for key in keys if key in source}
1080
+
1081
+
1082
+ def link_cli_exit_code(payload: JsonObject) -> int:
1083
+ fields = _link_cli_exit_code_fields(payload)
1084
+ status = fields.progress_view_model.status
1085
+ match status:
1086
+ case "completed" | "completed_with_warnings":
1087
+ return EXIT_OK
1088
+ case "waiting_agent" | "waiting_external" | "waiting_human" | "blocked":
1089
+ return EXIT_VALIDATION
1090
+ case "failed":
1091
+ if _link_error_context_missing_path(payload):
1092
+ return EXIT_MISSING
1093
+ return EXIT_IO
1094
+ case _:
1095
+ return EXIT_USAGE
1096
+
1097
+
1098
+ def _link_error_context_missing_path(payload: JsonObject) -> bool:
1099
+ try:
1100
+ fields = _LinkErrorContextFields.model_validate(
1101
+ _json_object_subset(payload, "error_context", ("missing_inputs",))
1102
+ )
1103
+ except (KeyError, ValueError, PydanticValidationError):
1104
+ return False
1105
+ return "wiki_dir" in fields.missing_inputs
1106
+
1107
+
1108
+ def _link_cli_exit_code_fields(payload: JsonObject) -> _LinkCliExitCodeFields:
1109
+ payload = JsonObjectAdapter.validate_python(payload)
1110
+ raw_fields: JsonObject = {
1111
+ "progress_view_model": _json_object_subset(payload, "progress_view_model", ("status",)),
1112
+ }
1113
+ try:
1114
+ return _LinkCliExitCodeFields.model_validate(raw_fields)
1115
+ except PydanticValidationError as exc:
1116
+ first = exc.errors()[0] if exc.errors() else {}
1117
+ loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
1118
+ msg = str(first.get("msg") or str(exc))
1119
+ raise ValueError(f"link FSM exit-code payload invalid: {loc}: {msg}") from exc