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,632 @@
1
+ """Apply med-link-graph-curator semantic ingestion items to the vocabulary DB."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import math
6
+ import sqlite3
7
+ from pathlib import Path
8
+
9
+ from pydantic import ValidationError as PydanticValidationError
10
+
11
+ from mednotes.domains.wiki.capabilities.notes.note_style.frontmatter import infer_title, split_frontmatter
12
+ from mednotes.domains.wiki.capabilities.vocabulary.vocabulary_map import (
13
+ initialize_vocabulary_db,
14
+ meaning_id_for,
15
+ note_content_hash,
16
+ upsert_meaning,
17
+ upsert_note,
18
+ upsert_policy,
19
+ upsert_surface,
20
+ )
21
+ from mednotes.domains.wiki.common import ValidationError
22
+ from mednotes.domains.wiki.contracts.vocabulary_ingestion import (
23
+ INGESTION_RECEIPT_SCHEMA,
24
+ INGESTION_SCHEMA,
25
+ AtomicityBodySizeStats,
26
+ AtomicityDeferredWorkDecision,
27
+ AtomicityDeferredWorkEvaluation,
28
+ AtomicityDeferredWorkPreflight,
29
+ AtomicitySemanticSignal,
30
+ SemanticAlias,
31
+ SemanticDeferredWorkItem,
32
+ SemanticIngestionIdentity,
33
+ SemanticIngestionItem,
34
+ SemanticPrimaryMeaning,
35
+ )
36
+ from mednotes.domains.wiki.contracts.workflow_guardrails import error_context
37
+ from mednotes.kernel.base import JsonObject, JsonObjectAdapter
38
+
39
+ __all__ = ["INGESTION_RECEIPT_SCHEMA", "INGESTION_SCHEMA", "apply_semantic_ingestion"]
40
+
41
+ ALLOWED_ATOMIC_STATUSES = {"atomic", "suspected_non_atomic", "duplicate_candidate", "unknown"}
42
+ ATOMICITY_DEFERRED_REASONS = {"non_atomic_note", "one_note_multiple_meanings"}
43
+ ATOMICITY_EVIDENCE_WEIGHTS = {
44
+ "multiple_canonical_entities": 0.30,
45
+ "different_entity_types": 0.25,
46
+ "independent_definition_blocks": 0.20,
47
+ "independent_management_blocks": 0.20,
48
+ "independent_pathophysiology_blocks": 0.15,
49
+ "separable_sections": 0.15,
50
+ "linker_ambiguity": 0.15,
51
+ }
52
+ MIN_ATOMICITY_CHILD_BODY_CHARS = 240
53
+
54
+
55
+ def _bounded_float(value: object, *, default: float = 0.0) -> float:
56
+ if isinstance(value, bool) or not isinstance(value, int | float | str):
57
+ parsed = default
58
+ else:
59
+ try:
60
+ parsed = float(value)
61
+ except ValueError:
62
+ parsed = default
63
+ return max(0.0, min(1.0, parsed))
64
+
65
+
66
+ def _body_char_count_from_text(text: str) -> int:
67
+ _frontmatter, body = split_frontmatter(text)
68
+ lines = body.splitlines()
69
+ if lines and lines[0].startswith("# "):
70
+ lines = lines[1:]
71
+ return len("\n".join(lines).strip())
72
+
73
+
74
+ def _body_char_count(path: Path) -> int:
75
+ try:
76
+ return _body_char_count_from_text(path.read_text(encoding="utf-8"))
77
+ except OSError:
78
+ return 0
79
+
80
+
81
+ def _body_size_stats(conn: sqlite3.Connection, *, current_note_path: Path) -> AtomicityBodySizeStats:
82
+ counts: list[int] = []
83
+ seen: set[str] = set()
84
+ for (path_text,) in conn.execute("SELECT path FROM notes WHERE status = 'active'").fetchall():
85
+ path = Path(str(path_text))
86
+ if str(path) in seen:
87
+ continue
88
+ seen.add(str(path))
89
+ count = _body_char_count(path)
90
+ if count:
91
+ counts.append(count)
92
+ if str(current_note_path) not in seen:
93
+ count = _body_char_count(current_note_path)
94
+ if count:
95
+ counts.append(count)
96
+ if not counts:
97
+ return AtomicityBodySizeStats(min_child_body_chars=MIN_ATOMICITY_CHILD_BODY_CHARS)
98
+ mean = sum(counts) / len(counts)
99
+ variance = sum((count - mean) ** 2 for count in counts) / len(counts)
100
+ stddev = math.sqrt(variance)
101
+ current_count = _body_char_count(current_note_path)
102
+ threshold = mean + stddev
103
+ return AtomicityBodySizeStats(
104
+ sample_count=len(counts),
105
+ mean_body_chars=mean,
106
+ stddev_body_chars=stddev,
107
+ long_note_threshold_chars=threshold,
108
+ current_body_chars=current_count,
109
+ current_above_one_stddev=current_count > threshold,
110
+ min_child_body_chars=max(MIN_ATOMICITY_CHILD_BODY_CHARS, int(mean * 0.25)),
111
+ )
112
+
113
+
114
+ def _atomicity_signal_score(signal: AtomicitySemanticSignal) -> tuple[float, list[str], int, set[str]]:
115
+ evidence = [item for item in signal.evidence if item]
116
+ concepts = signal.concepts
117
+ concept_types = {concept.semantic_type for concept in concepts}
118
+ weighted_score = sum(ATOMICITY_EVIDENCE_WEIGHTS.get(item, 0.0) for item in set(evidence))
119
+ if len(concepts) >= 2:
120
+ weighted_score += ATOMICITY_EVIDENCE_WEIGHTS["multiple_canonical_entities"]
121
+ if len({item for item in concept_types if item}) >= 2:
122
+ weighted_score += ATOMICITY_EVIDENCE_WEIGHTS["different_entity_types"]
123
+ explicit_score = _bounded_float(signal.score)
124
+ return min(1.0, max(weighted_score, explicit_score)), evidence, len(concepts), concept_types
125
+
126
+
127
+ def _child_body_counts(signal: AtomicitySemanticSignal) -> list[int]:
128
+ counts: list[int] = []
129
+ for child in signal.child_note_estimates:
130
+ value = child.first_body_count()
131
+ if value is not None:
132
+ counts.append(max(0, value))
133
+ return counts
134
+
135
+
136
+ def _evaluate_atomicity_deferred_work_item(
137
+ *,
138
+ raw_work: SemanticDeferredWorkItem,
139
+ body_size_stats: AtomicityBodySizeStats,
140
+ ) -> AtomicityDeferredWorkEvaluation:
141
+ signal = raw_work.semantic_signal
142
+ if signal is None:
143
+ return AtomicityDeferredWorkEvaluation(
144
+ status="blocked",
145
+ blocked_reason="semantic_ingestion.atomicity_signal_required",
146
+ decision="human_decision_required",
147
+ message="Atomicity deferred work requires semantic_signal evidence from the note body.",
148
+ )
149
+ score, evidence, concept_count, _concept_types = _atomicity_signal_score(signal)
150
+ if not evidence or concept_count < 2:
151
+ return AtomicityDeferredWorkEvaluation(
152
+ status="blocked",
153
+ blocked_reason="semantic_ingestion.atomicity_signal_required",
154
+ decision="human_decision_required",
155
+ message="semantic_signal must include audit evidence and at least two developed concepts.",
156
+ semantic_score=score,
157
+ evidence=evidence,
158
+ concept_count=concept_count,
159
+ )
160
+ relationship_score = _bounded_float(signal.relationship_score)
161
+ explicit_fragment_risk = signal.fragment_risk.strip().casefold()
162
+ child_counts = _child_body_counts(signal)
163
+ min_child_chars = body_size_stats.min_child_body_chars or MIN_ATOMICITY_CHILD_BODY_CHARS
164
+ child_fragment_risk = bool(child_counts and min(child_counts) < min_child_chars)
165
+ fragment_risk = "high" if explicit_fragment_risk == "high" or child_fragment_risk else explicit_fragment_risk or "unknown"
166
+ if relationship_score >= 0.75:
167
+ decision = "relationship_note_valid"
168
+ elif score >= 0.75 and fragment_risk != "high":
169
+ decision = "split_required"
170
+ elif score >= 0.75:
171
+ decision = "split_deferred_fragment_risk"
172
+ elif score >= 0.45 or (body_size_stats.current_above_one_stddev and score >= 0.35):
173
+ decision = "split_candidate"
174
+ else:
175
+ decision = "no_action"
176
+ db_status = "pending" if decision == "split_required" else "cancelled"
177
+ return AtomicityDeferredWorkEvaluation(
178
+ status="ready",
179
+ decision=decision,
180
+ db_status=db_status,
181
+ source="db_semantic_signal_gate",
182
+ semantic_score=score,
183
+ semantic_threshold=0.75,
184
+ evidence=evidence,
185
+ concept_count=concept_count,
186
+ relationship_score=relationship_score,
187
+ fragmentation_gate=JsonObjectAdapter.validate_python({
188
+ "fragment_risk": fragment_risk,
189
+ "child_body_char_counts": child_counts,
190
+ "min_child_body_chars": min_child_chars,
191
+ }),
192
+ body_size_gate=body_size_stats.to_payload(),
193
+ )
194
+
195
+
196
+ def _atomicity_deferred_work_preflight(
197
+ *,
198
+ db_path: Path,
199
+ item: SemanticIngestionItem,
200
+ note_path: Path,
201
+ content_hash: str,
202
+ ) -> AtomicityDeferredWorkPreflight:
203
+ decisions: list[AtomicityDeferredWorkDecision] = []
204
+ evaluations: dict[str, AtomicityDeferredWorkEvaluation] = {}
205
+ atomicity_work_items = [
206
+ raw_work
207
+ for raw_work in item.deferred_work_items
208
+ if raw_work.reason in ATOMICITY_DEFERRED_REASONS
209
+ ]
210
+ if not atomicity_work_items:
211
+ return AtomicityDeferredWorkPreflight(status="ready")
212
+ initialize_vocabulary_db(db_path)
213
+ with sqlite3.connect(db_path) as conn:
214
+ body_size_stats = _body_size_stats(conn, current_note_path=note_path)
215
+ for raw_work in atomicity_work_items:
216
+ work_id = raw_work.effective_work_id(fallback_stem=note_path.stem)
217
+ evaluation = _evaluate_atomicity_deferred_work_item(raw_work=raw_work, body_size_stats=body_size_stats)
218
+ if evaluation.status == "blocked":
219
+ _mark_queue_status(db_path=db_path, note_path=note_path, content_hash=content_hash, status="blocked")
220
+ next_action = (
221
+ "Regenerar o note-semantic-ingestion.v1 com semantic_signal auditável no corpo da nota; "
222
+ "o DB só cria split pendente quando a evidência passa o gate semântico e anti-fragmentação."
223
+ )
224
+ blocked_reason = evaluation.blocked_reason or "semantic_ingestion.atomicity_signal_required"
225
+ return AtomicityDeferredWorkPreflight(
226
+ status="blocked",
227
+ blocked_reason=blocked_reason,
228
+ note_path=str(note_path),
229
+ content_hash=content_hash,
230
+ work_id=work_id,
231
+ atomicity_evaluation=evaluation,
232
+ next_action=next_action,
233
+ error_context=error_context(
234
+ phase="semantic_ingestion",
235
+ blocked_reason=blocked_reason,
236
+ root_cause=blocked_reason,
237
+ affected_artifact="deferred_work_items",
238
+ error_summary=evaluation.message or "Atomicity deferred work lacks auditable semantic signal.",
239
+ suggested_fix=next_action,
240
+ next_action=next_action,
241
+ retry_scope="single_curator_work_item",
242
+ affected_items=[str(note_path)],
243
+ ),
244
+ )
245
+ evaluations[work_id] = evaluation
246
+ decisions.append(
247
+ AtomicityDeferredWorkDecision(
248
+ work_id=work_id,
249
+ decision=evaluation.decision or "human_decision_required",
250
+ status=evaluation.db_status or "blocked",
251
+ )
252
+ )
253
+ return AtomicityDeferredWorkPreflight(status="ready", evaluations=evaluations, decisions=decisions)
254
+
255
+
256
+ def _mark_queue_status(
257
+ *,
258
+ db_path: Path,
259
+ note_path: Path,
260
+ content_hash: str,
261
+ status: str,
262
+ conn: sqlite3.Connection | None = None,
263
+ ) -> None:
264
+ if conn is not None:
265
+ conn.execute(
266
+ """
267
+ UPDATE note_semantic_ingestion_queue
268
+ SET status=?, updated_at=CURRENT_TIMESTAMP
269
+ WHERE note_path = ? AND content_hash = ? AND status IN ('pending', 'claimed')
270
+ """,
271
+ (status, str(note_path), content_hash),
272
+ )
273
+ return
274
+ initialize_vocabulary_db(db_path)
275
+ with sqlite3.connect(db_path) as owned_conn:
276
+ _mark_queue_status(db_path=db_path, note_path=note_path, content_hash=content_hash, status=status, conn=owned_conn)
277
+
278
+
279
+ def _format_validation_location(location: tuple[object, ...]) -> str:
280
+ path = ""
281
+ for part in location:
282
+ if isinstance(part, int):
283
+ path += f"[{part}]"
284
+ else:
285
+ path = f"{path}.{part}" if path else str(part)
286
+ return path or "$"
287
+
288
+
289
+ def _semantic_ingestion_contract_error(exc: PydanticValidationError) -> str:
290
+ details = "; ".join(
291
+ f"{_format_validation_location(tuple(error.get('loc', ()) or ()))}: {error.get('msg', 'invalid')}"
292
+ for error in exc.errors()
293
+ )
294
+ return f"semantic ingestion contract invalid: {details}"
295
+
296
+
297
+ def _semantic_ingestion_identity(value: object) -> SemanticIngestionIdentity:
298
+ try:
299
+ return SemanticIngestionIdentity.model_validate(value)
300
+ except PydanticValidationError:
301
+ return SemanticIngestionIdentity()
302
+
303
+
304
+ def _semantic_ingestion_item(value: object, *, db_path: Path, conn: sqlite3.Connection | None) -> SemanticIngestionItem:
305
+ identity = _semantic_ingestion_identity(value)
306
+ try:
307
+ return SemanticIngestionItem.model_validate(value)
308
+ except PydanticValidationError as exc:
309
+ if identity.note_path is not None and identity.content_hash:
310
+ _mark_queue_status(
311
+ db_path=db_path,
312
+ note_path=identity.note_path,
313
+ content_hash=identity.content_hash,
314
+ status="blocked",
315
+ conn=conn,
316
+ )
317
+ raise ValidationError(_semantic_ingestion_contract_error(exc)) from exc
318
+
319
+
320
+ def apply_semantic_ingestion(
321
+ *,
322
+ db_path: Path,
323
+ item: object,
324
+ require_contract: bool = True,
325
+ conn: sqlite3.Connection | None = None,
326
+ ) -> JsonObject:
327
+ del require_contract # Kept for older callers; validation is no longer optional.
328
+ typed_item = _semantic_ingestion_item(item, db_path=db_path, conn=conn)
329
+ note_path = typed_item.note_path
330
+ expected_hash = typed_item.content_hash
331
+ primary = typed_item.primary_meaning
332
+ try:
333
+ return _apply_semantic_ingestion_unchecked(db_path=db_path, item=typed_item, conn=conn)
334
+ except sqlite3.IntegrityError as exc:
335
+ if str(note_path) and expected_hash:
336
+ _mark_queue_status(db_path=db_path, note_path=note_path, content_hash=expected_hash, status="blocked", conn=conn)
337
+ root_cause = "blocked.integrityerror"
338
+ summary = _integrity_error_summary(exc)
339
+ next_action = (
340
+ "Resolve duplicate meaning merge or atomicity split via the official plan/apply "
341
+ "workflow before retrying."
342
+ )
343
+ return {
344
+ "schema": INGESTION_RECEIPT_SCHEMA,
345
+ "status": "blocked",
346
+ "blocked_reason": "semantic_ingestion.integrity_conflict",
347
+ "note_path": str(note_path),
348
+ "content_hash": expected_hash,
349
+ "meaning_id": primary.id,
350
+ "error_type": "IntegrityError",
351
+ "error_summary": summary,
352
+ "next_action": next_action,
353
+ "diagnostic_context": {
354
+ "root_cause_code": root_cause,
355
+ "traceback_summary": summary,
356
+ "recovery_command": next_action,
357
+ },
358
+ "error_context": error_context(
359
+ phase="semantic_ingestion",
360
+ blocked_reason="semantic_ingestion.integrity_conflict",
361
+ root_cause=root_cause,
362
+ affected_artifact="vocabulary_db",
363
+ error_summary=summary,
364
+ suggested_fix=next_action,
365
+ next_action=next_action,
366
+ retry_scope="semantic_ingestion_conflict_resolution",
367
+ affected_items=[str(note_path)] if str(note_path) else None,
368
+ ),
369
+ }
370
+
371
+
372
+ def _integrity_error_summary(exc: sqlite3.IntegrityError) -> str:
373
+ text = str(exc)
374
+ if "UNIQUE constraint" in text or "CHECK constraint" in text or "FOREIGN KEY constraint" in text:
375
+ text = "SQLite integrity constraint failed during semantic ingestion."
376
+ return f"{exc.__class__.__name__}: {text}"[:1000]
377
+
378
+
379
+ def _apply_semantic_ingestion_unchecked(
380
+ *,
381
+ db_path: Path,
382
+ item: SemanticIngestionItem,
383
+ conn: sqlite3.Connection | None = None,
384
+ ) -> JsonObject:
385
+ note_path = item.note_path
386
+ if not note_path.is_file():
387
+ raise ValidationError(f"semantic ingestion note_path not found: {note_path}")
388
+ actual_hash = note_content_hash(note_path)
389
+ expected_hash = item.content_hash
390
+ if expected_hash != actual_hash:
391
+ _mark_queue_status(db_path=db_path, note_path=note_path, content_hash=expected_hash, status="stale", conn=conn)
392
+ return {
393
+ "schema": INGESTION_RECEIPT_SCHEMA,
394
+ "status": "blocked",
395
+ "blocked_reason": "semantic_ingestion.stale_note_hash",
396
+ "note_path": str(note_path),
397
+ "expected_hash": item.content_hash,
398
+ "actual_hash": actual_hash,
399
+ }
400
+
401
+ primary = item.primary_meaning
402
+ meaning_id = primary.id or meaning_id_for(primary.label)
403
+ atomic_status = primary.atomic_status or "atomic"
404
+ if atomic_status not in ALLOWED_ATOMIC_STATUSES:
405
+ _mark_queue_status(db_path=db_path, note_path=note_path, content_hash=actual_hash, status="blocked", conn=conn)
406
+ return {
407
+ "schema": INGESTION_RECEIPT_SCHEMA,
408
+ "status": "blocked",
409
+ "blocked_reason": "semantic_ingestion.invalid_atomic_status",
410
+ "note_path": str(note_path),
411
+ "content_hash": actual_hash,
412
+ "meaning_id": meaning_id,
413
+ "atomic_status": atomic_status,
414
+ "allowed_atomic_statuses": sorted(ALLOWED_ATOMIC_STATUSES),
415
+ }
416
+ text = note_path.read_text(encoding="utf-8")
417
+ atomicity_preflight = _atomicity_deferred_work_preflight(
418
+ db_path=db_path,
419
+ item=item,
420
+ note_path=note_path,
421
+ content_hash=actual_hash,
422
+ )
423
+ if atomicity_preflight.status == "blocked":
424
+ return {
425
+ "schema": INGESTION_RECEIPT_SCHEMA,
426
+ "status": "blocked",
427
+ **atomicity_preflight.to_payload(),
428
+ }
429
+ atomicity_evaluations = atomicity_preflight.evaluations
430
+ deferred_atomicity_decisions = atomicity_preflight.decisions
431
+
432
+ if conn is None:
433
+ initialize_vocabulary_db(db_path)
434
+ with sqlite3.connect(db_path) as owned_conn:
435
+ return _apply_semantic_ingestion_db_ops(
436
+ conn=owned_conn,
437
+ db_path=db_path,
438
+ item=item,
439
+ note_path=note_path,
440
+ actual_hash=actual_hash,
441
+ primary=primary,
442
+ meaning_id=meaning_id,
443
+ atomic_status=atomic_status,
444
+ text=text,
445
+ aliases=item.aliases,
446
+ atomicity_evaluations=atomicity_evaluations,
447
+ deferred_atomicity_decisions=deferred_atomicity_decisions,
448
+ )
449
+ return _apply_semantic_ingestion_db_ops(
450
+ conn=conn,
451
+ db_path=db_path,
452
+ item=item,
453
+ note_path=note_path,
454
+ actual_hash=actual_hash,
455
+ primary=primary,
456
+ meaning_id=meaning_id,
457
+ atomic_status=atomic_status,
458
+ text=text,
459
+ aliases=item.aliases,
460
+ atomicity_evaluations=atomicity_evaluations,
461
+ deferred_atomicity_decisions=deferred_atomicity_decisions,
462
+ )
463
+
464
+
465
+ def _apply_semantic_ingestion_db_ops(
466
+ *,
467
+ conn: sqlite3.Connection,
468
+ db_path: Path,
469
+ item: SemanticIngestionItem,
470
+ note_path: Path,
471
+ actual_hash: str,
472
+ primary: SemanticPrimaryMeaning,
473
+ meaning_id: str,
474
+ atomic_status: str,
475
+ text: str,
476
+ aliases: list[SemanticAlias],
477
+ atomicity_evaluations: dict[str, AtomicityDeferredWorkEvaluation],
478
+ deferred_atomicity_decisions: list[AtomicityDeferredWorkDecision],
479
+ ) -> JsonObject:
480
+ note_id = upsert_note(conn, path=note_path, title=infer_title(text, note_path), content_hash=actual_hash)
481
+ existing_note_link = conn.execute(
482
+ """
483
+ SELECT l.meaning_id, m.label
484
+ FROM meaning_note_links l
485
+ LEFT JOIN meanings m ON m.id = l.meaning_id
486
+ WHERE l.note_id = ? AND l.role = 'canonical' AND l.status = 'active'
487
+ """,
488
+ (note_id,),
489
+ ).fetchone()
490
+ if existing_note_link and str(existing_note_link[0]) != meaning_id:
491
+ existing_meaning_label = existing_note_link[1] if isinstance(existing_note_link[1], str) else ""
492
+ conn.execute(
493
+ """
494
+ UPDATE note_semantic_ingestion_queue
495
+ SET status='blocked', updated_at=CURRENT_TIMESTAMP
496
+ WHERE note_path = ? AND content_hash = ? AND status IN ('pending', 'claimed')
497
+ """,
498
+ (str(note_path), actual_hash),
499
+ )
500
+ return {
501
+ "schema": INGESTION_RECEIPT_SCHEMA,
502
+ "status": "blocked",
503
+ "blocked_reason": "semantic_ingestion.meaning_note_conflict",
504
+ "note_path": str(note_path),
505
+ "content_hash": actual_hash,
506
+ "existing_meaning_id": str(existing_note_link[0]),
507
+ "existing_meaning_label": existing_meaning_label,
508
+ "proposed_meaning_id": meaning_id,
509
+ "proposed_meaning_label": primary.label,
510
+ }
511
+ upsert_meaning(
512
+ conn,
513
+ meaning_id=meaning_id,
514
+ label=primary.label,
515
+ semantic_type=primary.semantic_type or "medical_concept",
516
+ atomic_status=atomic_status,
517
+ )
518
+ existing_canonical = conn.execute(
519
+ """
520
+ SELECT l.note_id, n.path
521
+ FROM meaning_note_links l
522
+ JOIN notes n ON n.id = l.note_id
523
+ WHERE l.meaning_id = ? AND l.role = 'canonical' AND l.status = 'active'
524
+ """,
525
+ (meaning_id,),
526
+ ).fetchone()
527
+ idempotent = bool(existing_canonical and int(existing_canonical[0]) == int(note_id))
528
+ if existing_canonical and not idempotent:
529
+ conn.execute(
530
+ """
531
+ UPDATE note_semantic_ingestion_queue
532
+ SET status='blocked', updated_at=CURRENT_TIMESTAMP
533
+ WHERE note_path = ? AND content_hash = ? AND status IN ('pending', 'claimed')
534
+ """,
535
+ (str(note_path), actual_hash),
536
+ )
537
+ return {
538
+ "schema": INGESTION_RECEIPT_SCHEMA,
539
+ "status": "blocked",
540
+ "blocked_reason": "semantic_ingestion.meaning_canonical_conflict",
541
+ "note_path": str(note_path),
542
+ "content_hash": actual_hash,
543
+ "meaning_id": meaning_id,
544
+ "existing_note_id": int(existing_canonical[0]),
545
+ "existing_note_path": str(existing_canonical[1]),
546
+ "proposed_note_id": int(note_id),
547
+ "proposed_note_path": str(note_path),
548
+ }
549
+ conn.execute(
550
+ """
551
+ INSERT INTO meaning_note_links(meaning_id, note_id, role, status, confidence)
552
+ VALUES (?, ?, 'canonical', 'active', ?)
553
+ ON CONFLICT(meaning_id, note_id, role) DO UPDATE SET
554
+ status='active',
555
+ confidence=excluded.confidence,
556
+ updated_at=CURRENT_TIMESTAMP
557
+ """,
558
+ (meaning_id, note_id, item.confidence),
559
+ )
560
+ applied_aliases = 0
561
+ for alias in aliases:
562
+ source = alias.source or item.source or "curator"
563
+ if source not in {"curator", "yaml", "projection", "human", "llm", "system"}:
564
+ source = "curator"
565
+ surface_id = upsert_surface(
566
+ conn,
567
+ display_text=alias.text,
568
+ intrinsically_ambiguous=alias.intrinsically_ambiguous,
569
+ ambiguity_reason=", ".join(value for value in alias.ambiguous_with if value),
570
+ )
571
+ upsert_policy(
572
+ conn,
573
+ surface_id=surface_id,
574
+ meaning_id=meaning_id,
575
+ link_policy=alias.link_policy or "requires_context",
576
+ display_text=alias.text,
577
+ visible_in_yaml=alias.visible_in_yaml,
578
+ source=source,
579
+ confidence=item.confidence,
580
+ )
581
+ applied_aliases += 1
582
+
583
+ for raw_work in item.deferred_work_items:
584
+ work_id = raw_work.effective_work_id(fallback_stem=note_path.stem)
585
+ atomicity_evaluation = atomicity_evaluations[work_id] if work_id in atomicity_evaluations else None
586
+ payload = raw_work.to_payload()
587
+ status = raw_work.status or "pending"
588
+ if atomicity_evaluation is not None:
589
+ payload["atomicity_decision"] = atomicity_evaluation.decision or "human_decision_required"
590
+ payload["atomicity_evaluation"] = atomicity_evaluation.to_payload()
591
+ status = atomicity_evaluation.db_status or "blocked"
592
+ conn.execute(
593
+ """
594
+ INSERT INTO deferred_work_items(
595
+ work_id, source_agent, assigned_agent, reason, note_path, content_hash, payload_json, status
596
+ )
597
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
598
+ ON CONFLICT(work_id) DO UPDATE SET
599
+ payload_json=excluded.payload_json,
600
+ status=excluded.status,
601
+ updated_at=CURRENT_TIMESTAMP
602
+ """,
603
+ (
604
+ work_id,
605
+ raw_work.source_agent or "med-link-graph-curator",
606
+ raw_work.assigned_agent or "med-knowledge-architect",
607
+ raw_work.reason or "deferred_link_graph_work",
608
+ str(raw_work.effective_note_path(fallback=note_path)),
609
+ raw_work.effective_content_hash(fallback=actual_hash),
610
+ json.dumps(payload, ensure_ascii=False, sort_keys=True),
611
+ status,
612
+ ),
613
+ )
614
+ conn.execute(
615
+ """
616
+ UPDATE note_semantic_ingestion_queue
617
+ SET status='applied', updated_at=CURRENT_TIMESTAMP
618
+ WHERE note_path = ? AND content_hash = ? AND status IN ('pending', 'claimed')
619
+ """,
620
+ (str(note_path), actual_hash),
621
+ )
622
+
623
+ return {
624
+ "schema": INGESTION_RECEIPT_SCHEMA,
625
+ "status": "applied",
626
+ "note_path": str(note_path),
627
+ "content_hash": actual_hash,
628
+ "meaning_id": meaning_id,
629
+ "applied_alias_count": applied_aliases,
630
+ "idempotent": idempotent,
631
+ "deferred_atomicity_decisions": [decision.to_payload() for decision in deferred_atomicity_decisions],
632
+ }