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,817 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import ConfigDict, Field, StrictBool, StrictStr, model_validator
6
+ from pydantic.json_schema import SkipJsonSchema
7
+
8
+ from mednotes.domains.flashcards.contracts import FlashcardAcceptedCard, FlashcardsTaggingReceipt, FlashcardWritePlan
9
+ from mednotes.domains.flashcards.flashcards_machine import (
10
+ FlashcardsMachine,
11
+ FlashcardsState,
12
+ ObsidianTaggingCompletedEvent,
13
+ category_for_flashcards_state,
14
+ )
15
+ from mednotes.kernel.agent_directive import (
16
+ AgentDirective,
17
+ agent_directive_from_progress_view_model,
18
+ assert_agent_directive_matches_progress,
19
+ )
20
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
21
+ from mednotes.kernel.effects import WorkflowEffectKind
22
+ from mednotes.kernel.fsm_model import WorkflowModel
23
+ from mednotes.kernel.fsm_transition_result import WorkflowTransitionResult
24
+ from mednotes.kernel.progress import (
25
+ WorkflowProgressCounts,
26
+ WorkflowProgressEventType,
27
+ WorkflowProgressState,
28
+ WorkflowProgressStatus,
29
+ WorkflowProgressViewModel,
30
+ build_progress_view_model,
31
+ progress_state_from_view_model,
32
+ )
33
+ from mednotes.kernel.public_report import (
34
+ WorkflowPublicReport,
35
+ WorkflowReports,
36
+ assert_public_report_matches_progress,
37
+ public_progress_followup_line,
38
+ )
39
+ from mednotes.kernel.state_machine import (
40
+ WorkflowStateCategory,
41
+ WorkflowStateMachineSnapshot,
42
+ WorkflowTransition,
43
+ send_workflow_event,
44
+ )
45
+ from mednotes.kernel.workflow import (
46
+ HumanDecisionPacket,
47
+ WorkflowDecision,
48
+ assert_diagnostic_context_evidence_only,
49
+ diagnostic_context_evidence_only,
50
+ )
51
+
52
+ FLASHCARDS_WORKFLOW = "/flashcards"
53
+ FLASHCARDS_FSM_SCHEMA = "medical-notes-workbench.flashcards-fsm-result.v1"
54
+
55
+
56
+ class _FlashcardsMachineEventEvidence(ContractModel):
57
+ """Typed lens over persisted flashcards machine event evidence."""
58
+
59
+ model_config = ConfigDict(extra="ignore")
60
+
61
+ audit_evidence: JsonObject = Field(default_factory=dict)
62
+
63
+
64
+ FLASHCARDS_ALLOWED_ROOT_KEYS = frozenset(
65
+ {
66
+ "schema",
67
+ "workflow",
68
+ "run_id",
69
+ "state_machine_snapshot",
70
+ "progress_view_model",
71
+ "decision",
72
+ "human_decision_packet",
73
+ "receipt",
74
+ "reports",
75
+ "agent_directive",
76
+ "artifacts",
77
+ "diagnostic_context",
78
+ "error_context",
79
+ }
80
+ )
81
+ FLASHCARDS_FORBIDDEN_ROOT_KEYS = frozenset(
82
+ {
83
+ "status",
84
+ "phase",
85
+ "blocked_reason",
86
+ "next_action",
87
+ "required_inputs",
88
+ "human_decision_required",
89
+ "workflow_exit_code",
90
+ }
91
+ )
92
+
93
+
94
+ def _json_object_field(payload: JsonObject, key: str) -> JsonObject:
95
+ value = payload[key] if key in payload else {}
96
+ return JsonObjectAdapter.validate_python(value) if isinstance(value, dict) else {}
97
+
98
+
99
+ class FlashcardsPrimaryObjectiveSummary(ContractModel):
100
+ schema_id: Literal["workflow.primary-objective-summary.v1"] = Field(
101
+ default="workflow.primary-objective-summary.v1",
102
+ alias="schema",
103
+ )
104
+ workflow: Literal["/flashcards"] = FLASHCARDS_WORKFLOW
105
+ run_id: StrictStr = Field(min_length=1)
106
+ objective: StrictStr = "Criar flashcards no Anki e marcar as fontes no Obsidian após sucesso real."
107
+ completed: StrictBool
108
+ status: StrictStr = Field(min_length=1)
109
+ mutation_state: Literal["changed", "unchanged", "not_applicable"]
110
+ mutation_summary: StrictStr = Field(min_length=1)
111
+ remaining_work_summary: StrictStr = Field(min_length=1)
112
+ next_step_summary: StrictStr = Field(min_length=1)
113
+ blocked_reason: StrictStr = ""
114
+ required_report_items: list[StrictStr] = Field(
115
+ default_factory=lambda: [
116
+ "objective_status",
117
+ "mutation_summary",
118
+ "remaining_work_summary",
119
+ "next_step_summary",
120
+ ]
121
+ )
122
+ preview_only: bool
123
+ created_cards: bool
124
+ created_card_count: int = Field(ge=0)
125
+ processed_source_count: int = Field(ge=0)
126
+ tagged_source_count: int = Field(ge=0)
127
+ obsidian_links_valid: bool
128
+
129
+
130
+ class FlashcardsReceipt(ContractModel):
131
+ status: WorkflowProgressStatus
132
+ changed_files: list[str] = Field(default_factory=list)
133
+ created_card_count: int = Field(default=0, ge=0)
134
+
135
+
136
+ class FlashcardReportInput(ContractModel):
137
+ accepted_cards: list[FlashcardAcceptedCard] = Field(default_factory=list)
138
+ reports: WorkflowReports | None = None
139
+
140
+
141
+ class FlashcardsArtifacts(ContractModel):
142
+ source_manifest_path: str = ""
143
+ write_plan_path: str = ""
144
+ final_report_path: str = ""
145
+ index_path: str = ""
146
+ dry_run: bool = False
147
+ write_plan: FlashcardWritePlan | None = None
148
+ apply_result: JsonObject = Field(default_factory=dict)
149
+
150
+
151
+ class FlashcardsFsmResult(ContractModel):
152
+ schema_id: Literal["medical-notes-workbench.flashcards-fsm-result.v1"] = Field(
153
+ default=FLASHCARDS_FSM_SCHEMA,
154
+ alias="schema",
155
+ )
156
+ workflow: Literal["/flashcards"] = FLASHCARDS_WORKFLOW
157
+ run_id: str
158
+ state_machine_snapshot: WorkflowStateMachineSnapshot
159
+ progress_state: SkipJsonSchema[WorkflowProgressState]
160
+ progress_view_model: WorkflowProgressViewModel
161
+ decision: WorkflowDecision | None = None
162
+ human_decision_packet: HumanDecisionPacket | None = None
163
+ receipt: FlashcardsReceipt
164
+ reports: WorkflowReports
165
+ agent_directive: JsonObject
166
+ artifacts: FlashcardsArtifacts = Field(default_factory=FlashcardsArtifacts)
167
+ diagnostic_context: JsonObject = Field(default_factory=dict)
168
+ error_context: JsonObject | None = None
169
+
170
+ @model_validator(mode="before")
171
+ @classmethod
172
+ def _hydrate_progress_state_from_public_payload(cls, value: object) -> object:
173
+ """Accept public payloads where progress_state is intentionally hidden."""
174
+
175
+ if not isinstance(value, dict) or "progress_state" in value or "progress_view_model" not in value:
176
+ return value
177
+ hydrated = dict(value)
178
+ progress_view = WorkflowProgressViewModel.model_validate(value["progress_view_model"])
179
+ hydrated["progress_state"] = progress_state_from_view_model(progress_view).to_payload()
180
+ return hydrated
181
+
182
+ @model_validator(mode="after")
183
+ def _progress_view_model_matches_state(self) -> FlashcardsFsmResult:
184
+ expected = build_progress_view_model(self.progress_state).to_payload()
185
+ if self.progress_view_model.to_payload() != expected:
186
+ raise ValueError("progress_view_model must match progress_state")
187
+ return self
188
+
189
+ def to_payload(self) -> JsonObject:
190
+ payload: JsonObject = {
191
+ "schema": self.schema_id,
192
+ "workflow": self.workflow,
193
+ "run_id": self.run_id,
194
+ "state_machine_snapshot": self.state_machine_snapshot.to_payload(),
195
+ "progress_view_model": self.progress_view_model.to_payload(),
196
+ "decision": self.decision.to_payload() if self.decision is not None else None,
197
+ "human_decision_packet": self.human_decision_packet.to_payload()
198
+ if self.human_decision_packet is not None
199
+ else None,
200
+ "receipt": self.receipt.to_payload(),
201
+ "reports": self.reports.to_payload(),
202
+ "agent_directive": dict(self.agent_directive),
203
+ "artifacts": self.artifacts.to_payload(),
204
+ }
205
+ if self.diagnostic_context:
206
+ payload["diagnostic_context"] = dict(self.diagnostic_context)
207
+ if self.error_context is not None:
208
+ payload["error_context"] = dict(self.error_context)
209
+ payload = JsonObjectAdapter.validate_python(payload)
210
+ assert_flashcards_fsm_payload(payload)
211
+ return payload
212
+
213
+
214
+ def assert_flashcards_fsm_payload(payload: JsonObject) -> None:
215
+ """Gate the public `/flashcards` FSM payload against legacy root truth."""
216
+
217
+ payload = JsonObjectAdapter.validate_python(payload)
218
+ legacy_keys = set(payload) & FLASHCARDS_FORBIDDEN_ROOT_KEYS
219
+ if legacy_keys:
220
+ raise ValueError(f"flashcards FSM payload contains legacy root keys: {sorted(legacy_keys)}")
221
+ required_keys = FLASHCARDS_ALLOWED_ROOT_KEYS - {"diagnostic_context", "error_context"}
222
+ missing_keys = required_keys - set(payload)
223
+ if missing_keys:
224
+ raise ValueError(f"flashcards FSM payload missing canonical root keys: {sorted(missing_keys)}")
225
+ unexpected_keys = set(payload) - FLASHCARDS_ALLOWED_ROOT_KEYS
226
+ if unexpected_keys:
227
+ raise ValueError(f"flashcards FSM payload contains unexpected root keys: {sorted(unexpected_keys)}")
228
+ diagnostic_context = _json_object_field(payload, "diagnostic_context")
229
+ assert_diagnostic_context_evidence_only(diagnostic_context)
230
+ if "agent_directive" in diagnostic_context:
231
+ raise ValueError("flashcards FSM diagnostic_context must not contain agent_directive")
232
+ reports = WorkflowReports.model_validate(payload["reports"])
233
+ snapshot = WorkflowStateMachineSnapshot.model_validate(payload["state_machine_snapshot"])
234
+ progress_view_model = WorkflowProgressViewModel.model_validate(payload["progress_view_model"])
235
+ receipt = FlashcardsReceipt.model_validate(payload["receipt"])
236
+ if progress_view_model.status != snapshot.current_category.value:
237
+ raise ValueError("flashcards FSM status must match state_machine_snapshot category")
238
+ if receipt.status != progress_view_model.status:
239
+ raise ValueError("flashcards FSM receipt status must match progress view status")
240
+ assert_public_report_matches_progress(
241
+ reports.public_report,
242
+ workflow=FLASHCARDS_WORKFLOW,
243
+ run_id=str(payload["run_id"]),
244
+ progress_view_model=progress_view_model,
245
+ label="flashcards FSM",
246
+ )
247
+ assert_agent_directive_matches_progress(
248
+ AgentDirective.model_validate(_json_object_field(payload, "agent_directive")),
249
+ workflow=FLASHCARDS_WORKFLOW,
250
+ run_id=str(payload["run_id"]),
251
+ progress_view_model=progress_view_model,
252
+ snapshot=snapshot,
253
+ allowed_effect_kinds=_allowed_agent_effect_kinds_for_category(snapshot.current_category),
254
+ label="flashcards FSM",
255
+ )
256
+
257
+
258
+ def _allowed_agent_effect_kinds_for_category(category: WorkflowStateCategory) -> set[WorkflowEffectKind]:
259
+ """Flashcards delegates no hidden execution outside its FSM contract."""
260
+
261
+ match category:
262
+ case WorkflowStateCategory.WAITING_AGENT:
263
+ return {WorkflowEffectKind.RUN_SUBWORKFLOW}
264
+ case WorkflowStateCategory.WAITING_EXTERNAL:
265
+ return {WorkflowEffectKind.WAIT_EXTERNAL}
266
+ case WorkflowStateCategory.WAITING_HUMAN:
267
+ return {WorkflowEffectKind.ASK_HUMAN}
268
+ case _:
269
+ return set()
270
+
271
+
272
+ def build_flashcards_fsm_result_from_model(model: WorkflowModel) -> FlashcardsFsmResult:
273
+ """Project the real FlashcardsMachine model without reclassifying aggregate facts."""
274
+
275
+ _validate_flashcards_machine_model(model)
276
+ state = FlashcardsState(model.state)
277
+ category = category_for_flashcards_state(state)
278
+ progress_state = _progress_state_from_model(model, state, category)
279
+ progress_view_model = build_progress_view_model(progress_state)
280
+ snapshot = _snapshot_from_model(model, state, category)
281
+ reports = _reports_from_model(model, state, progress_state)
282
+ agent_directive = agent_directive_from_progress_view_model(
283
+ progress_view_model,
284
+ schema="medical-notes-workbench.agent-directive.v1",
285
+ reason=_machine_reason_code(model, state),
286
+ effects=model.pending_effects,
287
+ blockers=_machine_blockers(category, model, state),
288
+ resume=progress_state.resume_action,
289
+ report_requires=["primary_objective", "anki_write", "obsidian_links"],
290
+ summary=_machine_agent_summary(state, progress_state),
291
+ instructions=_machine_agent_instructions(category),
292
+ ).to_payload()
293
+ # Keep the public flashcards payload on the repository-wide directive schema
294
+ # while the generic kernel model remains intentionally domain-neutral.
295
+ return FlashcardsFsmResult(
296
+ run_id=model.run_id,
297
+ state_machine_snapshot=snapshot,
298
+ progress_state=progress_state,
299
+ progress_view_model=progress_view_model,
300
+ decision=model.last_transition.decision if model.last_transition is not None else None,
301
+ human_decision_packet=model.last_transition.human_decision_packet if model.last_transition is not None else None,
302
+ receipt=FlashcardsReceipt(
303
+ status=progress_view_model.status,
304
+ changed_files=_event_string_list(model, "changed_files"),
305
+ created_card_count=_event_int_max(model, "created_card_count"),
306
+ ),
307
+ reports=reports,
308
+ agent_directive=JsonObjectAdapter.validate_python(agent_directive),
309
+ artifacts=FlashcardsArtifacts(),
310
+ diagnostic_context=_diagnostic_context_from_model(model, state, category),
311
+ error_context=_error_context_for(model.last_transition.decision if model.last_transition is not None else None),
312
+ )
313
+
314
+ def flashcards_fsm_payload_from_model(model: WorkflowModel) -> JsonObject:
315
+ """JSON boundary for the machine-driven `/flashcards` FSM projection."""
316
+
317
+ return build_flashcards_fsm_result_from_model(model).to_payload()
318
+
319
+
320
+ def flashcards_fsm_payload_from_tagging_receipt(receipt: JsonObject, *, run_id: str) -> JsonObject:
321
+ """Project the official Obsidian tag receipt into the terminal FSM state."""
322
+
323
+ tagging = FlashcardsTaggingReceipt.model_validate(receipt)
324
+ model = WorkflowModel.start(
325
+ workflow=FLASHCARDS_WORKFLOW,
326
+ run_id=run_id,
327
+ initial_state=FlashcardsState.TAGGING_OBSIDIAN.value,
328
+ )
329
+ send_workflow_event(
330
+ FlashcardsMachine(model=model, state_field=WorkflowModel.STATECHART_STATE_FIELD),
331
+ ObsidianTaggingCompletedEvent(
332
+ workflow=FLASHCARDS_WORKFLOW,
333
+ run_id=run_id,
334
+ current_state=FlashcardsState.TAGGING_OBSIDIAN.value,
335
+ tagged_source_count=len(tagging.changed_files),
336
+ changed_files=list(tagging.changed_files),
337
+ audit_evidence={
338
+ "effect_target": tagging.effect_target,
339
+ "status": tagging.status,
340
+ "tag": tagging.tag,
341
+ },
342
+ ),
343
+ )
344
+ return flashcards_fsm_payload_from_model(model)
345
+
346
+
347
+ def _validate_flashcards_machine_model(model: WorkflowModel) -> None:
348
+ if model.workflow != FLASHCARDS_WORKFLOW:
349
+ raise ValueError(f"flashcards FSM projector requires workflow={FLASHCARDS_WORKFLOW}")
350
+ FlashcardsState(model.state)
351
+
352
+
353
+ def _progress_state_from_model(
354
+ model: WorkflowModel,
355
+ state: FlashcardsState,
356
+ category: WorkflowStateCategory,
357
+ ) -> WorkflowProgressState:
358
+ status = _machine_progress_status(category)
359
+ current, total, counts = _machine_counts(model, state)
360
+ return WorkflowProgressState(
361
+ workflow=FLASHCARDS_WORKFLOW,
362
+ run_id=model.run_id,
363
+ state=state.value,
364
+ phase=_machine_phase_for_state(state),
365
+ event_type=_machine_event_type(status),
366
+ message=_machine_message_for_state(state),
367
+ status=status,
368
+ current=current,
369
+ total=total,
370
+ counts=counts,
371
+ resume_action=_machine_resume_action(model, state),
372
+ resume_supported=status
373
+ in {
374
+ WorkflowProgressStatus.WAITING_AGENT,
375
+ WorkflowProgressStatus.WAITING_EXTERNAL,
376
+ WorkflowProgressStatus.WAITING_HUMAN,
377
+ WorkflowProgressStatus.BLOCKED,
378
+ },
379
+ can_continue_now=status
380
+ in {
381
+ WorkflowProgressStatus.RUNNING,
382
+ WorkflowProgressStatus.WAITING_AGENT,
383
+ },
384
+ decision=model.last_transition.decision.decision_summary()
385
+ if model.last_transition is not None and model.last_transition.decision is not None
386
+ else None,
387
+ technical_context={
388
+ "reason": _machine_reason_code(model, state),
389
+ "category": category.value,
390
+ "source": "FlashcardsMachine",
391
+ "source_count": _event_int_max(model, "source_count"),
392
+ "candidate_count": _event_int_max(model, "candidate_count"),
393
+ "new_card_count": _event_int_max(model, "new_card_count"),
394
+ "created_card_count": _event_int_max(model, "created_card_count"),
395
+ "tagged_source_count": _event_int_max(model, "tagged_source_count"),
396
+ },
397
+ )
398
+
399
+
400
+ def _machine_counts(
401
+ model: WorkflowModel,
402
+ state: FlashcardsState,
403
+ ) -> tuple[int, int, WorkflowProgressCounts]:
404
+ source_count = _event_int_max(model, "source_count")
405
+ candidate_count = _event_int_max(model, "candidate_count")
406
+ new_card_count = _event_int_max(model, "new_card_count")
407
+ created_count = _event_int_max(model, "created_card_count")
408
+ tagged_count = _event_int_max(model, "tagged_source_count")
409
+ planned = max(candidate_count, new_card_count, created_count, source_count)
410
+ if state == FlashcardsState.COMPLETED:
411
+ total = max(created_count, tagged_count, candidate_count, source_count)
412
+ return (
413
+ total,
414
+ total,
415
+ WorkflowProgressCounts(
416
+ planned_items=total,
417
+ processed_items=total,
418
+ mutated_files=tagged_count,
419
+ written_files=tagged_count,
420
+ ),
421
+ )
422
+ if state == FlashcardsState.WAITING_HUMAN_CONFIRMATION:
423
+ return (
424
+ 0,
425
+ max(candidate_count, new_card_count),
426
+ WorkflowProgressCounts(
427
+ planned_items=max(candidate_count, new_card_count),
428
+ remaining_items=new_card_count,
429
+ blocked_items=new_card_count,
430
+ ),
431
+ )
432
+ if state in {
433
+ FlashcardsState.STALE_SOURCE,
434
+ FlashcardsState.CREATE_CANCELLED,
435
+ FlashcardsState.SOURCE_SELECTION_BLOCKED,
436
+ FlashcardsState.CANDIDATE_GENERATION_BLOCKED,
437
+ FlashcardsState.PREVIEW_DECISION_BLOCKED,
438
+ FlashcardsState.ANKI_WRITE_BLOCKED,
439
+ FlashcardsState.OBSIDIAN_TAGGING_BLOCKED,
440
+ FlashcardsState.FAILED,
441
+ }:
442
+ blocked = max(planned, 1)
443
+ return (
444
+ 0,
445
+ blocked,
446
+ WorkflowProgressCounts(
447
+ planned_items=planned,
448
+ remaining_items=blocked,
449
+ blocked_items=blocked,
450
+ ),
451
+ )
452
+ return (
453
+ 0,
454
+ planned,
455
+ WorkflowProgressCounts(
456
+ planned_items=planned,
457
+ remaining_items=planned,
458
+ ),
459
+ )
460
+
461
+
462
+ def _snapshot_from_model(
463
+ model: WorkflowModel,
464
+ state: FlashcardsState,
465
+ category: WorkflowStateCategory,
466
+ ) -> WorkflowStateMachineSnapshot:
467
+ return WorkflowStateMachineSnapshot(
468
+ workflow=FLASHCARDS_WORKFLOW,
469
+ run_id=model.run_id,
470
+ current_state=state.value,
471
+ current_category=category,
472
+ transitions=[_machine_snapshot_transition(transition) for transition in model.transition_log],
473
+ metadata={"reason": _machine_reason_code(model, state), "source": "FlashcardsMachine"},
474
+ )
475
+
476
+
477
+ def _machine_snapshot_transition(transition: WorkflowTransitionResult) -> WorkflowTransition:
478
+ return WorkflowTransition(
479
+ workflow=transition.workflow,
480
+ from_state=transition.from_state,
481
+ to_state=transition.to_state,
482
+ to_category=category_for_flashcards_state(FlashcardsState(transition.to_state)),
483
+ trigger=transition.trigger,
484
+ effects=list(transition.effects),
485
+ decision=transition.decision,
486
+ resume_action=transition.resume_action,
487
+ )
488
+
489
+
490
+ def _reports_from_model(
491
+ model: WorkflowModel,
492
+ state: FlashcardsState,
493
+ progress_state: WorkflowProgressState,
494
+ ) -> WorkflowReports:
495
+ summary = _machine_message_for_state(state)
496
+ public_lines = [summary]
497
+ followup_line = public_progress_followup_line(progress_state)
498
+ if followup_line:
499
+ public_lines.append(followup_line)
500
+ public_report = WorkflowPublicReport(
501
+ workflow=FLASHCARDS_WORKFLOW,
502
+ run_id=progress_state.run_id,
503
+ headline=summary,
504
+ lines=public_lines,
505
+ )
506
+ created_count = _event_int_max(model, "created_card_count")
507
+ tagged_count = _event_int_max(model, "tagged_source_count")
508
+ completed = state == FlashcardsState.COMPLETED
509
+ return WorkflowReports(
510
+ summary=summary,
511
+ public_report=public_report,
512
+ details={
513
+ "primary_objective_summary": FlashcardsPrimaryObjectiveSummary(
514
+ run_id=model.run_id,
515
+ completed=completed,
516
+ status=state.value,
517
+ mutation_state="changed" if created_count > 0 or tagged_count > 0 else "unchanged",
518
+ mutation_summary=_flashcards_mutation_summary(created_count, tagged_count),
519
+ remaining_work_summary=_flashcards_remaining_work_summary(state, completed),
520
+ next_step_summary=_flashcards_next_step_summary(progress_state, completed),
521
+ blocked_reason="" if completed else state.value,
522
+ preview_only=state
523
+ in {
524
+ FlashcardsState.WAITING_AGENT_CANDIDATES,
525
+ FlashcardsState.WAITING_HUMAN_CONFIRMATION,
526
+ },
527
+ created_cards=created_count > 0,
528
+ created_card_count=created_count,
529
+ processed_source_count=max(
530
+ _event_int_max(model, "source_count"),
531
+ progress_state.counts.processed_items,
532
+ ),
533
+ tagged_source_count=tagged_count,
534
+ obsidian_links_valid=state != FlashcardsState.STALE_SOURCE,
535
+ ).to_payload()
536
+ },
537
+ )
538
+
539
+
540
+ def _flashcards_mutation_summary(created_count: int, tagged_count: int) -> str:
541
+ if created_count > 0:
542
+ return f"{created_count} card(s) foram criados no Anki; {tagged_count} fonte(s) foram marcadas."
543
+ if tagged_count > 0:
544
+ return f"{tagged_count} fonte(s) foram marcadas no Obsidian."
545
+ return "Nenhum card foi criado e nenhuma fonte foi marcada nesta etapa."
546
+
547
+
548
+ def _flashcards_remaining_work_summary(state: FlashcardsState, completed: bool) -> str:
549
+ if completed:
550
+ return "Cards aceitos foram criados e as fontes foram marcadas quando aplicável."
551
+ return _machine_message_for_state(state)
552
+
553
+
554
+ def _flashcards_next_step_summary(progress_state: WorkflowProgressState, completed: bool) -> str:
555
+ if completed:
556
+ return "Nenhuma ação pendente para flashcards."
557
+ return progress_state.resume_action or "Retomar /flashcards pela rota oficial."
558
+
559
+
560
+ def _diagnostic_context_from_model(
561
+ model: WorkflowModel,
562
+ state: FlashcardsState,
563
+ category: WorkflowStateCategory,
564
+ ) -> JsonObject:
565
+ if category == WorkflowStateCategory.COMPLETED:
566
+ return {}
567
+ context: JsonObject = {
568
+ "schema": "medical-notes-workbench.flashcards-fsm-diagnostic-context.v2",
569
+ "state": state.value,
570
+ "category": category.value,
571
+ "reason": _machine_reason_code(model, state),
572
+ "source": "FlashcardsMachine",
573
+ }
574
+ evidence = _machine_audit_evidence(model)
575
+ for key, value in evidence.items():
576
+ if key not in context:
577
+ context[key] = value
578
+ return diagnostic_context_evidence_only(context)
579
+
580
+
581
+ def _machine_audit_evidence(model: WorkflowModel) -> JsonObject:
582
+ if not model.event_log:
583
+ return {}
584
+ event = _FlashcardsMachineEventEvidence.model_validate(model.event_log[-1])
585
+ return JsonObjectAdapter.validate_python(event.audit_evidence)
586
+
587
+
588
+ def _machine_progress_status(category: WorkflowStateCategory) -> WorkflowProgressStatus:
589
+ match category:
590
+ case WorkflowStateCategory.PREPARING | WorkflowStateCategory.RUNNING:
591
+ return WorkflowProgressStatus.RUNNING
592
+ case WorkflowStateCategory.WAITING_AGENT:
593
+ return WorkflowProgressStatus.WAITING_AGENT
594
+ case WorkflowStateCategory.WAITING_EXTERNAL:
595
+ return WorkflowProgressStatus.WAITING_EXTERNAL
596
+ case WorkflowStateCategory.WAITING_HUMAN:
597
+ return WorkflowProgressStatus.WAITING_HUMAN
598
+ case WorkflowStateCategory.BLOCKED:
599
+ return WorkflowProgressStatus.BLOCKED
600
+ case WorkflowStateCategory.FAILED:
601
+ return WorkflowProgressStatus.FAILED
602
+ case WorkflowStateCategory.COMPLETED:
603
+ return WorkflowProgressStatus.COMPLETED
604
+ case WorkflowStateCategory.COMPLETED_WITH_WARNINGS:
605
+ return WorkflowProgressStatus.COMPLETED_WITH_WARNINGS
606
+
607
+
608
+ def _machine_event_type(status: WorkflowProgressStatus) -> WorkflowProgressEventType:
609
+ match status:
610
+ case WorkflowProgressStatus.COMPLETED | WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
611
+ return WorkflowProgressEventType.WORKFLOW_COMPLETED
612
+ case WorkflowProgressStatus.FAILED:
613
+ return WorkflowProgressEventType.WORKFLOW_FAILED
614
+ case WorkflowProgressStatus.WAITING_EXTERNAL:
615
+ return WorkflowProgressEventType.EXTERNAL_WAIT_STARTED
616
+ case WorkflowProgressStatus.WAITING_HUMAN | WorkflowProgressStatus.BLOCKED:
617
+ return WorkflowProgressEventType.DECISION_EMITTED
618
+ case _:
619
+ return WorkflowProgressEventType.STATE_ENTERED
620
+
621
+
622
+ def _machine_phase_for_state(state: FlashcardsState) -> str:
623
+ match state:
624
+ case FlashcardsState.CHECKING_SOURCES | FlashcardsState.STALE_SOURCE:
625
+ return "flashcards_sources"
626
+ case FlashcardsState.WAITING_AGENT_CANDIDATES | FlashcardsState.WAITING_HUMAN_CONFIRMATION:
627
+ return "flashcards_preview"
628
+ case FlashcardsState.ANKI_UNAVAILABLE | FlashcardsState.WRITING_ANKI:
629
+ return "flashcards_anki"
630
+ case FlashcardsState.TAGGING_OBSIDIAN:
631
+ return "flashcards_obsidian_tagging"
632
+ case FlashcardsState.COMPLETED:
633
+ return "flashcards_completed"
634
+ case FlashcardsState.CREATE_CANCELLED:
635
+ return "flashcards_create_cancelled"
636
+ case FlashcardsState.SOURCE_SELECTION_BLOCKED:
637
+ return "flashcards_source_selection_blocked"
638
+ case FlashcardsState.CANDIDATE_GENERATION_BLOCKED:
639
+ return "flashcards_candidate_generation_blocked"
640
+ case FlashcardsState.PREVIEW_DECISION_BLOCKED:
641
+ return "flashcards_preview_decision_blocked"
642
+ case FlashcardsState.ANKI_WRITE_BLOCKED:
643
+ return "flashcards_anki_write_blocked"
644
+ case FlashcardsState.OBSIDIAN_TAGGING_BLOCKED:
645
+ return "flashcards_obsidian_tagging_blocked"
646
+ case FlashcardsState.FAILED:
647
+ return "flashcards_failed"
648
+
649
+
650
+ def _machine_message_for_state(state: FlashcardsState) -> str:
651
+ match state:
652
+ case FlashcardsState.WAITING_AGENT_CANDIDATES:
653
+ return "Flashcards aguardam geração de candidatos pelo agente."
654
+ case FlashcardsState.WAITING_HUMAN_CONFIRMATION:
655
+ return "Revise a prévia antes de criar cards no Anki."
656
+ case FlashcardsState.ANKI_UNAVAILABLE:
657
+ return "Flashcards aguardam o Anki ficar disponível."
658
+ case FlashcardsState.WRITING_ANKI:
659
+ return "Flashcards estão sendo gravados no Anki."
660
+ case FlashcardsState.TAGGING_OBSIDIAN:
661
+ return "Flashcards aguardam marcação das fontes no Obsidian."
662
+ case FlashcardsState.STALE_SOURCE:
663
+ return "A fonte dos flashcards ficou desatualizada."
664
+ case FlashcardsState.COMPLETED:
665
+ return "Flashcards criados e fontes conferidas."
666
+ case FlashcardsState.CREATE_CANCELLED:
667
+ return "Criação de flashcards cancelada antes de gravar no Anki."
668
+ case FlashcardsState.SOURCE_SELECTION_BLOCKED:
669
+ return "Seleção das fontes de flashcards bloqueada."
670
+ case FlashcardsState.CANDIDATE_GENERATION_BLOCKED:
671
+ return "Geração de candidatos de flashcards bloqueada."
672
+ case FlashcardsState.PREVIEW_DECISION_BLOCKED:
673
+ return "Decisão da prévia de flashcards bloqueada."
674
+ case FlashcardsState.ANKI_WRITE_BLOCKED:
675
+ return "Escrita dos flashcards no Anki bloqueada."
676
+ case FlashcardsState.OBSIDIAN_TAGGING_BLOCKED:
677
+ return "Marcação das fontes no Obsidian bloqueada."
678
+ case FlashcardsState.FAILED:
679
+ return "Flashcards falharam antes de concluir."
680
+ case _:
681
+ return "Workflow de flashcards em andamento."
682
+
683
+
684
+ def _machine_resume_action(model: WorkflowModel, state: FlashcardsState) -> str:
685
+ if state == FlashcardsState.COMPLETED:
686
+ return ""
687
+ if model.last_transition is not None and model.last_transition.resume_action:
688
+ return model.last_transition.resume_action
689
+ match state:
690
+ case FlashcardsState.WAITING_AGENT_CANDIDATES:
691
+ return "flashcards:generate-candidates"
692
+ case FlashcardsState.WAITING_HUMAN_CONFIRMATION:
693
+ return "flashcards:confirm-create"
694
+ case FlashcardsState.ANKI_UNAVAILABLE:
695
+ return "flashcards:retry-anki"
696
+ case FlashcardsState.WRITING_ANKI:
697
+ return "flashcards:write-anki"
698
+ case FlashcardsState.TAGGING_OBSIDIAN:
699
+ return "flashcards:tag-obsidian"
700
+ case (
701
+ FlashcardsState.STALE_SOURCE
702
+ | FlashcardsState.CREATE_CANCELLED
703
+ | FlashcardsState.SOURCE_SELECTION_BLOCKED
704
+ | FlashcardsState.CANDIDATE_GENERATION_BLOCKED
705
+ | FlashcardsState.PREVIEW_DECISION_BLOCKED
706
+ | FlashcardsState.ANKI_WRITE_BLOCKED
707
+ | FlashcardsState.OBSIDIAN_TAGGING_BLOCKED
708
+ | FlashcardsState.FAILED
709
+ ):
710
+ return "flashcards:prepare"
711
+ case _:
712
+ return ""
713
+
714
+
715
+ def _machine_reason_code(model: WorkflowModel, state: FlashcardsState) -> str:
716
+ if model.last_transition is not None:
717
+ return model.last_transition.reason_code
718
+ return state.value
719
+
720
+
721
+ def _machine_blockers(
722
+ category: WorkflowStateCategory,
723
+ model: WorkflowModel,
724
+ state: FlashcardsState,
725
+ ) -> list[str]:
726
+ if category in {
727
+ WorkflowStateCategory.WAITING_AGENT,
728
+ WorkflowStateCategory.WAITING_EXTERNAL,
729
+ WorkflowStateCategory.WAITING_HUMAN,
730
+ WorkflowStateCategory.BLOCKED,
731
+ WorkflowStateCategory.FAILED,
732
+ }:
733
+ return [_machine_reason_code(model, state)]
734
+ return []
735
+
736
+
737
+ def _machine_agent_summary(state: FlashcardsState, progress_state: WorkflowProgressState) -> str:
738
+ return progress_state.message or _machine_message_for_state(state)
739
+
740
+
741
+ def _machine_agent_instructions(category: WorkflowStateCategory) -> list[str]:
742
+ if category == WorkflowStateCategory.WAITING_AGENT:
743
+ return ["Execute somente os efeitos em agent_directive.control.effects e retome /flashcards pelo resultado tipado."]
744
+ if category == WorkflowStateCategory.WAITING_EXTERNAL:
745
+ return ["Aguarde o Anki ficar disponivel antes de retomar /flashcards."]
746
+ if category == WorkflowStateCategory.WAITING_HUMAN:
747
+ return ["Peça a decisão humana fechada antes de criar cards no Anki."]
748
+ if category in {WorkflowStateCategory.BLOCKED, WorkflowStateCategory.FAILED}:
749
+ return ["Use a decisão e o resume_action da FSM para recuperar /flashcards."]
750
+ return ["Use a FlashcardsMachine como fonte de verdade do estado de flashcards."]
751
+
752
+
753
+ def _last_event_int(model: WorkflowModel, field_name: str) -> int:
754
+ if not model.event_log:
755
+ return 0
756
+ event = model.event_log[-1]
757
+ value = event[field_name] if field_name in event else 0
758
+ if isinstance(value, bool):
759
+ return 0
760
+ if isinstance(value, int) and value >= 0:
761
+ return value
762
+ return 0
763
+
764
+
765
+ def _event_int_max(model: WorkflowModel, field_name: str) -> int:
766
+ values: list[int] = []
767
+ for event in model.event_log:
768
+ value = event[field_name] if field_name in event else 0
769
+ if isinstance(value, bool):
770
+ continue
771
+ if isinstance(value, int) and value >= 0:
772
+ values.append(value)
773
+ return max(values, default=0)
774
+
775
+
776
+ def _event_string_list(model: WorkflowModel, field_name: str) -> list[str]:
777
+ values: list[str] = []
778
+ for event in model.event_log:
779
+ value = event[field_name] if field_name in event else []
780
+ if not isinstance(value, list):
781
+ continue
782
+ for item in value:
783
+ if isinstance(item, str) and item:
784
+ values.append(item)
785
+ return list(dict.fromkeys(values))
786
+
787
+
788
+ def _error_context_for(decision: WorkflowDecision | None) -> JsonObject | None:
789
+ if decision is None or decision.kind == "ask_human":
790
+ return None
791
+ if decision.reason_code == "invalid_obsidian_deeplink":
792
+ return JsonObjectAdapter.validate_python(
793
+ {
794
+ "phase": decision.phase,
795
+ "blocked_reason": decision.reason_code,
796
+ "root_cause": "invalid_obsidian_deeplink",
797
+ "affected_artifact": "flashcards_source_manifest",
798
+ "error_summary": decision.developer_summary,
799
+ "suggested_fix": "Regenerar o manifest de fontes e preparar novamente antes de criar cards.",
800
+ "next_action": decision.next_action,
801
+ "retry_scope": "regenerate_flashcard_sources_then_prepare",
802
+ "human_decision_required": False,
803
+ }
804
+ )
805
+ return JsonObjectAdapter.validate_python(
806
+ {
807
+ "phase": decision.phase,
808
+ "blocked_reason": decision.reason_code,
809
+ "root_cause": decision.reason_code,
810
+ "affected_artifact": "flashcards_prepare_payload",
811
+ "error_summary": decision.developer_summary,
812
+ "suggested_fix": decision.next_action,
813
+ "next_action": decision.next_action,
814
+ "retry_scope": "resolve_flashcards_prepare_blocker_then_prepare",
815
+ "human_decision_required": False,
816
+ }
817
+ )