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,605 @@
1
+ """Gemini exported artifact discovery and Wiki-note validation."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import re
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+ from urllib.parse import urlparse
11
+
12
+ from mednotes.domains.wiki.capabilities.notes.raw_chats import read_note_meta
13
+ from mednotes.domains.wiki.common import MissingPathError, ValidationError
14
+ from mednotes.domains.wiki.config import _path
15
+ from mednotes.kernel.base import JsonObject
16
+
17
+ ARTIFACT_HTML_MANIFEST_SCHEMA = "gemini-md-export.artifact-html-manifest.v1"
18
+ ARTIFACT_IMAGE_MANIFEST_SCHEMA = "gemini-md-export.artifact-image-manifest.v1"
19
+ ARTIFACT_VALIDATION_SCHEMA = "medical-notes-workbench.gemini-artifact-validation.v1"
20
+ ARTIFACT_HTML_VALIDATION_SCHEMA = "medical-notes-workbench.artifact-html-validation.v1"
21
+ _SUPPORTED_MANIFEST_SCHEMAS = {ARTIFACT_HTML_MANIFEST_SCHEMA, ARTIFACT_IMAGE_MANIFEST_SCHEMA}
22
+ _IMAGE_SUFFIXES = {".avif", ".gif", ".jpg", ".jpeg", ".png", ".svg", ".webp"}
23
+
24
+ _EXPLICIT_MANIFEST_KEYS = (
25
+ "artifact_manifest",
26
+ "artifact_manifests",
27
+ "artifact_html_manifest",
28
+ "artifact_html_manifests",
29
+ "artifact_image_manifest",
30
+ "artifact_image_manifests",
31
+ "artifact_manifest_path",
32
+ "artifact_manifest_paths",
33
+ "artifact_image_manifest_path",
34
+ "artifact_image_manifest_paths",
35
+ "gemini_artifact_manifest",
36
+ "gemini_artifact_manifests",
37
+ "gemini_image_artifact_manifest",
38
+ "gemini_image_artifact_manifests",
39
+ )
40
+ _FULL_HTML_RE = re.compile(r"(?is)<\s*!doctype\b|<\s*html\b|<\s*/\s*html\s*>|<\s*head\b|<\s*body\b|<\s*script\b")
41
+ _IFRAME_SRC_RE = re.compile(r"(?is)<iframe\b[^>]*\bsrc\s*=\s*([\"'])(.*?)\1[^>]*>")
42
+ _MARKDOWN_LINK_RE = re.compile(r"(?s)\[[^\]]+\]\(([^)\s]+(?:\s[^)]*)?)\)")
43
+ _MARKDOWN_IMAGE_RE = re.compile(r"(?s)!\[[^\]]*\]\(([^)\s]+(?:\s[^)]*)?)\)")
44
+ _COMMENT_RE = re.compile(r"(?is)<!--\s*gemini-artifact\b(?P<body>.*?)-->")
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class ArtifactHtml:
49
+ chat_id: str
50
+ source_url: str
51
+ manifest_path: Path
52
+ file_path: Path
53
+ sha256: str
54
+ kind: str = "html"
55
+ turn_index: str = ""
56
+ mime_type: str = ""
57
+ caption: str = ""
58
+
59
+ def to_json(self) -> dict[str, str]:
60
+ payload = {
61
+ "kind": self.kind,
62
+ "chat_id": self.chat_id,
63
+ "source_url": self.source_url,
64
+ "manifest": str(self.manifest_path),
65
+ "file": str(self.file_path),
66
+ "sha256": self.sha256,
67
+ }
68
+ if self.turn_index:
69
+ payload["turn_index"] = self.turn_index
70
+ if self.mime_type:
71
+ payload["mime_type"] = self.mime_type
72
+ if self.caption:
73
+ payload["caption"] = self.caption
74
+ return payload
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class ArtifactManifest:
79
+ path: Path
80
+ chat_id: str
81
+ source_url: str
82
+ saved_count: int
83
+ artifacts: tuple[ArtifactHtml, ...]
84
+ schema: str = ARTIFACT_HTML_MANIFEST_SCHEMA
85
+
86
+ def to_json(self) -> JsonObject:
87
+ return {
88
+ "schema": self.schema,
89
+ "path": str(self.path),
90
+ "chat_id": self.chat_id,
91
+ "source_url": self.source_url,
92
+ "saved_count": self.saved_count,
93
+ "artifacts": [artifact.to_json() for artifact in self.artifacts],
94
+ }
95
+
96
+
97
+ def _paths_match(left: str | Path, right: Path) -> bool:
98
+ left_path = _path(str(left))
99
+ try:
100
+ return left_path.resolve() == right.resolve()
101
+ except OSError:
102
+ return str(left_path) == str(right)
103
+
104
+
105
+ def _json_value(value: str) -> Any:
106
+ try:
107
+ return json.loads(value)
108
+ except json.JSONDecodeError:
109
+ return value
110
+
111
+
112
+ def _coerce_manifest_paths(value: str) -> list[Path]:
113
+ parsed = _json_value(value.strip())
114
+ raw_values: list[Any]
115
+ if isinstance(parsed, list):
116
+ raw_values = parsed
117
+ elif isinstance(parsed, str):
118
+ raw_values = re.split(r"[,;\n]", parsed)
119
+ else:
120
+ raw_values = [parsed]
121
+ return [_path(str(item).strip()) for item in raw_values if str(item).strip()]
122
+
123
+
124
+ def _extract_chat_id(value: str) -> str:
125
+ value = value.strip().strip("\"'")
126
+ if not value:
127
+ return ""
128
+ if re.match(r"^https?://", value):
129
+ parsed = urlparse(value)
130
+ parts = [part for part in parsed.path.split("/") if part]
131
+ if "app" in parts:
132
+ index = parts.index("app")
133
+ if index + 1 < len(parts):
134
+ return parts[index + 1].strip("/")
135
+ return parts[-1].strip("/") if parts else ""
136
+ if "/app/" in value:
137
+ return value.rsplit("/app/", 1)[1].split("?", 1)[0].split("#", 1)[0].strip("/")
138
+ return value.rstrip("/").split("/")[-1].split("?", 1)[0].split("#", 1)[0]
139
+
140
+
141
+ def chat_id_from_raw(raw_file: Path) -> str:
142
+ return _extract_chat_id(read_note_meta(raw_file).get("fonte_id", ""))
143
+
144
+
145
+ def _source_url(raw_meta: dict[str, str], chat_id: str, manifest_data: dict[str, Any]) -> str:
146
+ for key in ("sourceUrl", "source_url", "geminiUrl", "gemini_url", "chatUrl", "chat_url", "url", "source"):
147
+ value = str(manifest_data.get(key) or "").strip()
148
+ if value:
149
+ return value
150
+ fonte_id = raw_meta.get("fonte_id", "").strip()
151
+ if re.match(r"^https?://", fonte_id):
152
+ return fonte_id
153
+ return f"https://gemini.google.com/app/{chat_id}" if chat_id else ""
154
+
155
+
156
+ def _manifest_chat_id(path: Path, data: dict[str, Any], fallback: str) -> str:
157
+ for key in ("chatId", "chat_id", "chatID", "sourceChatId", "source_chat_id"):
158
+ chat_id = _extract_chat_id(str(data.get(key) or ""))
159
+ if chat_id:
160
+ return chat_id
161
+ match = re.match(r"artifact-(?P<chat_id>.+)-manifest\.json$", path.name)
162
+ return _extract_chat_id(match.group("chat_id")) if match else fallback
163
+
164
+
165
+ def _saved_count(data: JsonObject, artifact_count: int) -> int:
166
+ value = data.get("savedCount", data.get("saved_count", artifact_count))
167
+ if value is None:
168
+ value = artifact_count
169
+ try:
170
+ return int(value)
171
+ except (TypeError, ValueError) as err:
172
+ raise ValidationError("Gemini artifact manifest savedCount must be an integer") from err
173
+
174
+
175
+ def _artifact_items(data: dict[str, Any]) -> list[Any]:
176
+ for key in ("artifacts", "files", "htmlFiles", "html_files", "savedArtifacts", "saved_artifacts", "saved", "outputs"):
177
+ value = data.get(key)
178
+ if isinstance(value, list):
179
+ return value
180
+ return []
181
+
182
+
183
+ def _item_path(value: Any, manifest_dir: Path) -> Path | None:
184
+ if isinstance(value, str):
185
+ raw_path = value
186
+ elif isinstance(value, dict):
187
+ raw_path = ""
188
+ for key in (
189
+ "file",
190
+ "path",
191
+ "filePath",
192
+ "file_path",
193
+ "htmlPath",
194
+ "html_path",
195
+ "absolutePath",
196
+ "absolute_path",
197
+ "outputPath",
198
+ "output_path",
199
+ "savedPath",
200
+ "saved_path",
201
+ "relativePath",
202
+ "relative_path",
203
+ "fileName",
204
+ "filename",
205
+ "name",
206
+ ):
207
+ raw_path = str(value.get(key) or "").strip()
208
+ if raw_path:
209
+ break
210
+ else:
211
+ return None
212
+ if not raw_path:
213
+ return None
214
+ path = _path(raw_path)
215
+ return path if path.is_absolute() else manifest_dir / path
216
+
217
+
218
+ def _item_sha256(item: Any) -> str:
219
+ if not isinstance(item, dict):
220
+ return ""
221
+ for key in ("sha256", "sha_256", "sha256Hex", "sha256_hex", "sha256Digest", "sha256_digest", "hash"):
222
+ value = str(item.get(key) or "").strip().lower()
223
+ if value:
224
+ return value.removeprefix("sha256:")
225
+ return ""
226
+
227
+
228
+ def _item_turn_index(item: Any) -> str:
229
+ if not isinstance(item, dict):
230
+ return ""
231
+ for key in ("turnIndex", "turn_index", "turn", "index"):
232
+ value = str(item.get(key) or "").strip()
233
+ if value:
234
+ return value
235
+ return ""
236
+
237
+
238
+ def _item_mime_type(item: Any) -> str:
239
+ if not isinstance(item, dict):
240
+ return ""
241
+ for key in ("mimeType", "mime_type", "mimetype", "contentType", "content_type"):
242
+ value = str(item.get(key) or "").strip()
243
+ if value:
244
+ return value
245
+ return ""
246
+
247
+
248
+ def _item_caption(item: Any) -> str:
249
+ if not isinstance(item, dict):
250
+ return ""
251
+ for key in ("caption", "alt", "altText", "alt_text", "description", "title", "prompt"):
252
+ value = str(item.get(key) or "").strip()
253
+ if value:
254
+ return value
255
+ return ""
256
+
257
+
258
+ def _sha256_file(path: Path) -> str:
259
+ digest = hashlib.sha256()
260
+ with path.open("rb") as fh:
261
+ for chunk in iter(lambda: fh.read(1024 * 1024), b""):
262
+ digest.update(chunk)
263
+ return digest.hexdigest()
264
+
265
+
266
+ def load_artifact_manifest(path: Path, raw_file: Path) -> ArtifactManifest | None:
267
+ if not path.exists():
268
+ raise MissingPathError(f"Gemini artifact manifest not found: {path}")
269
+ try:
270
+ data = json.loads(path.read_text(encoding="utf-8"))
271
+ except json.JSONDecodeError as exc:
272
+ raise ValidationError(f"Invalid Gemini artifact manifest JSON: {exc}") from exc
273
+ if not isinstance(data, dict):
274
+ raise ValidationError("Gemini artifact manifest must be a JSON object")
275
+ schema = str(data.get("schema") or "")
276
+ if schema not in _SUPPORTED_MANIFEST_SCHEMAS:
277
+ return None
278
+ artifact_kind = "image" if schema == ARTIFACT_IMAGE_MANIFEST_SCHEMA else "html"
279
+
280
+ raw_meta = read_note_meta(raw_file)
281
+ raw_chat_id = _extract_chat_id(raw_meta.get("fonte_id", ""))
282
+ chat_id = _manifest_chat_id(path, data, raw_chat_id)
283
+ if raw_chat_id and chat_id and chat_id != raw_chat_id:
284
+ raise ValidationError(
285
+ f"Gemini artifact manifest chatId {chat_id!r} does not match raw chat fonte_id {raw_chat_id!r}"
286
+ )
287
+
288
+ source_url = _source_url(raw_meta, chat_id or raw_chat_id, data)
289
+ artifacts: list[ArtifactHtml] = []
290
+ for index, item in enumerate(_artifact_items(data), start=1):
291
+ file_path = _item_path(item, path.parent)
292
+ if file_path is None:
293
+ raise ValidationError(f"Gemini artifact manifest item #{index} is missing a file path")
294
+ suffix = file_path.suffix.lower()
295
+ mime_type = _item_mime_type(item)
296
+ if artifact_kind == "html" and suffix != ".html":
297
+ raise ValidationError(f"Artifact file must remain an isolated .html file: {file_path}")
298
+ if artifact_kind == "image" and suffix not in _IMAGE_SUFFIXES:
299
+ raise ValidationError(f"Gemini image artifact file must be an image file: {file_path}")
300
+ if artifact_kind == "image" and mime_type and not mime_type.lower().startswith("image/"):
301
+ raise ValidationError(f"Gemini image artifact mimeType must start with image/: {file_path}")
302
+ if not file_path.exists():
303
+ raise MissingPathError(f"Gemini artifact file not found: {file_path}")
304
+ computed_sha = _sha256_file(file_path)
305
+ manifest_sha = _item_sha256(item)
306
+ if manifest_sha and manifest_sha != computed_sha:
307
+ raise ValidationError(f"Gemini artifact SHA-256 mismatch for {file_path}")
308
+ artifacts.append(
309
+ ArtifactHtml(
310
+ chat_id=chat_id or raw_chat_id,
311
+ source_url=source_url,
312
+ manifest_path=path,
313
+ file_path=file_path,
314
+ sha256=manifest_sha or computed_sha,
315
+ kind=artifact_kind,
316
+ turn_index=_item_turn_index(item),
317
+ mime_type=mime_type,
318
+ caption=_item_caption(item),
319
+ )
320
+ )
321
+
322
+ saved_count = _saved_count(data, len(artifacts))
323
+ if saved_count > 0 and not artifacts:
324
+ raise ValidationError("Gemini artifact manifest savedCount > 0 but no artifact files were listed")
325
+ if saved_count > 0 and len(artifacts) != saved_count:
326
+ raise ValidationError(
327
+ f"Gemini artifact manifest savedCount={saved_count} but listed {len(artifacts)} artifact files"
328
+ )
329
+ return ArtifactManifest(
330
+ path=path,
331
+ chat_id=chat_id or raw_chat_id,
332
+ source_url=source_url,
333
+ saved_count=saved_count,
334
+ artifacts=tuple(artifacts),
335
+ schema=schema,
336
+ )
337
+
338
+
339
+ def _explicit_manifest_paths(raw_file: Path) -> list[Path]:
340
+ meta = read_note_meta(raw_file)
341
+ paths: list[Path] = []
342
+ for key in _EXPLICIT_MANIFEST_KEYS:
343
+ value = meta.get(key)
344
+ if value:
345
+ paths.extend(_coerce_manifest_paths(value))
346
+ return paths
347
+
348
+
349
+ def _candidate_search_roots(raw_file: Path, artifact_dir: Path | None) -> list[Path]:
350
+ roots = [path for path in (artifact_dir, raw_file.parent, raw_file.parent / "artifacts", raw_file.parent.parent / "artifacts") if path]
351
+ seen: set[str] = set()
352
+ unique: list[Path] = []
353
+ for root in roots:
354
+ key = str(root.expanduser())
355
+ if key not in seen:
356
+ seen.add(key)
357
+ unique.append(root)
358
+ return unique
359
+
360
+
361
+ def discover_artifact_manifests(raw_file: Path, *, artifact_dir: Path | None = None) -> list[ArtifactManifest]:
362
+ """Find required Gemini artifact manifests for one raw chat."""
363
+
364
+ chat_id = chat_id_from_raw(raw_file)
365
+ candidates = _explicit_manifest_paths(raw_file)
366
+ if chat_id:
367
+ patterns = (
368
+ f"artifact-{chat_id}-manifest.json",
369
+ f"artifact-{chat_id}-images-manifest.json",
370
+ f"artifact-{chat_id}-image-manifest.json",
371
+ f"media-{chat_id}-manifest.json",
372
+ f"image-{chat_id}-manifest.json",
373
+ )
374
+ for root in _candidate_search_roots(raw_file, artifact_dir):
375
+ if root.is_file() and root.name in patterns:
376
+ candidates.append(root)
377
+ elif root.is_dir():
378
+ for pattern in patterns:
379
+ candidates.extend(sorted(root.rglob(pattern)))
380
+
381
+ manifests: list[ArtifactManifest] = []
382
+ seen: set[str] = set()
383
+ for candidate in candidates:
384
+ key = str(candidate.expanduser())
385
+ if key in seen:
386
+ continue
387
+ seen.add(key)
388
+ manifest = load_artifact_manifest(candidate, raw_file)
389
+ if manifest is not None and manifest.saved_count > 0:
390
+ manifests.append(manifest)
391
+ return manifests
392
+
393
+
394
+ def _file_uri_variants(path: Path) -> set[str]:
395
+ resolved = path.resolve()
396
+ variants = {resolved.as_uri(), resolved.as_posix(), str(path)}
397
+ if resolved.as_posix().startswith("/"):
398
+ variants.add("file://" + resolved.as_posix())
399
+ return variants
400
+
401
+
402
+ def _comment_fields(content: str) -> list[dict[str, str]]:
403
+ comments: list[dict[str, str]] = []
404
+ for match in _COMMENT_RE.finditer(content):
405
+ fields: dict[str, str] = {}
406
+ for line in match.group("body").splitlines():
407
+ if ":" not in line:
408
+ continue
409
+ key, value = line.split(":", 1)
410
+ fields[key.strip()] = value.strip()
411
+ comments.append(fields)
412
+ return comments
413
+
414
+
415
+ def _has_artifact_comment(comments: list[dict[str, str]], artifact: ArtifactHtml) -> bool:
416
+ for fields in comments:
417
+ if fields.get("chat_id", "") != artifact.chat_id:
418
+ continue
419
+ if fields.get("sha256", "").lower() != artifact.sha256.lower():
420
+ continue
421
+ if not _paths_match(fields.get("manifest", ""), artifact.manifest_path):
422
+ continue
423
+ if not _paths_match(fields.get("file", ""), artifact.file_path):
424
+ continue
425
+ return True
426
+ return False
427
+
428
+
429
+ def _has_iframe(content: str, variants: set[str]) -> bool:
430
+ return any(src.strip() in variants for _quote, src in _IFRAME_SRC_RE.findall(content))
431
+
432
+
433
+ def _has_markdown_link(content: str, variants: set[str]) -> bool:
434
+ return any(target.strip() in variants for target in _MARKDOWN_LINK_RE.findall(content))
435
+
436
+
437
+ def _has_markdown_image(content: str, variants: set[str]) -> bool:
438
+ return any(target.strip() in variants for target in _MARKDOWN_IMAGE_RE.findall(content))
439
+
440
+
441
+ def _has_caption(content: str, artifact: ArtifactHtml) -> bool:
442
+ if not artifact.caption:
443
+ return bool(re.search(r"(?im)^\s*\*?\s*(?:Figura|Legenda)\s*:", content))
444
+ return artifact.caption in content
445
+
446
+
447
+ def _artifact_key(artifact: ArtifactHtml) -> str:
448
+ return f"{artifact.kind}:{artifact.sha256}:{artifact.file_path}"
449
+
450
+
451
+ def _artifact_presence(content: str, artifact: ArtifactHtml) -> dict[str, Any]:
452
+ variants = _file_uri_variants(artifact.file_path)
453
+ has_iframe = _has_iframe(content, variants)
454
+ has_link = _has_markdown_link(content, variants)
455
+ has_image = _has_markdown_image(content, variants)
456
+ has_caption = _has_caption(content, artifact)
457
+ has_comment = _has_artifact_comment(_comment_fields(content), artifact)
458
+ missing_parts = []
459
+ if artifact.kind == "html":
460
+ if not has_iframe:
461
+ missing_parts.append("iframe")
462
+ if not has_link:
463
+ missing_parts.append("Markdown link")
464
+ complete = has_iframe and has_link and has_comment
465
+ touched = has_iframe or has_link or has_comment
466
+ else:
467
+ if not has_image:
468
+ missing_parts.append("Markdown image")
469
+ if not has_caption:
470
+ missing_parts.append("caption")
471
+ complete = has_image and has_caption and has_comment
472
+ touched = has_image or has_caption or has_comment
473
+ if not has_comment:
474
+ missing_parts.append("gemini-artifact provenance comment")
475
+ return {
476
+ "artifact": artifact.to_json(),
477
+ "key": _artifact_key(artifact),
478
+ "has_iframe": has_iframe,
479
+ "has_markdown_link": has_link,
480
+ "has_markdown_image": has_image,
481
+ "has_caption": has_caption,
482
+ "has_provenance_comment": has_comment,
483
+ "complete": complete,
484
+ "touched": touched,
485
+ "missing_parts": missing_parts,
486
+ }
487
+
488
+
489
+ def _note_artifact_report(
490
+ content: str,
491
+ *,
492
+ manifests: list[ArtifactManifest],
493
+ artifacts: list[ArtifactHtml],
494
+ note: str | None = None,
495
+ ) -> dict[str, Any]:
496
+ statuses = [_artifact_presence(content, artifact) for artifact in artifacts]
497
+ included = [status["artifact"] for status in statuses if status["complete"]]
498
+ missing = [status["artifact"] for status in statuses if not status["complete"]]
499
+ partial = [status for status in statuses if status["touched"] and not status["complete"]]
500
+ errors: list[str] = []
501
+ if artifacts and _FULL_HTML_RE.search(content):
502
+ errors.append("captured HTML must stay in isolated .html files; do not inline full HTML into the Markdown note")
503
+ for status in partial:
504
+ errors.append(
505
+ "incomplete Gemini artifact declaration"
506
+ + (f" in {note}" if note else "")
507
+ + f" for {status['artifact']['file']}: missing {', '.join(status['missing_parts'])}"
508
+ )
509
+
510
+ result: dict[str, Any] = {
511
+ "schema": ARTIFACT_VALIDATION_SCHEMA,
512
+ "scope": "note",
513
+ "required": bool(artifacts),
514
+ "manifest_count": len(manifests),
515
+ "artifact_count": len(artifacts),
516
+ "included_artifact_count": len(included),
517
+ "missing_artifact_count": len(missing),
518
+ "manifests": [manifest.to_json() for manifest in manifests],
519
+ "artifacts": [artifact.to_json() for artifact in artifacts],
520
+ "included_artifacts": included,
521
+ "missing_artifacts": missing,
522
+ "partial_artifacts": partial,
523
+ "errors": errors,
524
+ }
525
+ if note is not None:
526
+ result["note"] = note
527
+ return result
528
+
529
+
530
+ def required_artifacts_for_raw(raw_file: Path, *, artifact_dir: Path | None = None) -> list[ArtifactHtml]:
531
+ artifacts: list[ArtifactHtml] = []
532
+ for manifest in discover_artifact_manifests(raw_file, artifact_dir=artifact_dir):
533
+ artifacts.extend(manifest.artifacts)
534
+ return artifacts
535
+
536
+
537
+ def validate_note_artifacts(
538
+ content: str,
539
+ *,
540
+ raw_file: Path,
541
+ artifact_dir: Path | None = None,
542
+ ) -> dict[str, Any]:
543
+ """Validate one note's artifact syntax without requiring full raw-chat coverage."""
544
+
545
+ manifests = discover_artifact_manifests(raw_file, artifact_dir=artifact_dir)
546
+ artifacts = [artifact for manifest in manifests for artifact in manifest.artifacts]
547
+ result = _note_artifact_report(content, manifests=manifests, artifacts=artifacts)
548
+ if result["errors"]:
549
+ raise ValidationError("Gemini artifact validation failed: " + "; ".join(result["errors"]))
550
+ return result
551
+
552
+
553
+ def validate_artifact_batch(
554
+ notes: list[dict[str, str]],
555
+ *,
556
+ raw_file: Path,
557
+ artifact_dir: Path | None = None,
558
+ ) -> dict[str, Any]:
559
+ """Validate that a raw chat's staged note group covers all required artifacts."""
560
+
561
+ manifests = discover_artifact_manifests(raw_file, artifact_dir=artifact_dir)
562
+ artifacts = [artifact for manifest in manifests for artifact in manifest.artifacts]
563
+ note_reports = [
564
+ _note_artifact_report(
565
+ str(note.get("content") or ""),
566
+ manifests=manifests,
567
+ artifacts=artifacts,
568
+ note=str(note.get("title") or note.get("content_path") or ""),
569
+ )
570
+ for note in notes
571
+ ]
572
+ included_keys = {
573
+ f"{item.get('kind', 'html')}:{item['sha256']}:{_path(str(item['file']))}"
574
+ for report in note_reports
575
+ for item in report["included_artifacts"]
576
+ }
577
+ missing = [artifact for artifact in artifacts if _artifact_key(artifact) not in included_keys]
578
+ errors = [
579
+ error
580
+ for report in note_reports
581
+ for error in report["errors"]
582
+ ]
583
+ for artifact in missing:
584
+ errors.append(f"missing required Gemini artifact across staged note group: {artifact.file_path}")
585
+
586
+ result: dict[str, Any] = {
587
+ "schema": ARTIFACT_VALIDATION_SCHEMA,
588
+ "scope": "raw_chat_batch",
589
+ "required": bool(artifacts),
590
+ "manifest_count": len(manifests),
591
+ "artifact_count": len(artifacts),
592
+ "covered_artifact_count": len(artifacts) - len(missing),
593
+ "missing_artifact_count": len(missing),
594
+ "manifests": [manifest.to_json() for manifest in manifests],
595
+ "artifacts": [artifact.to_json() for artifact in artifacts],
596
+ "missing_artifacts": [artifact.to_json() for artifact in missing],
597
+ "notes": note_reports,
598
+ "errors": errors,
599
+ }
600
+ if errors:
601
+ raise ValidationError(
602
+ "Gemini artifact batch validation failed (Gemini artifact HTML batch validation failed): "
603
+ + "; ".join(errors)
604
+ )
605
+ return result