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,930 @@
1
+ """Vocabulary DB and YAML-claim diagnosis for Wiki link semantics."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import sqlite3
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import TypeAlias, TypedDict
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.capabilities.notes.note_style.frontmatter import infer_title
14
+ from mednotes.domains.wiki.capabilities.vocabulary.link_terms import extract_aliases, normalize_key
15
+ from mednotes.domains.wiki.capabilities.vocabulary.link_terms import is_index_note as _is_index_note
16
+ from mednotes.domains.wiki.common import wiki_cli_command
17
+ from mednotes.domains.wiki.contracts.workflow_blockers import decision_for_code
18
+ from mednotes.kernel.base import JsonObject
19
+
20
+ VOCABULARY_MAP_SCHEMA = "medical-notes-workbench.vocabulary-map.v1"
21
+ VocabularyHashRowValue: TypeAlias = str | int | float | None
22
+ VocabularyHashPayload: TypeAlias = dict[str, list[dict[str, VocabularyHashRowValue]]]
23
+
24
+
25
+ class VocabularyIssuePayload(TypedDict, total=False):
26
+ severity: str
27
+ code: str
28
+ message: str
29
+ phase: str
30
+ note_path: str
31
+ surface: str
32
+ next_action: str
33
+ required_inputs: list[str]
34
+ stale_count: int
35
+ surface_count: int
36
+ meaning_id: str
37
+ label: str
38
+ decision_summary: JsonObject
39
+ display_text: str
40
+
41
+
42
+ class VocabularyDiagnosisPayload(TypedDict):
43
+ schema: str
44
+ status: str
45
+ db_path: str
46
+ map_hash: str
47
+ note_count: int
48
+ meaning_count: int
49
+ surface_count: int
50
+ ambiguous_surface_count: int
51
+ pending_semantic_ingestion_count: int
52
+ issues: list[VocabularyIssuePayload]
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class KnownMeaningSeed:
57
+ surface: str
58
+ meaning: str
59
+ note_title: str = ""
60
+ semantic_type: str = "medical_concept"
61
+ intrinsically_ambiguous: bool = False
62
+ ambiguity_reason: str = ""
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class AliasClaim:
67
+ note_path: str
68
+ alias_text: str
69
+ normalized_surface: str
70
+ claim_status: str
71
+ link_policy: str
72
+ visible_in_yaml: bool = True
73
+ meaning_ids: tuple[str, ...] = ()
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class SurfaceInfo:
78
+ normalized_surface: str
79
+ best_display_text: str
80
+ intrinsically_ambiguous: bool
81
+ direct_link_allowed: bool
82
+ link_policy: str
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class VocabularyBlocker:
87
+ code: str
88
+ message: str
89
+ note_path: str = ""
90
+ surface: str = ""
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class ProjectionAlias:
95
+ text: str
96
+ normalized_surface: str
97
+ link_policy: str
98
+ visible_in_yaml: bool
99
+ source: str
100
+ order: int
101
+
102
+
103
+ @dataclass
104
+ class VocabularyMap:
105
+ schema: str = VOCABULARY_MAP_SCHEMA
106
+ db_path: Path | None = None
107
+ alias_claims: list[AliasClaim] = field(default_factory=list)
108
+ surfaces: dict[str, SurfaceInfo] = field(default_factory=dict)
109
+ blockers: list[VocabularyBlocker] = field(default_factory=list)
110
+ note_aliases: dict[str, list[ProjectionAlias]] = field(default_factory=dict)
111
+ map_hash: str = ""
112
+ note_count: int = 0
113
+ meaning_count: int = 0
114
+ surface_count: int = 0
115
+ ambiguous_surface_count: int = 0
116
+ pending_semantic_ingestion_count: int = 0
117
+ issues: list[VocabularyIssuePayload] = field(default_factory=list)
118
+
119
+ def as_diagnosis_dict(self) -> VocabularyDiagnosisPayload:
120
+ human_codes = {
121
+ "vocabulary_map.duplicate_meaning",
122
+ "vocabulary_map.non_atomic_note",
123
+ "vocabulary_map.conflicting_alias",
124
+ }
125
+ has_human_issue = any(
126
+ issue.get("severity") == "human_decision" or issue.get("code") in human_codes
127
+ for issue in self.issues
128
+ )
129
+ if has_human_issue:
130
+ status = "blocked_human"
131
+ elif self.pending_semantic_ingestion_count > 0 or any(
132
+ issue.get("severity") == "blocker" for issue in self.issues
133
+ ):
134
+ status = "blocked_pending"
135
+ else:
136
+ status = "ready"
137
+ return {
138
+ "schema": self.schema,
139
+ "status": status,
140
+ "db_path": str(self.db_path) if self.db_path else "",
141
+ "map_hash": self.map_hash,
142
+ "note_count": self.note_count,
143
+ "meaning_count": self.meaning_count,
144
+ "surface_count": self.surface_count,
145
+ "ambiguous_surface_count": self.ambiguous_surface_count,
146
+ "pending_semantic_ingestion_count": self.pending_semantic_ingestion_count,
147
+ "issues": self.issues,
148
+ }
149
+
150
+
151
+ def meaning_id_for(label: str) -> str:
152
+ normalized = normalize_key(label).replace(" ", "_")
153
+ return "meaning:" + (normalized or "unknown")
154
+
155
+
156
+ def initialize_vocabulary_db(db_path: Path) -> None:
157
+ db_path.parent.mkdir(parents=True, exist_ok=True)
158
+ with sqlite3.connect(db_path) as conn:
159
+ conn.executescript(
160
+ """
161
+ PRAGMA foreign_keys = ON;
162
+
163
+ CREATE TABLE IF NOT EXISTS notes (
164
+ id INTEGER PRIMARY KEY,
165
+ path TEXT NOT NULL UNIQUE,
166
+ title TEXT NOT NULL,
167
+ stem TEXT NOT NULL,
168
+ content_hash TEXT NOT NULL,
169
+ status TEXT NOT NULL CHECK (status IN ('active', 'deleted', 'renamed', 'merged')),
170
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
171
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
172
+ );
173
+
174
+ CREATE TABLE IF NOT EXISTS meanings (
175
+ id TEXT PRIMARY KEY,
176
+ label TEXT NOT NULL,
177
+ normalized_label TEXT NOT NULL,
178
+ semantic_type TEXT NOT NULL DEFAULT '',
179
+ atomic_status TEXT NOT NULL CHECK (atomic_status IN ('atomic', 'suspected_non_atomic', 'duplicate_candidate', 'unknown')),
180
+ status TEXT NOT NULL CHECK (status IN ('active', 'retired', 'needs_review')),
181
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
182
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
183
+ );
184
+
185
+ CREATE TABLE IF NOT EXISTS surfaces (
186
+ id INTEGER PRIMARY KEY,
187
+ normalized_surface TEXT NOT NULL UNIQUE,
188
+ best_display_text TEXT NOT NULL,
189
+ intrinsically_ambiguous INTEGER NOT NULL DEFAULT 0,
190
+ ambiguity_reason TEXT NOT NULL DEFAULT '',
191
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
192
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
193
+ );
194
+
195
+ CREATE TABLE IF NOT EXISTS meaning_note_links (
196
+ id INTEGER PRIMARY KEY,
197
+ meaning_id TEXT NOT NULL REFERENCES meanings(id),
198
+ note_id INTEGER NOT NULL REFERENCES notes(id),
199
+ role TEXT NOT NULL CHECK (role IN ('canonical', 'alias_target', 'historical')),
200
+ status TEXT NOT NULL CHECK (status IN ('active', 'retired', 'needs_review')),
201
+ confidence REAL NOT NULL DEFAULT 0,
202
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
203
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
204
+ UNIQUE(meaning_id, note_id, role)
205
+ );
206
+
207
+ CREATE UNIQUE INDEX IF NOT EXISTS one_active_canonical_note_per_meaning
208
+ ON meaning_note_links(meaning_id)
209
+ WHERE role = 'canonical' AND status = 'active';
210
+
211
+ CREATE UNIQUE INDEX IF NOT EXISTS one_active_primary_meaning_per_note
212
+ ON meaning_note_links(note_id)
213
+ WHERE role = 'canonical' AND status = 'active';
214
+
215
+ CREATE TABLE IF NOT EXISTS surface_meaning_policy (
216
+ id INTEGER PRIMARY KEY,
217
+ surface_id INTEGER NOT NULL REFERENCES surfaces(id),
218
+ meaning_id TEXT NOT NULL REFERENCES meanings(id),
219
+ link_policy TEXT NOT NULL CHECK (link_policy IN ('direct', 'requires_context', 'blocked', 'no_link')),
220
+ visible_in_yaml INTEGER NOT NULL DEFAULT 1,
221
+ display_text TEXT NOT NULL DEFAULT '',
222
+ source TEXT NOT NULL CHECK (source IN ('curator', 'yaml', 'projection', 'human', 'llm', 'system')),
223
+ confidence REAL NOT NULL DEFAULT 0,
224
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
225
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
226
+ UNIQUE(surface_id, meaning_id)
227
+ );
228
+
229
+ CREATE TABLE IF NOT EXISTS note_semantic_ingestion_queue (
230
+ id INTEGER PRIMARY KEY,
231
+ note_id INTEGER NOT NULL REFERENCES notes(id),
232
+ note_path TEXT NOT NULL,
233
+ content_hash TEXT NOT NULL,
234
+ queue_flags_json TEXT NOT NULL,
235
+ assigned_agent TEXT NOT NULL,
236
+ status TEXT NOT NULL CHECK (status IN ('pending', 'claimed', 'applied', 'blocked', 'stale')),
237
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
238
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
239
+ UNIQUE(note_path, content_hash)
240
+ );
241
+
242
+ CREATE TABLE IF NOT EXISTS yaml_alias_claims (
243
+ id INTEGER PRIMARY KEY,
244
+ note_id INTEGER NOT NULL REFERENCES notes(id),
245
+ alias_text TEXT NOT NULL,
246
+ normalized_surface TEXT NOT NULL,
247
+ note_hash TEXT NOT NULL,
248
+ source TEXT NOT NULL CHECK (source IN ('yaml', 'projection', 'human', 'llm')),
249
+ claim_status TEXT NOT NULL CHECK (
250
+ claim_status IN ('accepted_alias', 'contextual_alias', 'duplicate_alias', 'conflicting_alias', 'stale_alias')
251
+ ),
252
+ link_policy TEXT NOT NULL CHECK (link_policy IN ('direct', 'requires_context', 'blocked', 'no_link')),
253
+ visible_in_yaml INTEGER NOT NULL DEFAULT 1,
254
+ first_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
255
+ last_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
256
+ UNIQUE(note_id, normalized_surface)
257
+ );
258
+
259
+ CREATE TABLE IF NOT EXISTS deferred_work_items (
260
+ work_id TEXT PRIMARY KEY,
261
+ source_agent TEXT NOT NULL,
262
+ assigned_agent TEXT NOT NULL,
263
+ reason TEXT NOT NULL,
264
+ note_path TEXT,
265
+ content_hash TEXT,
266
+ payload_json TEXT NOT NULL,
267
+ status TEXT NOT NULL CHECK (status IN ('pending', 'claimed', 'completed', 'blocked', 'cancelled')),
268
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
269
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
270
+ );
271
+
272
+ CREATE TABLE IF NOT EXISTS contextual_alias_decisions (
273
+ occurrence_id TEXT PRIMARY KEY,
274
+ note_path TEXT NOT NULL,
275
+ normalized_surface TEXT NOT NULL,
276
+ matched_text TEXT NOT NULL,
277
+ context_hash TEXT NOT NULL,
278
+ candidate_targets_json TEXT NOT NULL,
279
+ action TEXT NOT NULL CHECK (action IN ('link', 'no_link', 'defer')),
280
+ chosen_meaning_id TEXT NOT NULL DEFAULT '',
281
+ chosen_target_path TEXT NOT NULL DEFAULT '',
282
+ chosen_target TEXT NOT NULL DEFAULT '',
283
+ confidence REAL NOT NULL DEFAULT 0,
284
+ model TEXT NOT NULL DEFAULT '',
285
+ response_hash TEXT NOT NULL DEFAULT '',
286
+ reason_code TEXT NOT NULL DEFAULT '',
287
+ rationale_summary TEXT NOT NULL DEFAULT '',
288
+ status TEXT NOT NULL CHECK (status IN ('active', 'rejected', 'stale')),
289
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
290
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
291
+ );
292
+ """
293
+ )
294
+
295
+
296
+ def note_content_hash(path: Path) -> str:
297
+ return "sha256:" + file_sha256(path)
298
+
299
+
300
+ def upsert_note(conn: sqlite3.Connection, *, path: Path, title: str, content_hash: str) -> int:
301
+ conn.execute(
302
+ """
303
+ INSERT INTO notes(path, title, stem, content_hash, status)
304
+ VALUES (?, ?, ?, ?, 'active')
305
+ ON CONFLICT(path) DO UPDATE SET
306
+ title=excluded.title,
307
+ stem=excluded.stem,
308
+ content_hash=excluded.content_hash,
309
+ status='active',
310
+ updated_at=CURRENT_TIMESTAMP
311
+ """,
312
+ (str(path), title, path.stem, content_hash),
313
+ )
314
+ row = conn.execute("SELECT id FROM notes WHERE path = ?", (str(path),)).fetchone()
315
+ if row is None: # pragma: no cover - sqlite invariant
316
+ raise RuntimeError(f"failed to upsert note: {path}")
317
+ return int(row[0])
318
+
319
+
320
+ def upsert_meaning(
321
+ conn: sqlite3.Connection,
322
+ *,
323
+ meaning_id: str,
324
+ label: str,
325
+ semantic_type: str = "medical_concept",
326
+ atomic_status: str = "atomic",
327
+ ) -> None:
328
+ conn.execute(
329
+ """
330
+ INSERT INTO meanings(id, label, normalized_label, semantic_type, atomic_status, status)
331
+ VALUES (?, ?, ?, ?, ?, 'active')
332
+ ON CONFLICT(id) DO UPDATE SET
333
+ label=excluded.label,
334
+ normalized_label=excluded.normalized_label,
335
+ semantic_type=excluded.semantic_type,
336
+ atomic_status=excluded.atomic_status,
337
+ status='active',
338
+ updated_at=CURRENT_TIMESTAMP
339
+ """,
340
+ (meaning_id, label, normalize_key(label), semantic_type, atomic_status),
341
+ )
342
+
343
+
344
+ def upsert_surface(
345
+ conn: sqlite3.Connection,
346
+ *,
347
+ display_text: str,
348
+ intrinsically_ambiguous: bool = False,
349
+ ambiguity_reason: str = "",
350
+ ) -> int:
351
+ normalized = normalize_key(display_text)
352
+ row = conn.execute("SELECT id, intrinsically_ambiguous, best_display_text FROM surfaces WHERE normalized_surface = ?", (normalized,)).fetchone()
353
+ if row is None:
354
+ conn.execute(
355
+ """
356
+ INSERT INTO surfaces(normalized_surface, best_display_text, intrinsically_ambiguous, ambiguity_reason)
357
+ VALUES (?, ?, ?, ?)
358
+ """,
359
+ (normalized, display_text, int(intrinsically_ambiguous), ambiguity_reason),
360
+ )
361
+ else:
362
+ existing_ambiguous = bool(row[1])
363
+ best = _best_display_text(str(row[2]), display_text)
364
+ conn.execute(
365
+ """
366
+ UPDATE surfaces
367
+ SET best_display_text = ?, intrinsically_ambiguous = ?, ambiguity_reason = CASE WHEN ? != '' THEN ? ELSE ambiguity_reason END,
368
+ updated_at = CURRENT_TIMESTAMP
369
+ WHERE normalized_surface = ?
370
+ """,
371
+ (best, int(existing_ambiguous or intrinsically_ambiguous), ambiguity_reason, ambiguity_reason, normalized),
372
+ )
373
+ row = conn.execute("SELECT id FROM surfaces WHERE normalized_surface = ?", (normalized,)).fetchone()
374
+ if row is None: # pragma: no cover
375
+ raise RuntimeError(f"failed to upsert surface: {display_text}")
376
+ return int(row[0])
377
+
378
+
379
+ def upsert_policy(
380
+ conn: sqlite3.Connection,
381
+ *,
382
+ surface_id: int,
383
+ meaning_id: str,
384
+ link_policy: str,
385
+ display_text: str,
386
+ visible_in_yaml: bool = True,
387
+ source: str = "system",
388
+ confidence: float = 0.0,
389
+ ) -> None:
390
+ conn.execute(
391
+ """
392
+ INSERT INTO surface_meaning_policy(
393
+ surface_id, meaning_id, link_policy, visible_in_yaml, display_text, source, confidence
394
+ )
395
+ VALUES (?, ?, ?, ?, ?, ?, ?)
396
+ ON CONFLICT(surface_id, meaning_id) DO UPDATE SET
397
+ link_policy=excluded.link_policy,
398
+ visible_in_yaml=excluded.visible_in_yaml,
399
+ display_text=excluded.display_text,
400
+ source=excluded.source,
401
+ confidence=excluded.confidence,
402
+ updated_at=CURRENT_TIMESTAMP
403
+ """,
404
+ (surface_id, meaning_id, link_policy, int(visible_in_yaml), display_text, source, confidence),
405
+ )
406
+
407
+
408
+ def _best_display_text(left: str, right: str) -> str:
409
+ def score(value: str) -> tuple[int, int, int]:
410
+ return (
411
+ int(any(ord(char) > 127 for char in value)),
412
+ int(value.isupper() and len(value) <= 8),
413
+ sum(1 for char in value if char.isupper()),
414
+ )
415
+
416
+ return max([left, right], key=score)
417
+
418
+
419
+ def _scan_notes(wiki_dir: Path) -> list[tuple[Path, str, str]]:
420
+ notes: list[tuple[Path, str, str]] = []
421
+ if not wiki_dir.exists():
422
+ return notes
423
+ for path in iter_notes(wiki_dir):
424
+ text = path.read_text(encoding="utf-8")
425
+ if _is_index_note(path, text):
426
+ continue
427
+ notes.append((path, infer_title(text, path), text))
428
+ return notes
429
+
430
+
431
+ def _seed_meaning_id(seed: KnownMeaningSeed) -> str:
432
+ return meaning_id_for(seed.meaning)
433
+
434
+
435
+ def rebuild_vocabulary_map(
436
+ *,
437
+ wiki_dir: Path,
438
+ db_path: Path,
439
+ import_yaml_aliases: bool,
440
+ known_meanings: list[KnownMeaningSeed] | None = None,
441
+ ) -> VocabularyMap:
442
+ initialize_vocabulary_db(db_path)
443
+ seeds = known_meanings or []
444
+ result = VocabularyMap(db_path=db_path)
445
+ notes = _scan_notes(wiki_dir)
446
+ note_ids: dict[Path, int] = {}
447
+ with sqlite3.connect(db_path) as conn:
448
+ for path, title, _text in notes:
449
+ note_ids[path] = upsert_note(conn, path=path, title=title, content_hash=note_content_hash(path))
450
+
451
+ for seed in seeds:
452
+ meaning_id = _seed_meaning_id(seed)
453
+ upsert_meaning(conn, meaning_id=meaning_id, label=seed.meaning, semantic_type=seed.semantic_type)
454
+ surface_id = upsert_surface(
455
+ conn,
456
+ display_text=seed.surface,
457
+ intrinsically_ambiguous=seed.intrinsically_ambiguous,
458
+ ambiguity_reason=seed.ambiguity_reason,
459
+ )
460
+ upsert_policy(
461
+ conn,
462
+ surface_id=surface_id,
463
+ meaning_id=meaning_id,
464
+ link_policy="requires_context" if seed.intrinsically_ambiguous else "direct",
465
+ display_text=seed.surface,
466
+ source="system",
467
+ )
468
+ if seed.note_title:
469
+ matching = [path for path, title, _text in notes if normalize_key(title) == normalize_key(seed.note_title) or normalize_key(path.stem) == normalize_key(seed.note_title)]
470
+ for path in matching:
471
+ try:
472
+ conn.execute(
473
+ """
474
+ INSERT INTO meaning_note_links(meaning_id, note_id, role, status, confidence)
475
+ VALUES (?, ?, 'canonical', 'active', 1.0)
476
+ ON CONFLICT(meaning_id, note_id, role) DO UPDATE SET status='active', updated_at=CURRENT_TIMESTAMP
477
+ """,
478
+ (meaning_id, note_ids[path]),
479
+ )
480
+ except sqlite3.IntegrityError:
481
+ result.blockers.append(
482
+ VocabularyBlocker(
483
+ code="vocabulary_map.duplicate_meaning",
484
+ message=f"Meaning {seed.meaning} maps to more than one active canonical note.",
485
+ note_path=str(path),
486
+ surface=seed.surface,
487
+ )
488
+ )
489
+
490
+ if import_yaml_aliases:
491
+ _import_yaml_alias_claims(conn, result, notes, note_ids, seeds)
492
+
493
+ _load_surface_info(conn, result)
494
+ return result
495
+
496
+
497
+ def _import_yaml_alias_claims(
498
+ conn: sqlite3.Connection,
499
+ result: VocabularyMap,
500
+ notes: list[tuple[Path, str, str]],
501
+ note_ids: dict[Path, int],
502
+ seeds: list[KnownMeaningSeed],
503
+ ) -> None:
504
+ seeds_by_surface: dict[str, list[KnownMeaningSeed]] = {}
505
+ for seed in seeds:
506
+ seeds_by_surface.setdefault(normalize_key(seed.surface), []).append(seed)
507
+
508
+ for path, title, text in notes:
509
+ seen: set[str] = set()
510
+ projection_items: list[ProjectionAlias] = []
511
+ seed_order = 0
512
+ for seed in seeds:
513
+ if seed.note_title and normalize_key(seed.note_title) not in {normalize_key(title), normalize_key(path.stem)}:
514
+ continue
515
+ normalized = normalize_key(seed.surface)
516
+ if normalized in seen:
517
+ continue
518
+ seen.add(normalized)
519
+ policy = "requires_context" if _surface_requires_context(seeds_by_surface.get(normalized, [])) else "direct"
520
+ projection_items.append(
521
+ ProjectionAlias(
522
+ text=seed.surface,
523
+ normalized_surface=normalized,
524
+ link_policy=policy,
525
+ visible_in_yaml=True,
526
+ source="seed",
527
+ order=seed_order,
528
+ )
529
+ )
530
+ seed_order += 1
531
+
532
+ for alias in extract_aliases(text):
533
+ normalized = normalize_key(alias)
534
+ surface_seeds = seeds_by_surface.get(normalized, [])
535
+ claim_status, link_policy, blocker = _classify_alias_claim(alias, title, surface_seeds)
536
+ meaning_ids = tuple(_seed_meaning_id(seed) for seed in surface_seeds)
537
+ claim = AliasClaim(
538
+ note_path=str(path),
539
+ alias_text=alias,
540
+ normalized_surface=normalized,
541
+ claim_status=claim_status,
542
+ link_policy=link_policy,
543
+ meaning_ids=meaning_ids,
544
+ )
545
+ result.alias_claims.append(claim)
546
+ if blocker is not None:
547
+ result.blockers.append(VocabularyBlocker(**{**blocker, "note_path": str(path), "surface": alias}))
548
+ conn.execute(
549
+ """
550
+ INSERT INTO yaml_alias_claims(note_id, alias_text, normalized_surface, note_hash, source, claim_status, link_policy, visible_in_yaml)
551
+ VALUES (?, ?, ?, ?, 'yaml', ?, ?, 1)
552
+ ON CONFLICT(note_id, normalized_surface) DO UPDATE SET
553
+ alias_text=excluded.alias_text,
554
+ note_hash=excluded.note_hash,
555
+ claim_status=excluded.claim_status,
556
+ link_policy=excluded.link_policy,
557
+ visible_in_yaml=1,
558
+ last_seen_at=CURRENT_TIMESTAMP
559
+ """,
560
+ (note_ids[path], alias, normalized, note_content_hash(path), claim_status, link_policy),
561
+ )
562
+ if normalized not in seen and claim_status != "conflicting_alias":
563
+ seen.add(normalized)
564
+ projection_items.append(
565
+ ProjectionAlias(
566
+ text=alias,
567
+ normalized_surface=normalized,
568
+ link_policy=link_policy,
569
+ visible_in_yaml=True,
570
+ source="yaml",
571
+ order=seed_order,
572
+ )
573
+ )
574
+ seed_order += 1
575
+ result.note_aliases[str(path)] = projection_items
576
+
577
+
578
+ def _classify_alias_claim(
579
+ alias: str,
580
+ note_title: str,
581
+ surface_seeds: list[KnownMeaningSeed],
582
+ ) -> tuple[str, str, dict[str, str] | None]:
583
+ if not surface_seeds:
584
+ return "contextual_alias", "requires_context", None
585
+ meaning_to_titles: dict[str, set[str]] = {}
586
+ for seed in surface_seeds:
587
+ meaning_to_titles.setdefault(seed.meaning, set())
588
+ if seed.note_title:
589
+ meaning_to_titles[seed.meaning].add(seed.note_title)
590
+ if any(len(titles) > 1 for titles in meaning_to_titles.values()):
591
+ return (
592
+ "conflicting_alias",
593
+ "blocked",
594
+ {
595
+ "code": "vocabulary_map.duplicate_meaning",
596
+ "message": f"Alias {alias} maps one meaning to multiple canonical notes.",
597
+ },
598
+ )
599
+ if _surface_requires_context(surface_seeds):
600
+ return "contextual_alias", "requires_context", None
601
+ if len({seed.meaning for seed in surface_seeds}) == 1:
602
+ return "accepted_alias", "direct", None
603
+ return "contextual_alias", "requires_context", None
604
+
605
+
606
+ def _surface_requires_context(surface_seeds: list[KnownMeaningSeed]) -> bool:
607
+ if len({seed.meaning for seed in surface_seeds}) > 1:
608
+ return True
609
+ return any(seed.intrinsically_ambiguous for seed in surface_seeds)
610
+
611
+
612
+ def _load_surface_info(conn: sqlite3.Connection, result: VocabularyMap) -> None:
613
+ rows = conn.execute(
614
+ """
615
+ SELECT s.normalized_surface, s.best_display_text, s.intrinsically_ambiguous,
616
+ COUNT(DISTINCT p.meaning_id) AS meaning_count,
617
+ SUM(CASE WHEN p.link_policy = 'direct' THEN 1 ELSE 0 END) AS direct_count
618
+ FROM surfaces s
619
+ LEFT JOIN surface_meaning_policy p ON p.surface_id = s.id
620
+ GROUP BY s.id
621
+ """
622
+ ).fetchall()
623
+ for normalized, display, ambiguous, meaning_count, direct_count in rows:
624
+ link_policy = "direct" if direct_count and meaning_count == 1 and not ambiguous else "requires_context"
625
+ result.surfaces[str(normalized)] = SurfaceInfo(
626
+ normalized_surface=str(normalized),
627
+ best_display_text=str(display),
628
+ intrinsically_ambiguous=bool(ambiguous),
629
+ direct_link_allowed=link_policy == "direct",
630
+ link_policy=link_policy,
631
+ )
632
+
633
+
634
+ def pending_semantic_ingestion_count(db_path: Path) -> int:
635
+ if not db_path.exists():
636
+ return 0
637
+ with sqlite3.connect(db_path) as conn:
638
+ row = conn.execute("SELECT COUNT(*) FROM note_semantic_ingestion_queue WHERE status IN ('pending', 'claimed')").fetchone()
639
+ return int(row[0]) if row else 0
640
+
641
+
642
+ def _query_scalar_count(conn: sqlite3.Connection, sql: str, params: tuple[object, ...] = ()) -> int:
643
+ row = conn.execute(sql, params).fetchone()
644
+ return int(row[0]) if row else 0
645
+
646
+
647
+ def _hash_row_value(value: object) -> VocabularyHashRowValue:
648
+ if value is None or isinstance(value, str | int | float):
649
+ return value
650
+ return str(value)
651
+
652
+
653
+ def vocabulary_map_hash(db_path: Path) -> str:
654
+ initialize_vocabulary_db(db_path)
655
+ tables = (
656
+ "notes",
657
+ "meanings",
658
+ "surfaces",
659
+ "meaning_note_links",
660
+ "surface_meaning_policy",
661
+ "yaml_alias_claims",
662
+ "note_semantic_ingestion_queue",
663
+ "deferred_work_items",
664
+ )
665
+ payload: VocabularyHashPayload = {}
666
+ with sqlite3.connect(db_path) as conn:
667
+ conn.row_factory = sqlite3.Row
668
+ for table in tables:
669
+ columns = [str(row[1]) for row in conn.execute(f"PRAGMA table_info({table})").fetchall()]
670
+ stable_columns = [column for column in columns if not column.endswith("_at")]
671
+ order_by = ", ".join(stable_columns) if stable_columns else "rowid"
672
+ rows = conn.execute(f"SELECT * FROM {table} ORDER BY {order_by}").fetchall()
673
+ payload[table] = [
674
+ {key: _hash_row_value(row[key]) for key in row.keys() if not key.endswith("_at")}
675
+ for row in rows
676
+ ]
677
+ encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
678
+ return "sha256:" + hashlib.sha256(encoded).hexdigest()
679
+
680
+
681
+ def _issue(
682
+ *,
683
+ severity: str,
684
+ code: str,
685
+ message: str,
686
+ phase: str = "vocabulary_map_diagnosis",
687
+ note_path: str = "",
688
+ surface: str = "",
689
+ next_action: str = "",
690
+ required_inputs: list[str] | None = None,
691
+ ) -> VocabularyIssuePayload:
692
+ return {
693
+ "severity": severity,
694
+ "code": code,
695
+ "message": message,
696
+ "phase": phase,
697
+ "note_path": note_path,
698
+ "surface": surface,
699
+ "next_action": next_action,
700
+ "required_inputs": required_inputs or [],
701
+ }
702
+
703
+
704
+ def _load_alias_claims(conn: sqlite3.Connection, result: VocabularyMap) -> None:
705
+ rows = conn.execute(
706
+ """
707
+ SELECT n.path, c.alias_text, c.normalized_surface, c.claim_status, c.link_policy, c.visible_in_yaml
708
+ FROM yaml_alias_claims c
709
+ JOIN notes n ON n.id = c.note_id
710
+ ORDER BY n.path, c.normalized_surface
711
+ """
712
+ ).fetchall()
713
+ for note_path, alias_text, normalized_surface, claim_status, link_policy, visible in rows:
714
+ claim = AliasClaim(
715
+ note_path=str(note_path),
716
+ alias_text=str(alias_text),
717
+ normalized_surface=str(normalized_surface),
718
+ claim_status=str(claim_status),
719
+ link_policy=str(link_policy),
720
+ visible_in_yaml=bool(visible),
721
+ )
722
+ result.alias_claims.append(claim)
723
+ if claim.visible_in_yaml:
724
+ result.note_aliases.setdefault(claim.note_path, []).append(
725
+ ProjectionAlias(
726
+ text=claim.alias_text,
727
+ normalized_surface=claim.normalized_surface,
728
+ link_policy=claim.link_policy,
729
+ visible_in_yaml=True,
730
+ source="yaml",
731
+ order=len(result.note_aliases.get(claim.note_path, [])),
732
+ )
733
+ )
734
+
735
+
736
+ def _load_db_projection_aliases(conn: sqlite3.Connection, result: VocabularyMap) -> None:
737
+ rows = conn.execute(
738
+ """
739
+ SELECT n.path,
740
+ COALESCE(NULLIF(p.display_text, ''), s.best_display_text) AS display_text,
741
+ s.normalized_surface,
742
+ p.link_policy,
743
+ p.visible_in_yaml,
744
+ p.source
745
+ FROM notes n
746
+ JOIN meaning_note_links l ON l.note_id = n.id
747
+ JOIN surface_meaning_policy p ON p.meaning_id = l.meaning_id
748
+ JOIN surfaces s ON s.id = p.surface_id
749
+ WHERE n.status = 'active'
750
+ AND l.role = 'canonical'
751
+ AND l.status = 'active'
752
+ AND p.visible_in_yaml = 1
753
+ AND p.link_policy IN ('direct', 'requires_context')
754
+ ORDER BY n.path, s.normalized_surface
755
+ """
756
+ ).fetchall()
757
+ for note_path, display_text, normalized_surface, link_policy, visible, source in rows:
758
+ items = result.note_aliases.setdefault(str(note_path), [])
759
+ items.append(
760
+ ProjectionAlias(
761
+ text=str(display_text),
762
+ normalized_surface=str(normalized_surface),
763
+ link_policy=str(link_policy),
764
+ visible_in_yaml=bool(visible),
765
+ source=str(source or "curator"),
766
+ order=len(items),
767
+ )
768
+ )
769
+
770
+
771
+ def load_vocabulary_map_diagnosis(db_path: Path) -> VocabularyMap:
772
+ initialize_vocabulary_db(db_path)
773
+ result = VocabularyMap(db_path=db_path)
774
+ with sqlite3.connect(db_path) as conn:
775
+ result.note_count = _query_scalar_count(conn, "SELECT COUNT(*) FROM notes WHERE status = 'active'")
776
+ result.meaning_count = _query_scalar_count(conn, "SELECT COUNT(*) FROM meanings WHERE status = 'active'")
777
+ result.surface_count = _query_scalar_count(conn, "SELECT COUNT(*) FROM surfaces")
778
+ result.ambiguous_surface_count = _query_scalar_count(conn, "SELECT COUNT(*) FROM surfaces WHERE intrinsically_ambiguous = 1")
779
+ result.pending_semantic_ingestion_count = _query_scalar_count(
780
+ conn,
781
+ "SELECT COUNT(*) FROM note_semantic_ingestion_queue WHERE status IN ('pending', 'claimed')",
782
+ )
783
+ stale_semantic_ingestion_count = _query_scalar_count(
784
+ conn,
785
+ "SELECT COUNT(*) FROM note_semantic_ingestion_queue WHERE status='stale'",
786
+ )
787
+ _load_surface_info(conn, result)
788
+ _load_db_projection_aliases(conn, result)
789
+ _load_alias_claims(conn, result)
790
+ if result.pending_semantic_ingestion_count:
791
+ result.issues.append(
792
+ _issue(
793
+ severity="blocker",
794
+ code="vocabulary_semantic_ingestion_pending",
795
+ message="Semantic ingestion queue has pending notes.",
796
+ next_action="Processar note_semantic_ingestion_queue com med-link-graph-curator.",
797
+ required_inputs=["vocabulary_semantic_ingestion"],
798
+ )
799
+ )
800
+ if stale_semantic_ingestion_count:
801
+ recovery = wiki_cli_command("vocabulary-recover", "--mode", "reconcile-queue", "--dry-run", "--json")
802
+ result.issues.append(
803
+ _issue(
804
+ severity="blocker",
805
+ code="vocabulary_semantic_ingestion_stale",
806
+ message="Semantic ingestion queue has stale notes that must be refreshed before curation can continue.",
807
+ next_action=recovery,
808
+ required_inputs=["vocabulary_recovery", "vocabulary_semantic_ingestion"],
809
+ )
810
+ | {"stale_count": stale_semantic_ingestion_count}
811
+ )
812
+ unresolved_surface_count = _query_scalar_count(
813
+ conn,
814
+ """
815
+ SELECT COUNT(*)
816
+ FROM surfaces s
817
+ LEFT JOIN surface_meaning_policy p ON p.surface_id = s.id
818
+ WHERE p.id IS NULL
819
+ """,
820
+ )
821
+ if unresolved_surface_count:
822
+ result.issues.append(
823
+ _issue(
824
+ severity="blocker",
825
+ code="vocabulary_map.unresolved_surfaces_without_meanings",
826
+ message="Vocabulary DB has surfaces without a meaning policy.",
827
+ next_action=(
828
+ "Reconciliar ou reconstruir o vocabulary DB pelo fluxo oficial de /mednotes:link; "
829
+ "não projetar aliases nem rodar body linker."
830
+ ),
831
+ required_inputs=["vocabulary_recovery", "vocabulary_semantic_ingestion"],
832
+ )
833
+ | {"surface_count": unresolved_surface_count}
834
+ )
835
+ for _meaning_id, label in conn.execute(
836
+ "SELECT id, label FROM meanings WHERE status = 'active' AND atomic_status = 'duplicate_candidate' ORDER BY id"
837
+ ).fetchall():
838
+ result.issues.append(
839
+ _issue(
840
+ severity="human_decision",
841
+ code="vocabulary_map.duplicate_meaning",
842
+ message=f"Meaning requires duplicate/merge review: {label}",
843
+ next_action="Criar plano de merge preservando provenance.",
844
+ required_inputs=["note_merge_decision"],
845
+ )
846
+ )
847
+ for meaning_id, label, note_path in conn.execute(
848
+ """
849
+ SELECT m.id, m.label, COALESCE(n.path, '')
850
+ FROM meanings m
851
+ LEFT JOIN meaning_note_links l
852
+ ON l.meaning_id = m.id
853
+ AND l.role = 'canonical'
854
+ AND l.status = 'active'
855
+ LEFT JOIN notes n ON n.id = l.note_id
856
+ WHERE m.status = 'active'
857
+ AND m.atomic_status = 'suspected_non_atomic'
858
+ ORDER BY m.id, n.path
859
+ """
860
+ ).fetchall():
861
+ issue = _issue(
862
+ severity="human_decision",
863
+ code="vocabulary_map.non_atomic_note",
864
+ message=f"Meaning may not be atomic: {label}",
865
+ note_path=str(note_path),
866
+ next_action="Separar ou reescrever a nota antes de linkar automaticamente.",
867
+ required_inputs=["atomicity_decision"],
868
+ )
869
+ issue["meaning_id"] = str(meaning_id)
870
+ issue["label"] = str(label)
871
+ result.issues.append(issue)
872
+ for path, alias_text, normalized_surface in conn.execute(
873
+ """
874
+ SELECT n.path, c.alias_text, c.normalized_surface
875
+ FROM yaml_alias_claims c
876
+ JOIN notes n ON n.id = c.note_id
877
+ WHERE c.claim_status = 'conflicting_alias'
878
+ ORDER BY n.path, c.normalized_surface
879
+ """
880
+ ).fetchall():
881
+ result.issues.append(
882
+ _issue(
883
+ severity="human_decision",
884
+ code="vocabulary_map.conflicting_alias",
885
+ message=f"YAML alias conflicts with vocabulary meaning: {alias_text}",
886
+ note_path=str(path),
887
+ surface=str(normalized_surface),
888
+ next_action="Resolver alias conflitante no DB antes de projetar YAML/linkar corpo.",
889
+ required_inputs=["alias_conflict_decision"],
890
+ )
891
+ )
892
+ for normalized_surface, display_text in conn.execute(
893
+ """
894
+ SELECT s.normalized_surface, s.best_display_text
895
+ FROM surfaces s
896
+ JOIN surface_meaning_policy p ON p.surface_id = s.id
897
+ GROUP BY s.id
898
+ HAVING
899
+ SUM(CASE WHEN p.link_policy = 'direct' THEN 1 ELSE 0 END) > 0
900
+ AND (
901
+ COUNT(DISTINCT p.meaning_id) > 1
902
+ OR MAX(s.intrinsically_ambiguous) = 1
903
+ )
904
+ ORDER BY s.normalized_surface
905
+ """
906
+ ).fetchall():
907
+ decision = decision_for_code(
908
+ "vocabulary_map.direct_policy_on_ambiguous_surface",
909
+ phase="vocabulary_map_diagnosis",
910
+ public_summary="Termo ambíguo tratado de forma contextual.",
911
+ developer_summary="Direct alias on ambiguous surface downgraded before linking.",
912
+ next_action="Continuar sem link direto automático para esta superfície.",
913
+ )
914
+ issue = _issue(
915
+ severity="warning",
916
+ code=decision.reason_code,
917
+ message=decision.public_summary,
918
+ surface=str(normalized_surface),
919
+ next_action=decision.next_action,
920
+ required_inputs=[],
921
+ )
922
+ issue["decision_summary"] = decision.decision_summary()
923
+ result.issues.append(
924
+ issue
925
+ | {
926
+ "display_text": str(display_text),
927
+ }
928
+ )
929
+ result.map_hash = vocabulary_map_hash(db_path)
930
+ return result