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,306 @@
1
+ """Master plan builder for the fix-wiki workflow."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ from collections.abc import Sequence
7
+ from pathlib import Path
8
+
9
+ from pydantic import Field
10
+
11
+ from mednotes.domains.wiki.batch_state import file_sha256
12
+ from mednotes.domains.wiki.capabilities.notes.note_iter import iter_notes
13
+ from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_problem import FixWikiProblem
14
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
15
+
16
+ FIX_WIKI_PLAN_SCHEMA = "medical-notes-workbench.fix-wiki-plan.v1"
17
+ ORDERED_FIX_WIKI_PHASES = (
18
+ "preflight",
19
+ "inventory",
20
+ "vocabulary_bootstrap",
21
+ "vocabulary_map_diagnosis",
22
+ "style_yaml",
23
+ "provenance_backfill",
24
+ "hygiene",
25
+ "duplicates",
26
+ "taxonomy",
27
+ "linker",
28
+ "final_validation",
29
+ )
30
+
31
+
32
+ class FixWikiSnapshotFile(ContractModel):
33
+ """Stable note identity used to detect stale fix-wiki plans before apply."""
34
+
35
+ path: str
36
+ hash: str
37
+
38
+
39
+ class FixWikiTaxonomyPlanItem(ContractModel):
40
+ """Projection of taxonomy plan rows; only `source` drives phase scope."""
41
+
42
+ action: str = ""
43
+ source: str = ""
44
+ destination: str = ""
45
+ reason: str = ""
46
+ blocked_reason: str = ""
47
+
48
+
49
+ class FixWikiPhase(ContractModel):
50
+ """Single public phase row in the preview/apply plan."""
51
+
52
+ phase: str
53
+ status: str = "ready"
54
+ can_apply: bool = True
55
+ blocked_reason: str = ""
56
+ requires_decision: str = ""
57
+ requires_human: bool = False
58
+ affected_paths: list[str] = Field(default_factory=list)
59
+ planned_artifacts: list[str] = Field(default_factory=list)
60
+ rollback_strategy: str = ""
61
+
62
+
63
+ class FixWikiPlan(ContractModel):
64
+ """Typed root plan; `plan_hash` is calculated from its serialized payload."""
65
+
66
+ schema_id: str = Field(default=FIX_WIKI_PLAN_SCHEMA, alias="schema")
67
+ run_id: str
68
+ wiki_dir: str
69
+ snapshot_hash: str
70
+ snapshot_files: list[FixWikiSnapshotFile] = Field(default_factory=list)
71
+ context_packet_hashes: JsonObject = Field(default_factory=dict)
72
+ git: JsonObject = Field(default_factory=dict)
73
+ vocabulary_map_hash: str = ""
74
+ phase_order: list[str]
75
+ plan_hash: str = ""
76
+ status: str
77
+ problems: list[FixWikiProblem] = Field(default_factory=list)
78
+ fix_wiki_problems: list[FixWikiProblem] = Field(default_factory=list)
79
+ deferred_work_items: list[JsonObject] = Field(default_factory=list)
80
+ phases: list[FixWikiPhase] = Field(default_factory=list)
81
+ blockers: list[FixWikiProblem] = Field(default_factory=list)
82
+ warnings: list[FixWikiProblem] = Field(default_factory=list)
83
+
84
+
85
+ def _vocabulary_blocked_reason(status: str) -> str:
86
+ if status == "blocked_pending":
87
+ return "vocabulary_semantic_ingestion_pending"
88
+ if status == "blocked_human":
89
+ return "vocabulary_map_blocked"
90
+ return ""
91
+
92
+
93
+ def _phase(
94
+ name: str,
95
+ *,
96
+ status: str = "ready",
97
+ can_apply: bool = True,
98
+ blocked_reason: str = "",
99
+ requires_decision: str = "",
100
+ requires_human: bool = False,
101
+ affected_paths: list[str] | None = None,
102
+ planned_artifacts: list[str] | None = None,
103
+ rollback_strategy: str = "",
104
+ ) -> FixWikiPhase:
105
+ return FixWikiPhase(
106
+ phase=name,
107
+ status=status,
108
+ can_apply=can_apply,
109
+ blocked_reason=blocked_reason,
110
+ requires_decision=requires_decision,
111
+ requires_human=requires_human,
112
+ affected_paths=affected_paths or [],
113
+ planned_artifacts=planned_artifacts or [],
114
+ rollback_strategy=rollback_strategy,
115
+ )
116
+
117
+
118
+ def _hash_plan(payload: JsonObject) -> str:
119
+ stable = {key: value for key, value in payload.items() if key != "plan_hash"}
120
+ encoded = json.dumps(stable, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
121
+ return "sha256:" + hashlib.sha256(encoded).hexdigest()
122
+
123
+
124
+ def collect_fix_wiki_snapshot_files(wiki_dir: Path) -> list[JsonObject]:
125
+ files: list[JsonObject] = []
126
+ for path in iter_notes(wiki_dir) if wiki_dir.exists() else []:
127
+ snapshot_file = FixWikiSnapshotFile(
128
+ path=path.relative_to(wiki_dir).as_posix(),
129
+ hash="sha256:" + file_sha256(path),
130
+ )
131
+ files.append(snapshot_file.to_payload())
132
+ return files
133
+
134
+
135
+ def fix_wiki_snapshot_hash(wiki_dir: Path) -> str:
136
+ encoded = json.dumps(
137
+ collect_fix_wiki_snapshot_files(wiki_dir),
138
+ ensure_ascii=False,
139
+ sort_keys=True,
140
+ separators=(",", ":"),
141
+ ).encode("utf-8")
142
+ return "sha256:" + hashlib.sha256(encoded).hexdigest()
143
+
144
+
145
+ def _context_packet_hashes(context_packets: JsonObject | None) -> JsonObject:
146
+ hashes: JsonObject = {}
147
+ for key, value in sorted((context_packets or {}).items()):
148
+ path = Path(str(value))
149
+ if path.is_file():
150
+ hashes[key] = "sha256:" + file_sha256(path)
151
+ return JsonObjectAdapter.validate_python(hashes)
152
+
153
+
154
+ def validate_fix_wiki_plan_snapshot(plan: object, wiki_dir: Path) -> JsonObject:
155
+ typed_plan = FixWikiPlan.model_validate(plan)
156
+ expected = typed_plan.snapshot_hash
157
+ actual = fix_wiki_snapshot_hash(wiki_dir)
158
+ if expected and expected != actual:
159
+ return JsonObjectAdapter.validate_python(
160
+ {
161
+ "status": "blocked",
162
+ "blocked_reason": "stale_fix_wiki_plan",
163
+ "expected_snapshot_hash": expected,
164
+ "actual_snapshot_hash": actual,
165
+ "next_action": "Rodar /mednotes:fix-wiki novamente para gerar um plano atualizado antes de aplicar.",
166
+ }
167
+ )
168
+ return JsonObjectAdapter.validate_python(
169
+ {
170
+ "status": "ready",
171
+ "blocked_reason": "",
172
+ "expected_snapshot_hash": expected,
173
+ "actual_snapshot_hash": actual,
174
+ }
175
+ )
176
+
177
+
178
+ def _problem_items(problems: Sequence[object]) -> list[FixWikiProblem]:
179
+ return [FixWikiProblem.model_validate(problem) for problem in problems]
180
+
181
+
182
+ def _taxonomy_items(items: Sequence[object]) -> list[FixWikiTaxonomyPlanItem]:
183
+ return [FixWikiTaxonomyPlanItem.model_validate(item) for item in items]
184
+
185
+
186
+ def _json_object_list(items: Sequence[JsonObject] | None) -> list[JsonObject]:
187
+ return [JsonObjectAdapter.validate_python(item) for item in (items or [])]
188
+
189
+
190
+ def build_fix_wiki_plan(
191
+ *,
192
+ run_id: str,
193
+ wiki_dir: Path,
194
+ snapshot_hash: str,
195
+ vocabulary_status: str,
196
+ problems: Sequence[object],
197
+ taxonomy_operations: Sequence[object],
198
+ taxonomy_blocked: Sequence[object],
199
+ taxonomy_decision_approved: bool,
200
+ snapshot_files: Sequence[JsonObject] | None = None,
201
+ context_packets: JsonObject | None = None,
202
+ git_state: JsonObject | None = None,
203
+ vocabulary_map_hash: str = "",
204
+ deferred_work_items: Sequence[JsonObject] | None = None,
205
+ ) -> JsonObject:
206
+ problem_items = _problem_items(problems)
207
+ taxonomy_operation_items = _taxonomy_items(taxonomy_operations)
208
+ taxonomy_blocked_items = _taxonomy_items(taxonomy_blocked)
209
+ vocabulary_reason = _vocabulary_blocked_reason(vocabulary_status)
210
+ context_artifacts = sorted(
211
+ {
212
+ problem.context_packet
213
+ for problem in problem_items
214
+ if problem.context_packet
215
+ }
216
+ )
217
+ taxonomy_artifacts = (
218
+ ["taxonomy-plan.json", *context_artifacts]
219
+ if taxonomy_operation_items or taxonomy_blocked_items
220
+ else context_artifacts
221
+ )
222
+
223
+ taxonomy_status = "ready"
224
+ taxonomy_can_apply = True
225
+ taxonomy_blocked_reason = ""
226
+ taxonomy_requires_decision = ""
227
+ taxonomy_requires_human = False
228
+ if taxonomy_blocked_items:
229
+ taxonomy_status = "blocked"
230
+ taxonomy_can_apply = False
231
+ taxonomy_blocked_reason = "taxonomy_plan_blocked"
232
+ elif taxonomy_operation_items and not taxonomy_decision_approved:
233
+ taxonomy_status = "needs_decision"
234
+ taxonomy_can_apply = False
235
+ taxonomy_blocked_reason = "taxonomy_decision_required"
236
+ taxonomy_requires_decision = "approve_taxonomy_moves"
237
+ taxonomy_requires_human = True
238
+
239
+ phases = [
240
+ _phase("preflight"),
241
+ _phase("inventory"),
242
+ _phase("vocabulary_bootstrap"),
243
+ _phase("vocabulary_map_diagnosis", status=vocabulary_status or "skipped", blocked_reason=vocabulary_reason),
244
+ _phase("style_yaml"),
245
+ _phase("provenance_backfill"),
246
+ _phase("hygiene"),
247
+ _phase(
248
+ "duplicates",
249
+ status="blocked" if vocabulary_status == "blocked_human" else "ready",
250
+ can_apply=vocabulary_status != "blocked_human",
251
+ blocked_reason="vocabulary_map_blocked" if vocabulary_status == "blocked_human" else "",
252
+ ),
253
+ _phase(
254
+ "taxonomy",
255
+ status=taxonomy_status,
256
+ can_apply=taxonomy_can_apply,
257
+ blocked_reason=taxonomy_blocked_reason,
258
+ requires_decision=taxonomy_requires_decision,
259
+ requires_human=taxonomy_requires_human,
260
+ affected_paths=[
261
+ item.source
262
+ for item in [*taxonomy_operation_items, *taxonomy_blocked_items]
263
+ if item.source
264
+ ],
265
+ planned_artifacts=taxonomy_artifacts,
266
+ rollback_strategy="taxonomy_receipt" if taxonomy_operation_items else "",
267
+ ),
268
+ _phase(
269
+ "linker",
270
+ status="blocked" if vocabulary_reason else "ready",
271
+ can_apply=not bool(vocabulary_reason),
272
+ blocked_reason=vocabulary_reason,
273
+ planned_artifacts=["link-trigger-context.json", "link-diagnosis.json"],
274
+ rollback_strategy="link-run-receipt",
275
+ ),
276
+ _phase("final_validation", can_apply=False),
277
+ ]
278
+
279
+ if taxonomy_blocked_items or vocabulary_status == "blocked_human":
280
+ status = "blocked"
281
+ elif any(problem.decision_required for problem in problem_items) or taxonomy_status == "needs_decision":
282
+ status = "needs_decision"
283
+ elif vocabulary_status == "blocked_pending":
284
+ status = "blocked"
285
+ else:
286
+ status = "ready"
287
+
288
+ plan = FixWikiPlan(
289
+ run_id=run_id,
290
+ wiki_dir=str(wiki_dir),
291
+ snapshot_hash=snapshot_hash,
292
+ snapshot_files=[FixWikiSnapshotFile.model_validate(item) for item in (snapshot_files or [])],
293
+ context_packet_hashes=_context_packet_hashes(context_packets),
294
+ git=JsonObjectAdapter.validate_python(git_state or {}),
295
+ vocabulary_map_hash=vocabulary_map_hash,
296
+ phase_order=list(ORDERED_FIX_WIKI_PHASES),
297
+ status=status,
298
+ problems=problem_items,
299
+ fix_wiki_problems=problem_items,
300
+ deferred_work_items=_json_object_list(deferred_work_items),
301
+ phases=phases,
302
+ blockers=[problem for problem in problem_items if problem.status == "blocked"],
303
+ warnings=[problem for problem in problem_items if problem.severity == "low"],
304
+ )
305
+ plan.plan_hash = _hash_plan(plan.to_payload())
306
+ return plan.to_payload()
@@ -0,0 +1,316 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
4
+
5
+ from mednotes.domains.wiki.contracts.agent_report import (
6
+ FixWikiGraphStatus,
7
+ FixWikiObjectiveStatus,
8
+ FixWikiPrimaryObjectiveSummary,
9
+ FixWikiRelatedNotesStatus,
10
+ )
11
+ from mednotes.kernel.base import JsonObject, JsonObjectAdapter, JsonValue
12
+
13
+ _INTERNAL_RELATED_NOTES_SKIP_REASONS = {
14
+ "linker_not_run",
15
+ "not_needed",
16
+ "no_link_trigger",
17
+ "no_related_notes_changes",
18
+ }
19
+
20
+
21
+ class _ObjectiveModel(BaseModel):
22
+ # These are partial typed lenses over larger payloads: unknown keys are
23
+ # ignored, but known operational fields remain strict and cannot be
24
+ # fabricated through Pydantic coercion.
25
+ model_config = ConfigDict(extra="ignore", populate_by_name=True, strict=True)
26
+
27
+
28
+ class _ObjectiveCounts(_ObjectiveModel):
29
+ mutated_files: int = 0
30
+ written_files: int = 0
31
+
32
+
33
+ class _ObjectiveProgress(_ObjectiveModel):
34
+ status: str = ""
35
+ counts: _ObjectiveCounts | None = None
36
+
37
+ @field_validator("counts", mode="before")
38
+ @classmethod
39
+ def _empty_counts(cls, value: object) -> object:
40
+ return value if isinstance(value, dict) else None
41
+
42
+
43
+ class _ObjectiveReceipt(_ObjectiveModel):
44
+ status: str = ""
45
+
46
+
47
+ class _ObjectiveApplyContext(_ObjectiveModel):
48
+ vault_changed_file_count: int = 0
49
+ written_count: int = 0
50
+
51
+
52
+ class _ObjectiveGraph(_ObjectiveModel):
53
+ error_count: int = 0
54
+ blocker_count: int = 0
55
+ blockers: JsonValue = 0
56
+
57
+
58
+ class _ObjectiveFinalValidation(_ObjectiveModel):
59
+ graph: _ObjectiveGraph | None = None
60
+
61
+
62
+ class _ObjectiveGraphAudit(_ObjectiveModel):
63
+ error_count: int = 0
64
+
65
+
66
+ class _ObjectiveRelatedNotesSync(_ObjectiveModel):
67
+ status: str = ""
68
+ blocked_reason: str = ""
69
+ skipped_reason: str = ""
70
+ applied_note_count: int = 0
71
+
72
+
73
+ class _ObjectiveLinkerApply(_ObjectiveModel):
74
+ related_notes_sync: _ObjectiveRelatedNotesSync | None = None
75
+
76
+
77
+ class _ObjectiveRelatedRecoveryState(_ObjectiveModel):
78
+ status: str = ""
79
+ blocked_reason: str = ""
80
+ remaining_count: int = 0
81
+ total_note_count: int = 0
82
+ fresh_record_count: int = 0
83
+ partial_record_count: int = 0
84
+
85
+
86
+ class _ObjectiveDiagnostic(_ObjectiveModel):
87
+ apply: _ObjectiveApplyContext | None = None
88
+ final_validation: _ObjectiveFinalValidation | None = None
89
+ graph_audit_before_linker: _ObjectiveGraphAudit | None = None
90
+ graph_error_count_before_linker: int | None = None
91
+ related_notes_recovery_state: _ObjectiveRelatedRecoveryState | None = None
92
+ related_notes_sync: _ObjectiveRelatedNotesSync | None = None
93
+ related_notes_applied: bool = False
94
+ linker_apply: _ObjectiveLinkerApply | None = None
95
+
96
+ @field_validator(
97
+ "apply",
98
+ "final_validation",
99
+ "graph_audit_before_linker",
100
+ "related_notes_recovery_state",
101
+ "related_notes_sync",
102
+ "linker_apply",
103
+ mode="before",
104
+ )
105
+ @classmethod
106
+ def _empty_optional_object(cls, value: object) -> object:
107
+ return value if isinstance(value, dict) else None
108
+
109
+
110
+ class _FixWikiObjectivePayload(_ObjectiveModel):
111
+ schema_id: str = Field(default="", alias="schema")
112
+ workflow: str = ""
113
+ progress_view_model: _ObjectiveProgress | None = None
114
+ receipt: _ObjectiveReceipt | None = None
115
+ diagnostic_context: _ObjectiveDiagnostic | None = None
116
+
117
+
118
+ def fix_wiki_primary_objective_summary(payload: JsonObject) -> FixWikiPrimaryObjectiveSummary | None:
119
+ payload = JsonObjectAdapter.validate_python(payload)
120
+ root = _FixWikiObjectivePayload.model_validate(payload)
121
+ if not _is_fix_wiki_payload(root):
122
+ return None
123
+
124
+ progress = root.progress_view_model or _ObjectiveProgress()
125
+ receipt = root.receipt or _ObjectiveReceipt()
126
+ diagnostic = root.diagnostic_context or _ObjectiveDiagnostic()
127
+ status = _first_status(progress.status, receipt.status)
128
+
129
+ mutation_count, written_count = _mutation_counts(
130
+ progress=progress,
131
+ diagnostic=diagnostic,
132
+ )
133
+ graph_status, graph_summary = _graph_outcome(diagnostic=diagnostic)
134
+ related_status, related_summary = _related_notes_outcome(diagnostic=diagnostic)
135
+ wiki_fixed, wiki_summary = _wiki_outcome(status=status, graph_status=graph_status, related_status=related_status)
136
+
137
+ return FixWikiPrimaryObjectiveSummary(
138
+ wiki_fixed=wiki_fixed,
139
+ wiki_summary=wiki_summary,
140
+ mutation_count=mutation_count,
141
+ written_count=written_count,
142
+ mutation_summary=_mutation_summary(mutation_count=mutation_count, written_count=written_count),
143
+ graph_status=graph_status,
144
+ graph_summary=graph_summary,
145
+ related_notes_status=related_status,
146
+ related_notes_summary=related_summary,
147
+ )
148
+
149
+
150
+ def _is_fix_wiki_payload(payload: _FixWikiObjectivePayload) -> bool:
151
+ return payload.workflow == "/mednotes:fix-wiki" or payload.schema_id == "medical-notes-workbench.fix-wiki-fsm-result.v1"
152
+
153
+
154
+ def _mutation_counts(
155
+ *,
156
+ progress: _ObjectiveProgress,
157
+ diagnostic: _ObjectiveDiagnostic,
158
+ ) -> tuple[int, int]:
159
+ mutation_count = (
160
+ _as_int((progress.counts or _ObjectiveCounts()).mutated_files)
161
+ or _as_int((diagnostic.apply or _ObjectiveApplyContext()).vault_changed_file_count)
162
+ )
163
+ written_count = (
164
+ _as_int((progress.counts or _ObjectiveCounts()).written_files)
165
+ or _as_int((diagnostic.apply or _ObjectiveApplyContext()).written_count)
166
+ )
167
+ return mutation_count, written_count
168
+
169
+
170
+ def _mutation_summary(*, mutation_count: int, written_count: int) -> str:
171
+ if mutation_count == 0 and written_count == 0:
172
+ return "Nenhum arquivo da Wiki foi alterado nesta etapa."
173
+ if mutation_count == written_count:
174
+ return f"{mutation_count} arquivo(s) da Wiki alterado(s)."
175
+ return f"{mutation_count} arquivo(s) da Wiki mudaram; {written_count} arquivo(s) foram gravados pelo reparo automático."
176
+
177
+
178
+ def _graph_outcome(
179
+ *,
180
+ diagnostic: _ObjectiveDiagnostic,
181
+ ) -> tuple[FixWikiGraphStatus, str]:
182
+ final_validation = diagnostic.final_validation or _ObjectiveFinalValidation()
183
+ graph = final_validation.graph
184
+ if graph is None and diagnostic.graph_error_count_before_linker is None and diagnostic.graph_audit_before_linker is None:
185
+ return "unknown", "O payload não trouxe comparação suficiente do grafo."
186
+ graph = graph or _ObjectiveGraph()
187
+ after_errors = _as_int(graph.error_count)
188
+ after_blockers = _as_int(graph.blocker_count or graph.blockers)
189
+ if diagnostic.graph_error_count_before_linker is not None:
190
+ before_errors = _optional_int(diagnostic.graph_error_count_before_linker)
191
+ elif diagnostic.graph_audit_before_linker is not None:
192
+ before_errors = _optional_int(diagnostic.graph_audit_before_linker.error_count)
193
+ else:
194
+ before_errors = None
195
+
196
+ if before_errors is not None:
197
+ if after_errors < before_errors:
198
+ return "improved", f"O grafo melhorou: {before_errors} erro(s) antes, {after_errors} depois."
199
+ if after_errors > before_errors:
200
+ return "worse", f"O grafo piorou: {before_errors} erro(s) antes, {after_errors} depois."
201
+ if after_errors == 0:
202
+ return "clean", "O grafo terminou sem erros."
203
+ return "unchanged", f"O grafo não melhorou: permaneceu com {after_errors} erro(s)."
204
+
205
+ if after_errors == 0 and after_blockers == 0:
206
+ return "clean", "O grafo terminou sem bloqueios."
207
+ if after_errors or after_blockers:
208
+ count = after_blockers or after_errors
209
+ return "blocked", f"O grafo ainda tem {count} bloqueio(s)."
210
+ return "unknown", "O payload não trouxe comparação suficiente do grafo."
211
+
212
+
213
+ def _related_notes_outcome(
214
+ *,
215
+ diagnostic: _ObjectiveDiagnostic,
216
+ ) -> tuple[FixWikiRelatedNotesStatus, str]:
217
+ recovery_state = diagnostic.related_notes_recovery_state or _ObjectiveRelatedRecoveryState()
218
+ related_sync = diagnostic.related_notes_sync or _ObjectiveRelatedNotesSync()
219
+ linker_apply = diagnostic.linker_apply or _ObjectiveLinkerApply()
220
+ linker_related = linker_apply.related_notes_sync or _ObjectiveRelatedNotesSync()
221
+ if not _related_notes_sync_has_signal(related_sync) and _related_notes_sync_has_signal(linker_related):
222
+ related_sync = linker_related
223
+
224
+ remaining = _as_int(recovery_state.remaining_count)
225
+ total = _as_int(recovery_state.total_note_count)
226
+ fresh = _as_int(recovery_state.fresh_record_count or recovery_state.partial_record_count)
227
+ recovery_reason = recovery_state.blocked_reason
228
+ if recovery_state.status == "waiting_for_retry" or remaining:
229
+ reason = "cota externa" if "quota" in recovery_reason or "cota" in recovery_reason else "condição externa"
230
+ count = f" ({fresh}/{total}, faltam {remaining})" if total else ""
231
+ return "pending", f"Notas Relacionadas ficou pendente por {reason}{count}."
232
+
233
+ if related_sync.status == "completed" or diagnostic.related_notes_applied is True:
234
+ applied = _as_int(related_sync.applied_note_count)
235
+ if applied:
236
+ return "updated", f"Notas Relacionadas foi atualizado em {applied} nota(s)."
237
+ return "updated", "Notas Relacionadas foi atualizado."
238
+
239
+ blocked_reason = related_sync.blocked_reason
240
+ if related_sync.status == "blocked" or blocked_reason:
241
+ return "blocked", f"Notas Relacionadas ficou bloqueado: {blocked_reason or 'bloqueio não especificado'}."
242
+
243
+ skipped_reason = related_sync.skipped_reason
244
+ if related_sync.status == "skipped" or skipped_reason:
245
+ reason = _public_related_notes_skip_reason(skipped_reason)
246
+ return "skipped", f"Notas Relacionadas não foi alterado: {reason}."
247
+
248
+ return "unknown", "O payload não confirmou atualização de Notas Relacionadas."
249
+
250
+
251
+ def _public_related_notes_skip_reason(value: str) -> str:
252
+ """Translate internal skip codes before they reach agent-facing reports."""
253
+
254
+ if not value:
255
+ return "fora do escopo desta etapa"
256
+ if value.strip().casefold() in _INTERNAL_RELATED_NOTES_SKIP_REASONS:
257
+ return "fora do escopo desta etapa"
258
+ return value
259
+
260
+
261
+ def _wiki_outcome(
262
+ *,
263
+ status: str,
264
+ graph_status: FixWikiGraphStatus,
265
+ related_status: FixWikiRelatedNotesStatus,
266
+ ) -> tuple[FixWikiObjectiveStatus, str]:
267
+ if status == "completed":
268
+ return "yes", "Sim, o fix-wiki concluiu a correção da Wiki."
269
+ if status == "completed_with_warnings":
270
+ return "partial", "A Wiki foi corrigida com avisos pendentes."
271
+ if status == "waiting_external":
272
+ return "waiting_external", "A Wiki ficou parcialmente corrigida e aguarda uma condição externa."
273
+ if status == "waiting_agent":
274
+ return "waiting_agent", "A Wiki ficou parcialmente corrigida e aguarda continuação assistida pelo agente."
275
+ if status == "failed":
276
+ return "failed", "Não, o fix-wiki falhou antes de concluir."
277
+ if status in {"blocked", "waiting_human"}:
278
+ return "no", "Não, o fix-wiki ainda não concluiu a correção da Wiki."
279
+ if graph_status in {"clean", "improved"} and related_status == "updated":
280
+ return "yes", "Sim, os sinais principais indicam Wiki corrigida."
281
+ return "unknown", "O payload não permite confirmar se o fix-wiki concluiu a Wiki."
282
+
283
+
284
+ def _related_notes_sync_has_signal(sync: _ObjectiveRelatedNotesSync) -> bool:
285
+ return bool(sync.status or sync.blocked_reason or sync.skipped_reason or sync.applied_note_count)
286
+
287
+
288
+ def _first_status(*sources: str) -> str:
289
+ for value in sources:
290
+ if value:
291
+ return value
292
+ return ""
293
+
294
+
295
+ def _as_int(value: object) -> int:
296
+ if value is None:
297
+ return 0
298
+ if isinstance(value, bool):
299
+ raise ValueError("numeric fix-wiki fields must be numbers, not booleans")
300
+ if isinstance(value, str):
301
+ raise ValueError("numeric fix-wiki fields must be numbers, not strings")
302
+ if isinstance(value, int | float):
303
+ return max(0, int(value))
304
+ raise ValueError("numeric fix-wiki fields must be numbers")
305
+
306
+
307
+ def _optional_int(value: object) -> int | None:
308
+ if value is None:
309
+ return None
310
+ if isinstance(value, bool):
311
+ raise ValueError("numeric fix-wiki fields must be numbers, not booleans")
312
+ if isinstance(value, str):
313
+ raise ValueError("numeric fix-wiki fields must be numbers, not strings")
314
+ if isinstance(value, int | float):
315
+ return max(0, int(value))
316
+ raise ValueError("numeric fix-wiki fields must be numbers")