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,682 @@
1
+ #!/usr/bin/env python3
2
+ """Resolve /flashcards note scopes into a deterministic JSON manifest.
3
+
4
+ The Gemini agent owns flashcard reasoning and Anki writes. This script owns the
5
+ auditable filesystem step before that: expanding files/directories/globs/tags,
6
+ building portable Obsidian deeplinks, and deriving the destination deck for each
7
+ source note.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import glob
13
+ import hashlib
14
+ import json
15
+ import os
16
+ import re
17
+ import shlex
18
+ import sys
19
+ import time
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ SCRIPT_DIR = Path(__file__).resolve().parent
25
+ MEDNOTES_DIR = SCRIPT_DIR.parent
26
+ if str(MEDNOTES_DIR) not in sys.path:
27
+ sys.path.insert(0, str(MEDNOTES_DIR))
28
+
29
+ from mednotes.domains.flashcards.contracts import FlashcardSourceManifest, FlashcardSourceNote # noqa: E402
30
+ from mednotes.domains.flashcards.obsidian_links import ( # noqa: E402
31
+ build_obsidian_link_candidates,
32
+ detect_path_style,
33
+ )
34
+ from mednotes.domains.flashcards.obsidian_note_utils import ( # noqa: E402
35
+ EXIT_IO,
36
+ EXIT_OK,
37
+ MissingPathError,
38
+ NoteUtilsError,
39
+ UsageError,
40
+ infer_vault_root,
41
+ normalize_tag,
42
+ )
43
+ from mednotes.platform.feedback import command_string, safe_record_workflow_run # noqa: E402
44
+ from mednotes.platform.paths import resolve_wiki_dir # noqa: E402
45
+
46
+ SCHEMA = "medical-notes-workbench.flashcard-sources.v1"
47
+ DEFAULT_CONFIRM_FILE_LIMIT = 10
48
+ DEFAULT_CONFIRM_CARD_LIMIT = 40
49
+ IGNORED_DIRS = {
50
+ ".git",
51
+ ".hg",
52
+ ".svn",
53
+ ".obsidian",
54
+ "__pycache__",
55
+ ".pytest_cache",
56
+ ".mypy_cache",
57
+ ".ruff_cache",
58
+ ".venv",
59
+ "node_modules",
60
+ "dist",
61
+ "attachments",
62
+ "assets",
63
+ }
64
+ MARKDOWN_EXTENSIONS = {".md", ".markdown"}
65
+ INLINE_TAG_RE = re.compile(r"(?<![\w/])#([A-Za-z0-9_/-]+)")
66
+ TAG_KEY_RE = re.compile(r"^\s*(tags?|Tags?)\s*:\s*(?P<value>.*)$")
67
+ LIST_ITEM_RE = re.compile(r"^\s*-\s*(?P<value>.*?)\s*$")
68
+ PATHISH_RE = re.compile(r"^([~./$]|[A-Za-z]:|.*[/\\]|.*\.(?:md|markdown)$|.*[*?\[\]]).*$")
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class Scope:
73
+ raw: str
74
+ explicit_inputs: tuple[str, ...]
75
+ tags: tuple[str, ...]
76
+ skip_tags: tuple[str, ...]
77
+ folders: tuple[str, ...]
78
+ tag_match: str
79
+
80
+
81
+ def _path(value: str | os.PathLike[str]) -> Path:
82
+ return Path(os.path.expandvars(str(value))).expanduser()
83
+
84
+
85
+ def _is_relative_to(path: Path, parent: Path) -> bool:
86
+ try:
87
+ path.relative_to(parent)
88
+ except ValueError:
89
+ return False
90
+ return True
91
+
92
+
93
+ def _json(data: Any) -> None:
94
+ print(json.dumps(data, ensure_ascii=False, indent=2))
95
+
96
+
97
+ def _dedupe_preserve(items: list[str]) -> tuple[str, ...]:
98
+ seen: set[str] = set()
99
+ result: list[str] = []
100
+ for item in items:
101
+ if item not in seen:
102
+ result.append(item)
103
+ seen.add(item)
104
+ return tuple(result)
105
+
106
+
107
+ def _strip_quotes(value: str) -> str:
108
+ value = value.strip()
109
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
110
+ return value[1:-1]
111
+ return value
112
+
113
+
114
+ def _existing_pathish_token_span(tokens: list[str], start: int) -> tuple[str, int] | None:
115
+ max_span = min(len(tokens), start + 16)
116
+ for end in range(max_span, start, -1):
117
+ candidate = " ".join(tokens[start:end])
118
+ path = _path(candidate)
119
+ if path.exists() or (_has_glob(str(path)) and glob.glob(str(path), recursive=True)):
120
+ return candidate, end
121
+ return None
122
+
123
+
124
+ def _join_existing_path_tokens(tokens: list[str]) -> list[str]:
125
+ joined: list[str] = []
126
+ idx = 0
127
+ while idx < len(tokens):
128
+ span = _existing_pathish_token_span(tokens, idx)
129
+ if span:
130
+ value, idx = span
131
+ joined.append(value)
132
+ continue
133
+ joined.append(tokens[idx])
134
+ idx += 1
135
+ return joined
136
+
137
+
138
+ def _strip_inline_comment(value: str) -> str:
139
+ quote_char: str | None = None
140
+ bracket_depth = 0
141
+ for idx, char in enumerate(value):
142
+ if char in {"'", '"'}:
143
+ quote_char = None if quote_char == char else char
144
+ elif char == "[" and quote_char is None:
145
+ bracket_depth += 1
146
+ elif char == "]" and quote_char is None and bracket_depth:
147
+ bracket_depth -= 1
148
+ if (
149
+ char == "#"
150
+ and quote_char is None
151
+ and bracket_depth == 0
152
+ and idx > 0
153
+ and value[idx - 1].isspace()
154
+ ):
155
+ return value[:idx].rstrip()
156
+ return value.strip()
157
+
158
+
159
+ def _parse_tag_values(value: str) -> list[str]:
160
+ value = _strip_inline_comment(value).strip()
161
+ if not value:
162
+ return []
163
+ if value.startswith("[") and value.endswith("]"):
164
+ value = value[1:-1]
165
+ raw_items = [part.strip() for part in value.split(",")] if "," in value else [value]
166
+ tags: list[str] = []
167
+ for raw in raw_items:
168
+ item = _strip_quotes(raw).lstrip("#").strip()
169
+ if item:
170
+ tags.append(normalize_tag(item))
171
+ return tags
172
+
173
+
174
+ def split_frontmatter(text: str) -> tuple[list[str] | None, str]:
175
+ lines = text.splitlines(keepends=True)
176
+ if not lines or lines[0].strip() != "---":
177
+ return None, text
178
+ for idx in range(1, len(lines)):
179
+ if lines[idx].strip() == "---":
180
+ return lines[1:idx], "".join(lines[idx + 1 :])
181
+ return None, text
182
+
183
+
184
+ def extract_frontmatter_tags(text: str) -> list[str]:
185
+ frontmatter, _body = split_frontmatter(text)
186
+ if frontmatter is None:
187
+ return []
188
+
189
+ tags: list[str] = []
190
+ idx = 0
191
+ while idx < len(frontmatter):
192
+ line = frontmatter[idx]
193
+ match = TAG_KEY_RE.match(line)
194
+ if not match:
195
+ idx += 1
196
+ continue
197
+ value = _strip_inline_comment(match.group("value"))
198
+ if value:
199
+ tags.extend(_parse_tag_values(value))
200
+ idx += 1
201
+ continue
202
+ idx += 1
203
+ while idx < len(frontmatter):
204
+ item = LIST_ITEM_RE.match(frontmatter[idx])
205
+ if not item:
206
+ break
207
+ item_value = _strip_inline_comment(item.group("value"))
208
+ tags.extend(_parse_tag_values(item_value))
209
+ idx += 1
210
+
211
+ return list(dict.fromkeys(tags))
212
+
213
+
214
+ def extract_inline_tags(text: str) -> list[str]:
215
+ _frontmatter, body = split_frontmatter(text)
216
+ tags: list[str] = []
217
+ in_fence = False
218
+ for line in body.splitlines():
219
+ stripped = line.strip()
220
+ if stripped.startswith(("```", "~~~")):
221
+ in_fence = not in_fence
222
+ continue
223
+ if in_fence:
224
+ continue
225
+ for match in INLINE_TAG_RE.finditer(line):
226
+ try:
227
+ tags.append(normalize_tag(match.group(1)))
228
+ except UsageError:
229
+ continue
230
+ return list(dict.fromkeys(tags))
231
+
232
+
233
+ def _has_glob(value: str) -> bool:
234
+ return any(char in value for char in "*?[")
235
+
236
+
237
+ def _ignored_path(path: Path) -> bool:
238
+ return any(part in IGNORED_DIRS for part in path.parts)
239
+
240
+
241
+ def _markdown_files_under(directory: Path) -> list[Path]:
242
+ files: list[Path] = []
243
+ for root, dirs, names in os.walk(directory):
244
+ dirs[:] = sorted(d for d in dirs if d not in IGNORED_DIRS)
245
+ root_path = Path(root)
246
+ if _ignored_path(root_path):
247
+ continue
248
+ for name in sorted(names):
249
+ candidate = root_path / name
250
+ if candidate.suffix.lower() in MARKDOWN_EXTENSIONS and not _ignored_path(candidate):
251
+ files.append(candidate.resolve())
252
+ return files
253
+
254
+
255
+ def _expand_input(value: str, *, strict: bool = True) -> list[Path]:
256
+ path = _path(value)
257
+ matches: list[Path] = []
258
+ if _has_glob(str(path)):
259
+ matches = [Path(match) for match in glob.glob(str(path), recursive=True)]
260
+ if not matches and strict:
261
+ raise MissingPathError(f"No files matched glob: {value}")
262
+ elif path.exists():
263
+ matches = [path]
264
+ elif strict:
265
+ raise MissingPathError(f"Path not found: {value}")
266
+ else:
267
+ return []
268
+
269
+ files: list[Path] = []
270
+ for match in matches:
271
+ resolved = match.resolve()
272
+ if resolved.is_dir():
273
+ files.extend(_markdown_files_under(resolved))
274
+ elif resolved.is_file() and resolved.suffix.lower() in MARKDOWN_EXTENSIONS:
275
+ files.append(resolved)
276
+ elif strict and resolved.exists():
277
+ raise UsageError(f"Expected Markdown file or directory, got: {resolved}")
278
+ return files
279
+
280
+
281
+ def _normalize_scope(args: argparse.Namespace) -> Scope:
282
+ raw_parts = list(args.scope or [])
283
+ raw = " ".join(raw_parts).strip()
284
+ tokens: list[str] = []
285
+ for part in raw_parts:
286
+ try:
287
+ tokens.extend(shlex.split(part, posix=os.name != "nt"))
288
+ except ValueError:
289
+ tokens.extend(part.split())
290
+ tokens = _join_existing_path_tokens(tokens)
291
+ looks_like_note_query = any(
292
+ token.lower() in {"nota", "notas", "arquivo", "arquivos"} for token in tokens
293
+ )
294
+
295
+ explicit_inputs = list(args.inputs or [])
296
+ tags = [normalize_tag(tag) for tag in (args.tag or [])]
297
+ skip_tags = [normalize_tag(tag) for tag in (args.skip_tag or [])]
298
+ folders = list(args.folder or [])
299
+
300
+ idx = 0
301
+ while idx < len(tokens):
302
+ token = tokens[idx]
303
+ lower = token.lower()
304
+ if token.startswith("#"):
305
+ tags.append(normalize_tag(token))
306
+ elif lower in {"tag", "tags"} and idx + 1 < len(tokens):
307
+ tags.append(normalize_tag(tokens[idx + 1]))
308
+ idx += 1
309
+ elif lower in {"pasta", "folder"} and idx + 1 < len(tokens):
310
+ folders.append(tokens[idx + 1])
311
+ idx += 1
312
+ elif (
313
+ lower in {"em", "na", "no", "dentro"}
314
+ and idx + 2 < len(tokens)
315
+ and tokens[idx + 1].lower() in {"pasta", "folder"}
316
+ ):
317
+ folders.append(tokens[idx + 2])
318
+ idx += 2
319
+ elif lower in {"em", "na", "no", "dentro"} and idx + 1 < len(tokens):
320
+ candidate = tokens[idx + 1]
321
+ if PATHISH_RE.match(candidate) or _path(candidate).exists():
322
+ explicit_inputs.append(candidate)
323
+ elif looks_like_note_query:
324
+ folders.append(candidate)
325
+ idx += 1
326
+ elif PATHISH_RE.match(token) or _path(token).exists():
327
+ explicit_inputs.append(token)
328
+ idx += 1
329
+
330
+ return Scope(
331
+ raw=raw,
332
+ explicit_inputs=_dedupe_preserve(explicit_inputs),
333
+ tags=_dedupe_preserve(tags),
334
+ skip_tags=_dedupe_preserve(skip_tags),
335
+ folders=_dedupe_preserve(folders),
336
+ tag_match=args.tag_match,
337
+ )
338
+
339
+
340
+ def _root_from_args(args: argparse.Namespace) -> Path | None:
341
+ root_value = args.vault_root or args.wiki_dir
342
+ resolution = resolve_wiki_dir(explicit=root_value, config=args.config, enable_gemini_probe=False)
343
+ return resolution.path if resolution.ok else None
344
+
345
+
346
+ def _find_folder(root: Path, name_or_path: str) -> Path:
347
+ direct = _path(name_or_path)
348
+ if direct.exists():
349
+ if not direct.is_dir():
350
+ raise UsageError(f"Expected folder path, got: {direct}")
351
+ return direct.resolve()
352
+
353
+ matches: list[Path] = []
354
+ target = name_or_path.casefold()
355
+ for current, dirs, _names in os.walk(root):
356
+ dirs[:] = sorted(d for d in dirs if d not in IGNORED_DIRS)
357
+ for dirname in dirs:
358
+ if dirname.casefold() == target:
359
+ matches.append((Path(current) / dirname).resolve())
360
+
361
+ if not matches:
362
+ raise MissingPathError(f"Folder not found under {root}: {name_or_path}")
363
+ if len(matches) > 1:
364
+ joined = "\n".join(f"- {match}" for match in matches[:20])
365
+ raise UsageError(f"Ambiguous folder name {name_or_path!r}; pass a full path. Matches:\n{joined}")
366
+ return matches[0]
367
+
368
+
369
+ def _candidate_files(scope: Scope, args: argparse.Namespace) -> tuple[list[Path], list[str]]:
370
+ warnings: list[str] = []
371
+ files: list[Path] = []
372
+
373
+ for raw_input in scope.explicit_inputs:
374
+ expanded = _expand_input(raw_input, strict=not args.scope)
375
+ if not expanded and args.scope:
376
+ warnings.append(f"No Markdown files matched input: {raw_input}")
377
+ files.extend(expanded)
378
+
379
+ root = _root_from_args(args)
380
+ if scope.folders:
381
+ if root is None:
382
+ raise UsageError(
383
+ "Folder filters need --vault-root, --wiki-dir, or app config.toml [paths].wiki_dir."
384
+ )
385
+ for folder in scope.folders:
386
+ files.extend(_markdown_files_under(_find_folder(root, folder)))
387
+
388
+ if scope.tags and not files:
389
+ if root is None:
390
+ raise UsageError(
391
+ "Tag filters need --vault-root, --wiki-dir, or app config.toml [paths].wiki_dir."
392
+ )
393
+ files.extend(_markdown_files_under(root))
394
+
395
+ if not files and scope.raw:
396
+ warnings.append("No Markdown note scope was resolved; treat the input as pasted text or ask for a path.")
397
+
398
+ deduped = sorted(set(files), key=lambda item: item.as_posix().casefold())
399
+ return deduped, warnings
400
+
401
+
402
+ def _note_matches_tags(tags: set[str], desired: tuple[str, ...], mode: str) -> bool:
403
+ if not desired:
404
+ return True
405
+ desired_set = set(desired)
406
+ if mode == "any":
407
+ return bool(tags & desired_set)
408
+ return desired_set.issubset(tags)
409
+
410
+
411
+ def _deck_for_note(path: Path, root: Path) -> str:
412
+ relative = path.relative_to(root)
413
+ parts = [root.name, *relative.with_suffix("").parts]
414
+ return "::".join(parts)
415
+
416
+
417
+ def _fallback_deck_for_note(path: Path) -> str:
418
+ parent = path.parent.name if path.parent.name else "Inbox"
419
+ return f"Medicina::{parent}::{path.stem}"
420
+
421
+
422
+ def _manifest_note(path: Path, args: argparse.Namespace) -> FlashcardSourceNote:
423
+ text = path.read_text(encoding="utf-8")
424
+ root = infer_vault_root(path, explicit_root=args.vault_root or args.wiki_dir)
425
+ configured = _root_from_args(args)
426
+ if root is None and configured and _is_relative_to(path, configured):
427
+ root = configured
428
+
429
+ frontmatter_tags = extract_frontmatter_tags(text)
430
+ inline_tags = extract_inline_tags(text)
431
+ all_tags = list(dict.fromkeys([*frontmatter_tags, *inline_tags]))
432
+ absolute_path = str(path)
433
+ path_style = detect_path_style(absolute_path)
434
+ relative = path.relative_to(root).as_posix() if root else path.name
435
+ vault_name = (args.vault_name or root.name) if root else ""
436
+ link_candidates = build_obsidian_link_candidates(
437
+ absolute_path=absolute_path,
438
+ path_style=path_style,
439
+ vault_name=vault_name,
440
+ vault_relative_path=relative if root else "",
441
+ )
442
+ selected = link_candidates[0]
443
+ heading_count = sum(1 for line in text.splitlines() if line.lstrip().startswith("#"))
444
+
445
+ return FlashcardSourceNote.model_validate(
446
+ {
447
+ "path": str(path),
448
+ "title": path.stem,
449
+ "absolute_path": absolute_path,
450
+ "path_style": str(path_style),
451
+ "vault_root": str(root) if root else "",
452
+ "vault_name": vault_name,
453
+ "vault_relative_path": relative,
454
+ "link_mode": str(selected.mode),
455
+ "deeplink": selected.uri,
456
+ "deeplink_mode": str(selected.mode),
457
+ "deeplink_candidates": [candidate.uri for candidate in link_candidates],
458
+ "deck": _deck_for_note(path, root) if root else _fallback_deck_for_note(path),
459
+ "frontmatter_tags": frontmatter_tags,
460
+ "inline_tags": inline_tags,
461
+ "tags": all_tags,
462
+ "already_marked_anki": "anki" in all_tags,
463
+ "content_sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
464
+ "line_count": len(text.splitlines()),
465
+ "heading_count": heading_count,
466
+ }
467
+ )
468
+
469
+
470
+ def resolve_manifest(args: argparse.Namespace) -> dict[str, Any]:
471
+ scope = _normalize_scope(args)
472
+ files, warnings = _candidate_files(scope, args)
473
+ candidate_notes = [_manifest_note(path, args) for path in files]
474
+ tag_matched_notes = [
475
+ note
476
+ for note in candidate_notes
477
+ if _note_matches_tags(set(note.tags), scope.tags, scope.tag_match)
478
+ ]
479
+ skipped_notes: list[FlashcardSourceNote] = []
480
+ notes: list[FlashcardSourceNote] = []
481
+ skip_tags = set(scope.skip_tags)
482
+ for note in tag_matched_notes:
483
+ matched_skip_tags = sorted(set(note.tags) & skip_tags)
484
+ if matched_skip_tags:
485
+ skipped_notes.append(
486
+ FlashcardSourceNote.model_validate(
487
+ {
488
+ **note.to_payload(),
489
+ "skip_reason": "skip_tag",
490
+ "skip_tags": matched_skip_tags,
491
+ }
492
+ )
493
+ )
494
+ else:
495
+ notes.append(note)
496
+
497
+ confirmation_reasons: list[str] = []
498
+ if len(notes) > args.confirm_file_limit:
499
+ confirmation_reasons.append(f"more_than_{args.confirm_file_limit}_files")
500
+
501
+ scope_root = _root_from_args(args)
502
+ manifest = {
503
+ "schema": SCHEMA,
504
+ "dry_run": args.dry_run,
505
+ "scope": {
506
+ "raw": scope.raw,
507
+ "inputs": list(scope.explicit_inputs),
508
+ "tags": list(scope.tags),
509
+ "skip_tags": list(scope.skip_tags),
510
+ "folders": list(scope.folders),
511
+ "tag_match": scope.tag_match,
512
+ "vault_root": str(scope_root) if scope_root else "",
513
+ "vault_name": args.vault_name or "",
514
+ },
515
+ "summary": {
516
+ "candidate_file_count": len(tag_matched_notes),
517
+ "file_count": len(notes),
518
+ "skipped_count": len(skipped_notes),
519
+ "requires_confirmation": bool(confirmation_reasons),
520
+ "confirmation_reasons": confirmation_reasons,
521
+ "card_candidate_confirmation_limit": args.confirm_card_limit,
522
+ },
523
+ "notes": [note.to_payload() for note in notes],
524
+ "skipped_notes": [note.to_payload() for note in skipped_notes],
525
+ "warnings": warnings,
526
+ }
527
+ manifest_model = FlashcardSourceManifest.model_validate(manifest)
528
+ return manifest_model.to_payload()
529
+
530
+
531
+ def _cmd_resolve(args: argparse.Namespace) -> int:
532
+ started_at = time.time()
533
+ manifest = resolve_manifest(args)
534
+ _json(manifest)
535
+ _record_feedback(manifest, EXIT_OK, started_at, phase="flashcards_sources_resolve")
536
+ return EXIT_OK
537
+
538
+
539
+ def format_preview(manifest: dict[str, Any]) -> str:
540
+ summary = manifest["summary"]
541
+ scope = manifest["scope"]
542
+ lines = [
543
+ "Flashcard source preview",
544
+ f"- Processar: {summary['file_count']} nota(s)",
545
+ f"- Puladas: {summary['skipped_count']} nota(s)",
546
+ f"- Candidatas antes dos filtros de pulo: {summary['candidate_file_count']} nota(s)",
547
+ f"- Confirmacao necessaria: {'sim' if summary['requires_confirmation'] else 'nao'}",
548
+ ]
549
+ if scope["tags"]:
550
+ lines.append(f"- Tags exigidas: {', '.join(scope['tags'])}")
551
+ if scope["skip_tags"]:
552
+ lines.append(f"- Tags puladas: {', '.join(scope['skip_tags'])}")
553
+ if scope["folders"]:
554
+ lines.append(f"- Pastas: {', '.join(scope['folders'])}")
555
+
556
+ if manifest["notes"]:
557
+ lines.append("")
558
+ lines.append("Notas que serao processadas:")
559
+ for note in manifest["notes"]:
560
+ lines.append(f"- {note['vault_relative_path']} -> {note['deck']}")
561
+
562
+ if manifest["skipped_notes"]:
563
+ lines.append("")
564
+ lines.append("Notas puladas:")
565
+ for note in manifest["skipped_notes"]:
566
+ reason = note.get("skip_reason", "skip")
567
+ tags = ", ".join(note.get("skip_tags", []))
568
+ suffix = f" ({reason}: {tags})" if tags else f" ({reason})"
569
+ lines.append(f"- {note['vault_relative_path']}{suffix}")
570
+
571
+ if manifest["warnings"]:
572
+ lines.append("")
573
+ lines.append("Avisos:")
574
+ lines.extend(f"- {warning}" for warning in manifest["warnings"])
575
+
576
+ return "\n".join(lines) + "\n"
577
+
578
+
579
+ def _cmd_preview(args: argparse.Namespace) -> int:
580
+ started_at = time.time()
581
+ manifest = resolve_manifest(args)
582
+ print(format_preview(manifest), end="")
583
+ _record_feedback(manifest, EXIT_OK, started_at, phase="flashcards_sources_preview")
584
+ return EXIT_OK
585
+
586
+
587
+ def _record_feedback(manifest: dict[str, Any], exit_code: int, started_at: float, *, phase: str) -> None:
588
+ summary = manifest.get("summary") if isinstance(manifest.get("summary"), dict) else {}
589
+ safe_record_workflow_run(
590
+ workflow="/flashcards",
591
+ command=command_string(),
592
+ payload={
593
+ **manifest,
594
+ "phase": phase,
595
+ "status": "completed_with_warnings" if manifest.get("warnings") or summary.get("requires_confirmation") else "completed",
596
+ "next_action": "Confirmar escopo amplo antes de criar cards." if summary.get("requires_confirmation") else "",
597
+ "required_inputs": ["notes", "scope"],
598
+ },
599
+ exit_code=exit_code,
600
+ started_at=started_at,
601
+ )
602
+
603
+
604
+ def _add_resolve_arguments(parser: argparse.ArgumentParser) -> None:
605
+ parser.add_argument("inputs", nargs="*", help="explicit Markdown files, directories, or globs")
606
+ parser.add_argument(
607
+ "--scope",
608
+ action="append",
609
+ default=[],
610
+ help="free-form /flashcards argument text to parse for paths, folders, and #tags",
611
+ )
612
+ parser.add_argument("--tag", action="append", default=[], help="Obsidian tag filter")
613
+ parser.add_argument("--skip-tag", action="append", default=[], help="exclude notes with this Obsidian tag")
614
+ parser.add_argument("--folder", action="append", default=[], help="folder name/path filter")
615
+ parser.add_argument("--tag-match", choices=("all", "any"), default="all")
616
+ parser.add_argument("--vault-root", help="Obsidian vault/wiki root used for links and deck names")
617
+ parser.add_argument("--wiki-dir", help="alias for --vault-root in this project")
618
+ parser.add_argument("--vault-name", help="override the vault name encoded in obsidian:// links")
619
+ parser.add_argument("--config", help="optional config.toml; legacy fallback for wiki_dir/vault.path")
620
+ parser.add_argument("--dry-run", action="store_true", help="mark the manifest as preview-only")
621
+ parser.add_argument("--confirm-file-limit", type=int, default=DEFAULT_CONFIRM_FILE_LIMIT)
622
+ parser.add_argument("--confirm-card-limit", type=int, default=DEFAULT_CONFIRM_CARD_LIMIT)
623
+
624
+
625
+ def build_parser() -> argparse.ArgumentParser:
626
+ parser = argparse.ArgumentParser(description=__doc__)
627
+ sub = parser.add_subparsers(dest="command", required=True)
628
+
629
+ resolve = sub.add_parser("resolve", help="resolve files/folders/globs/tags into a JSON manifest")
630
+ _add_resolve_arguments(resolve)
631
+ resolve.set_defaults(func=_cmd_resolve)
632
+
633
+ preview = sub.add_parser("preview", help="print a human-readable preview for a resolved /flashcards scope")
634
+ _add_resolve_arguments(preview)
635
+ preview.set_defaults(func=_cmd_preview)
636
+
637
+ return parser
638
+
639
+
640
+ def main(argv: list[str] | None = None) -> int:
641
+ parser = build_parser()
642
+ args = parser.parse_args(argv)
643
+ try:
644
+ return args.func(args)
645
+ except NoteUtilsError as exc:
646
+ safe_record_workflow_run(
647
+ workflow="/flashcards",
648
+ command=command_string(),
649
+ payload={
650
+ "phase": f"flashcards_sources_{getattr(args, 'command', 'unknown')}",
651
+ "status": "failed",
652
+ "blocked_reason": exc.__class__.__name__,
653
+ "next_action": "Corrigir o escopo de fontes e rodar /flashcards novamente.",
654
+ "error": str(exc),
655
+ },
656
+ exit_code=exc.exit_code,
657
+ started_at=time.time(),
658
+ snippets=[str(exc)],
659
+ )
660
+ print(str(exc), file=sys.stderr)
661
+ return exc.exit_code
662
+ except OSError as exc:
663
+ safe_record_workflow_run(
664
+ workflow="/flashcards",
665
+ command=command_string(),
666
+ payload={
667
+ "phase": f"flashcards_sources_{getattr(args, 'command', 'unknown')}",
668
+ "status": "failed",
669
+ "blocked_reason": "OSError",
670
+ "next_action": "Corrigir caminho/permissao e rodar /flashcards novamente.",
671
+ "error": str(exc),
672
+ },
673
+ exit_code=EXIT_IO,
674
+ started_at=time.time(),
675
+ snippets=[str(exc)],
676
+ )
677
+ print(str(exc), file=sys.stderr)
678
+ return EXIT_IO
679
+
680
+
681
+ if __name__ == "__main__":
682
+ raise SystemExit(main())