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,718 @@
1
+ """Reference repair planning for the Wiki link workflow.
2
+
3
+ The planner turns graph-audit issues into an explicit note-by-note action list.
4
+ It intentionally avoids reading or emitting note body snippets; callers get
5
+ targets, lines, paths and decision packets only.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from collections.abc import Sequence
11
+ from pathlib import Path
12
+ from typing import Literal
13
+
14
+ from pydantic import ConfigDict, Field, field_validator
15
+
16
+ from mednotes.domains.wiki.capabilities.notes.raw_chats import atomic_write_text
17
+ from mednotes.domains.wiki.capabilities.vocabulary.link_terms import normalize_key, obsidian_target_name
18
+ from mednotes.domains.wiki.contracts.workflow_outcomes import (
19
+ DecisionEvidence,
20
+ HumanDecisionOption,
21
+ HumanDecisionPacket,
22
+ RejectedAutomation,
23
+ WorkflowDecision,
24
+ WorkflowDecisionSummary,
25
+ )
26
+ from mednotes.kernel.base import ContractModel, JsonObject
27
+
28
+ REFERENCE_REPAIR_PLAN_SCHEMA = "medical-notes-workbench.reference-repair-plan.v1"
29
+
30
+ _CATALOG_OPERATIONAL_BLOCKERS = {"catalog_invalid_json"}
31
+ _GRAPH_LINK_CODES = {"dangling_link", "ambiguous_link", "self_link"}
32
+ _WIKILINK_RE = re.compile(r"(!?)\[\[([^\]]+)\]\]")
33
+ _FENCED_RE = re.compile(r"```.*?```", re.DOTALL)
34
+ _FRONTMATTER_RE = re.compile(r"\A---\n.*?\n---\n", re.DOTALL)
35
+ _INLINE_CODE_RE = re.compile(r"`[^`\n]+`")
36
+ _HTML_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
37
+ _RELATED_HEADING_RE = re.compile(r"(?m)^##\s+(?:🔗\s+)?Notas Relacionadas\s*$")
38
+ _NEXT_H2_RE = re.compile(r"(?m)^##\s+")
39
+ _FOOTER_RE = re.compile(r"(?m)^---\s*$")
40
+ _HEADING_LINE_RE = re.compile(r"(?m)^#{1,6}\s+.*$")
41
+ _TABLE_LINE_RE = re.compile(r"(?m)^\|.*\|$")
42
+
43
+
44
+ def _as_str(value: object) -> str:
45
+ return value if isinstance(value, str) else ""
46
+
47
+
48
+ def _as_int(value: object) -> int | None:
49
+ if isinstance(value, int) and not isinstance(value, bool):
50
+ return value
51
+ return None
52
+
53
+
54
+ def _as_str_list(value: object) -> list[str]:
55
+ if not isinstance(value, list):
56
+ return []
57
+ return [item for item in value if isinstance(item, str)]
58
+
59
+
60
+ class _ReferenceRepairLooseInput(ContractModel):
61
+ """Typed adapter for legacy graph-audit dicts consumed by reference repair."""
62
+
63
+ model_config = ConfigDict(extra="ignore", populate_by_name=True, validate_assignment=True)
64
+
65
+
66
+ class _GraphIssueInput(_ReferenceRepairLooseInput):
67
+ code: str = ""
68
+ target: str = ""
69
+ raw: str = ""
70
+ file: str = ""
71
+ alias: str = ""
72
+ message: str = ""
73
+ line: int | None = None
74
+ files: list[str] = Field(default_factory=list)
75
+ targets: list[str] = Field(default_factory=list)
76
+
77
+ @field_validator("code", "target", "raw", "file", "alias", "message", mode="before")
78
+ @classmethod
79
+ def _coerce_text(cls, value: object) -> str:
80
+ return _as_str(value)
81
+
82
+ @field_validator("line", mode="before")
83
+ @classmethod
84
+ def _coerce_line(cls, value: object) -> int | None:
85
+ return _as_int(value)
86
+
87
+ @field_validator("files", "targets", mode="before")
88
+ @classmethod
89
+ def _coerce_text_list(cls, value: object) -> list[str]:
90
+ return _as_str_list(value)
91
+
92
+
93
+ class _StructuralEventInput(_ReferenceRepairLooseInput):
94
+ change_type: str = ""
95
+ old_title: str = ""
96
+ old_path: str = ""
97
+ replacement_title: str = ""
98
+ replacement_path: str = ""
99
+ title: str = ""
100
+ path: str = ""
101
+
102
+ @field_validator(
103
+ "change_type",
104
+ "old_title",
105
+ "old_path",
106
+ "replacement_title",
107
+ "replacement_path",
108
+ "title",
109
+ "path",
110
+ mode="before",
111
+ )
112
+ @classmethod
113
+ def _coerce_text(cls, value: object) -> str:
114
+ return _as_str(value)
115
+
116
+
117
+ class _ReferenceRepairAction(ContractModel):
118
+ """Single planned action; only this model may drive repair decisions."""
119
+
120
+ code: str = ""
121
+ action: str = ""
122
+ target: str = ""
123
+ raw: str = ""
124
+ reason: str = ""
125
+ line: int | None = None
126
+ old_target: str = ""
127
+ new_target: str = ""
128
+ replacement: str | None = None
129
+ change_type: str = ""
130
+ alias: str = ""
131
+ targets: list[str] = Field(default_factory=list)
132
+ message: str = ""
133
+ candidate_files: list[str] = Field(default_factory=list)
134
+ recommended_options: list[str] = Field(default_factory=list)
135
+ blocks_apply: bool = False
136
+ safe_auto_apply: bool = False
137
+ human_decision_required: bool = False
138
+ human_triage_required: bool = False
139
+ operational_blocker: bool = False
140
+ receipt_code: str = ""
141
+ visible_text_policy: str = ""
142
+ decision_summary: WorkflowDecisionSummary | None = None
143
+
144
+ def compact_payload(self) -> JsonObject:
145
+ """Serialize the action while preserving explicit null replacement values."""
146
+
147
+ payload = self.to_payload()
148
+ return {key: value for key, value in payload.items() if value not in ("", [])}
149
+
150
+
151
+ class _ReferenceRepairNoteActions(ContractModel):
152
+ path: str
153
+ action_count: int
154
+ blocking_action_count: int
155
+ human_decision_required: bool
156
+ blocks_apply: bool
157
+ actions: list[_ReferenceRepairAction]
158
+
159
+ @classmethod
160
+ def from_actions(cls, *, path: str, actions: list[_ReferenceRepairAction]) -> _ReferenceRepairNoteActions:
161
+ return cls(
162
+ path=path,
163
+ action_count=len(actions),
164
+ blocking_action_count=sum(1 for action in actions if action.blocks_apply),
165
+ human_decision_required=any(action.human_decision_required for action in actions),
166
+ blocks_apply=any(action.blocks_apply for action in actions),
167
+ actions=actions,
168
+ )
169
+
170
+ def compact_payload(self) -> JsonObject:
171
+ return {
172
+ "path": self.path,
173
+ "action_count": self.action_count,
174
+ "blocking_action_count": self.blocking_action_count,
175
+ "human_decision_required": self.human_decision_required,
176
+ "blocks_apply": self.blocks_apply,
177
+ "actions": [action.compact_payload() for action in self.actions],
178
+ }
179
+
180
+
181
+ class _ReferenceRepairPlan(ContractModel):
182
+ schema_id: Literal["medical-notes-workbench.reference-repair-plan.v1"] = Field(
183
+ default=REFERENCE_REPAIR_PLAN_SCHEMA,
184
+ alias="schema",
185
+ )
186
+ phase: Literal["reference_repair"] = "reference_repair"
187
+ status: str = "skipped"
188
+ package_mode: str = "diagnosis_bound"
189
+ manual_script_allowed: bool = False
190
+ requires_backup: bool = False
191
+ requires_receipt: bool = True
192
+ action_count: int = 0
193
+ affected_note_count: int = 0
194
+ blocking_action_count: int = 0
195
+ human_decision_count: int = 0
196
+ triage_count: int = 0
197
+ human_decision_required: bool = False
198
+ triage_required: bool = False
199
+ note_actions: list[_ReferenceRepairNoteActions] = Field(default_factory=list)
200
+ structural_actions: list[_ReferenceRepairAction] = Field(default_factory=list)
201
+ catalog_actions: list[_ReferenceRepairAction] = Field(default_factory=list)
202
+ human_decision_packets: list[HumanDecisionPacket] = Field(default_factory=list)
203
+
204
+ def compact_payload(self) -> JsonObject:
205
+ payload = self.to_payload()
206
+ payload["note_actions"] = [item.compact_payload() for item in self.note_actions]
207
+ payload["structural_actions"] = [item.compact_payload() for item in self.structural_actions]
208
+ payload["catalog_actions"] = [item.compact_payload() for item in self.catalog_actions]
209
+ payload["human_decision_packets"] = [item.to_payload() for item in self.human_decision_packets]
210
+ return payload
211
+
212
+
213
+ class _ReferenceRepairAppliedAction(ContractModel):
214
+ action: str
215
+ old_target: str
216
+ new_target: str | None = None
217
+ receipt_code: str
218
+
219
+
220
+ class _ReferenceRepairApplyReport(ContractModel):
221
+ path: str
222
+ changed: bool
223
+ error: str = ""
224
+ actions: list[_ReferenceRepairAppliedAction] = Field(default_factory=list)
225
+
226
+ def compact_payload(self) -> JsonObject:
227
+ payload = self.to_payload()
228
+ return {key: value for key, value in payload.items() if value not in ("", [])}
229
+
230
+
231
+ class _ReferenceRepairApplyResult(ContractModel):
232
+ schema_id: Literal["medical-notes-workbench.reference-repair-apply.v1"] = Field(
233
+ default="medical-notes-workbench.reference-repair-apply.v1",
234
+ alias="schema",
235
+ )
236
+ phase: Literal["reference_repair"] = "reference_repair"
237
+ status: Literal["completed"] = "completed"
238
+ changed_file_count: int
239
+ changed_files: list[str]
240
+ backup_paths: list[str] = Field(default_factory=list)
241
+ reports: list[_ReferenceRepairApplyReport]
242
+
243
+ def compact_payload(self) -> JsonObject:
244
+ payload = self.to_payload()
245
+ payload["reports"] = [report.compact_payload() for report in self.reports]
246
+ return payload
247
+
248
+
249
+ def _target_from_path(value: str) -> str:
250
+ if not value:
251
+ return ""
252
+ return obsidian_target_name(value)
253
+
254
+
255
+ def _old_target_from_event(event: _StructuralEventInput) -> str:
256
+ return event.old_title or _target_from_path(event.old_path)
257
+
258
+
259
+ def _new_target_from_event(event: _StructuralEventInput) -> str:
260
+ return event.replacement_title or _target_from_path(event.replacement_path) or event.title or _target_from_path(event.path)
261
+
262
+
263
+ def _event_replacements(events: list[_StructuralEventInput]) -> dict[str, str | None]:
264
+ replacements: dict[str, str | None] = {}
265
+ for event in events:
266
+ change_type = event.change_type
267
+ old_target = _old_target_from_event(event)
268
+ if not old_target:
269
+ continue
270
+ new_target = _new_target_from_event(event)
271
+ if change_type in {"renamed", "moved", "merged"}:
272
+ if new_target:
273
+ replacements[normalize_key(old_target)] = new_target
274
+ elif change_type == "deleted":
275
+ replacements[normalize_key(old_target)] = new_target or None
276
+ return replacements
277
+
278
+
279
+ def _packet_id(action: _ReferenceRepairAction, path: str = "") -> str:
280
+ line = "" if action.line is None else str(action.line)
281
+ parts = [
282
+ "reference_repair",
283
+ action.code,
284
+ path,
285
+ line,
286
+ action.target,
287
+ ]
288
+ return ":".join(part.replace(":", "_") for part in parts if part)
289
+
290
+
291
+ def _requires_reference_decision() -> bool:
292
+ return True
293
+
294
+
295
+ def _decision_packet(action: _ReferenceRepairAction, *, path: str = "") -> HumanDecisionPacket | None:
296
+ if not action.human_decision_required:
297
+ return None
298
+ code = action.code
299
+ target = action.target
300
+ if code == "dangling_link":
301
+ options = [
302
+ HumanDecisionOption(id="replace_target", label="Apontar para nota existente"),
303
+ HumanDecisionOption(id="remove_link", label="Remover link mantendo texto"),
304
+ HumanDecisionOption(id="create_missing_note", label="Criar nota faltante"),
305
+ ]
306
+ question = f"O alvo [[{target}]] nao existe. Como reparar este link?"
307
+ elif code == "ambiguous_link":
308
+ options = [HumanDecisionOption(id=f"use:{candidate}", label=candidate) for candidate in action.candidate_files]
309
+ options.append(HumanDecisionOption(id="remove_link", label="Remover link mantendo texto"))
310
+ question = f"O alvo [[{target}]] aponta para mais de uma nota. Qual e a nota canonica?"
311
+ elif code == "duplicate_stem":
312
+ options = [
313
+ HumanDecisionOption(id="choose_canonical", label="Escolher nota canonica"),
314
+ HumanDecisionOption(id="merge_duplicates", label="Mesclar duplicatas"),
315
+ HumanDecisionOption(id="rename_duplicates", label="Renomear duplicatas"),
316
+ ]
317
+ question = f"Ha multiplas notas com o alvo Obsidian [[{target}]]. Como consolidar?"
318
+ elif code == "structural_deleted":
319
+ options = [
320
+ HumanDecisionOption(id="remove_link_keep_text", label="Remover link mantendo texto"),
321
+ HumanDecisionOption(id="choose_replacement", label="Escolher nota substituta"),
322
+ HumanDecisionOption(id="skip_file", label="Pular este arquivo"),
323
+ ]
324
+ question = f"A nota [[{target}]] foi removida sem substituto confirmado. Como reparar as referencias?"
325
+ else:
326
+ options = [
327
+ HumanDecisionOption(id="edit_catalog", label="Corrigir catalogo"),
328
+ HumanDecisionOption(id="skip_for_now", label="Manter como blocker"),
329
+ ]
330
+ question = f"Como resolver o blocker de grafo {code}?"
331
+
332
+ resume_action = "Atualizar o vault/catalogo e rodar run-linker --diagnose novamente."
333
+ decision = WorkflowDecision(
334
+ kind="ask_human",
335
+ phase="reference_repair",
336
+ reason_code=code or "reference_repair_required",
337
+ public_summary=question,
338
+ developer_summary="Reference repair has no safe deterministic target for this action.",
339
+ evidence=[
340
+ DecisionEvidence(
341
+ summary=question,
342
+ technical_code=code or "reference_repair_required",
343
+ source="reference_repair",
344
+ affected_items=[item for item in (path, target) if item],
345
+ candidates=[{"target": target, "path": path, "line": "" if action.line is None else str(action.line)}],
346
+ risk="Escolha automatica pode apagar ou redirecionar referencia para alvo incorreto.",
347
+ )
348
+ ],
349
+ rejected_automations=[
350
+ RejectedAutomation(kind="auto_fix", reason_code="no_unambiguous_target", reason="Nao ha substituto unico seguro para corrigir automaticamente."),
351
+ RejectedAutomation(kind="auto_defer", reason_code="blocks_reference_repair", reason="Pular esta acao deixa blocker operacional ativo."),
352
+ RejectedAutomation(kind="auto_plan", reason_code="plan_needs_target", reason="O plano precisa da escolha de alvo/rota antes de aplicar."),
353
+ ],
354
+ next_action=resume_action,
355
+ resume_action=resume_action,
356
+ recommended_option_id=options[0].id if options else "manual_review",
357
+ options=options or [HumanDecisionOption(id="manual_review", label="Revisar manualmente")],
358
+ )
359
+ packet = HumanDecisionPacket.model_validate(decision.to_human_decision_packet())
360
+ packet.id = _packet_id(action, path=path)
361
+ packet.kind = "reference_repair"
362
+ packet.type = "reference_repair"
363
+ packet.path = path
364
+ packet.target = target
365
+ packet.line = action.line
366
+ action.decision_summary = packet.decision_summary
367
+ return packet
368
+
369
+
370
+ def _auto_link_action(
371
+ issue: _GraphIssueInput,
372
+ *,
373
+ duplicate_candidates: dict[str, list[str]],
374
+ replacements: dict[str, str | None],
375
+ ) -> _ReferenceRepairAction | None:
376
+ code = issue.code
377
+ target = issue.target
378
+ raw = issue.raw
379
+ line = issue.line
380
+ if code not in _GRAPH_LINK_CODES or not target:
381
+ return None
382
+ target_key = normalize_key(target)
383
+ has_structural_event = target_key in replacements
384
+ replacement = replacements[target_key] if has_structural_event else None
385
+ if code == "dangling_link" and has_structural_event and replacement is None:
386
+ return _ReferenceRepairAction(
387
+ code="structural_deleted",
388
+ action="resolve_deleted_wikilink_target",
389
+ target=target,
390
+ old_target=target,
391
+ raw=raw,
392
+ reason="structural_deleted_without_replacement",
393
+ replacement=None,
394
+ blocks_apply=True,
395
+ safe_auto_apply=False,
396
+ human_decision_required=_requires_reference_decision(),
397
+ recommended_options=["remove_link_keep_text", "choose_replacement", "skip_file"],
398
+ line=line,
399
+ )
400
+ if code == "self_link":
401
+ action = "unlink_incoming_wikilink"
402
+ receipt_code = "unlinked_self_link"
403
+ reason = "self_link"
404
+ elif replacement:
405
+ action = "rewrite_incoming_wikilink"
406
+ receipt_code = "rewritten_ambiguous_target" if code == "ambiguous_link" else "rewritten_missing_target"
407
+ reason = "structural_replacement"
408
+ else:
409
+ action = "unlink_incoming_wikilink"
410
+ if code == "ambiguous_link":
411
+ receipt_code = "unlinked_ambiguous_target"
412
+ reason = "ambiguous_without_replacement"
413
+ else:
414
+ receipt_code = "unlinked_deleted_target"
415
+ reason = "missing_without_replacement"
416
+ candidate_files = duplicate_candidates[normalize_key(target)] if code == "ambiguous_link" and normalize_key(target) in duplicate_candidates else []
417
+ return _ReferenceRepairAction(
418
+ code=code,
419
+ action=action,
420
+ target=target,
421
+ old_target=target,
422
+ raw=raw,
423
+ reason=reason,
424
+ blocks_apply=False,
425
+ safe_auto_apply=True,
426
+ human_decision_required=False,
427
+ receipt_code=receipt_code,
428
+ visible_text_policy="preserve_label_or_target",
429
+ new_target=replacement or "",
430
+ replacement=replacement,
431
+ line=line,
432
+ candidate_files=candidate_files,
433
+ )
434
+
435
+
436
+ def _catalog_action(issue: _GraphIssueInput) -> _ReferenceRepairAction:
437
+ code = issue.code
438
+ target = issue.target
439
+ blocks_apply = code in _CATALOG_OPERATIONAL_BLOCKERS
440
+ return _ReferenceRepairAction(
441
+ code=code,
442
+ action="operational_catalog_blocker" if blocks_apply else "skip_catalog_entry_for_linking",
443
+ target=target,
444
+ alias=issue.alias,
445
+ targets=issue.targets,
446
+ message=issue.message,
447
+ blocks_apply=blocks_apply,
448
+ safe_auto_apply=False,
449
+ human_decision_required=False,
450
+ human_triage_required=True,
451
+ operational_blocker=blocks_apply,
452
+ receipt_code=_catalog_receipt_code(code),
453
+ )
454
+
455
+
456
+ def _catalog_receipt_code(code: str) -> str:
457
+ match code:
458
+ case "catalog_missing":
459
+ return "catalog_skipped_missing"
460
+ case "catalog_invalid_json":
461
+ return "operational_blocker.catalog_invalid_json"
462
+ case "catalog_entry_missing_target":
463
+ return "catalog_entry_skipped_missing_target"
464
+ case "catalog_target_missing":
465
+ return "catalog_entry_skipped_target_missing"
466
+ case "catalog_target_ambiguous":
467
+ return "catalog_entry_skipped_target_ambiguous"
468
+ case "alias_conflict":
469
+ return "catalog_alias_skipped_conflict"
470
+ case "generic_alias" | "short_alias":
471
+ return "catalog_alias_skipped_low_signal"
472
+ case "":
473
+ return "catalog_skipped_unknown"
474
+ case _:
475
+ return f"catalog_skipped_{code}"
476
+
477
+
478
+ def _structural_action_from_event(event: _StructuralEventInput) -> _ReferenceRepairAction | None:
479
+ old_target = _old_target_from_event(event)
480
+ if not old_target:
481
+ return None
482
+ change_type = event.change_type
483
+ new_target = _new_target_from_event(event)
484
+ return _ReferenceRepairAction(
485
+ code=f"structural_{change_type}",
486
+ action="track_structural_link_event",
487
+ change_type=change_type,
488
+ old_target=old_target,
489
+ target=old_target,
490
+ new_target=new_target,
491
+ replacement=new_target or None,
492
+ blocks_apply=False,
493
+ safe_auto_apply=True,
494
+ human_decision_required=False,
495
+ )
496
+
497
+
498
+ def plan_reference_repair(
499
+ graph_issues: Sequence[object],
500
+ *,
501
+ structural_events: Sequence[object] | None = None,
502
+ ) -> JsonObject:
503
+ """Build a deterministic reference-repair plan from graph-audit issues."""
504
+ issues = [_GraphIssueInput.model_validate(issue) for issue in graph_issues]
505
+ events = [_StructuralEventInput.model_validate(event) for event in structural_events or []]
506
+ replacements = _event_replacements(events)
507
+ duplicate_candidates: dict[str, list[str]] = {}
508
+ for issue in issues:
509
+ if issue.code == "duplicate_stem":
510
+ duplicate_candidates[normalize_key(issue.target)] = issue.files
511
+
512
+ by_note: dict[str, list[_ReferenceRepairAction]] = {}
513
+ structural_actions: list[_ReferenceRepairAction] = []
514
+ catalog_actions: list[_ReferenceRepairAction] = []
515
+ human_decision_packets: list[HumanDecisionPacket] = []
516
+
517
+ for issue in issues:
518
+ code = issue.code
519
+ if code == "duplicate_stem":
520
+ structural_actions.append(
521
+ _ReferenceRepairAction(
522
+ code=code,
523
+ action="resolve_duplicate_obsidian_target",
524
+ target=issue.target,
525
+ candidate_files=issue.files,
526
+ blocks_apply=True,
527
+ safe_auto_apply=False,
528
+ human_decision_required=False,
529
+ recommended_options=["choose_canonical", "merge_duplicates", "rename_duplicates"],
530
+ )
531
+ )
532
+ continue
533
+
534
+ if code in {"dangling_link", "ambiguous_link", "self_link"}:
535
+ path = issue.file
536
+ if not path:
537
+ continue
538
+ action = _auto_link_action(issue, duplicate_candidates=duplicate_candidates, replacements=replacements)
539
+ if action is None:
540
+ continue
541
+ by_note.setdefault(path, []).append(action)
542
+ continue
543
+
544
+ if code.startswith("catalog_") or code == "alias_conflict":
545
+ action = _catalog_action(issue)
546
+ catalog_actions.append(action)
547
+ packet = _decision_packet(action)
548
+ if packet:
549
+ human_decision_packets.append(packet)
550
+
551
+ for event in events:
552
+ action = _structural_action_from_event(event)
553
+ if action:
554
+ structural_actions.append(action)
555
+
556
+ note_actions: list[_ReferenceRepairNoteActions] = []
557
+ for path, actions in sorted(by_note.items()):
558
+ sorted_actions = sorted(actions, key=lambda action: (action.line or 0, action.code))
559
+ for action in sorted_actions:
560
+ packet = _decision_packet(action, path=path)
561
+ if packet:
562
+ human_decision_packets.append(packet)
563
+ note_actions.append(_ReferenceRepairNoteActions.from_actions(path=path, actions=sorted_actions))
564
+
565
+ note_action_count = sum(item.action_count for item in note_actions)
566
+ catalog_action_count = len(catalog_actions)
567
+ blocking_action_count = sum(item.blocking_action_count for item in note_actions) + sum(
568
+ 1 for action in catalog_actions if action.blocks_apply
569
+ )
570
+ human_decision_count = sum(
571
+ 1
572
+ for item in note_actions
573
+ for action in item.actions
574
+ if action.human_decision_required
575
+ ) + sum(1 for action in catalog_actions if action.human_decision_required)
576
+ triage_count = sum(1 for action in catalog_actions if action.human_triage_required)
577
+
578
+ status = "blocked" if blocking_action_count else "planned" if note_action_count or catalog_action_count else "skipped"
579
+ plan = _ReferenceRepairPlan(
580
+ status=status,
581
+ action_count=note_action_count + catalog_action_count,
582
+ affected_note_count=len(note_actions),
583
+ blocking_action_count=blocking_action_count,
584
+ human_decision_count=human_decision_count,
585
+ triage_count=triage_count,
586
+ human_decision_required=human_decision_count > 0,
587
+ triage_required=triage_count > 0,
588
+ note_actions=note_actions,
589
+ structural_actions=sorted(structural_actions, key=lambda action: (action.code, action.target)),
590
+ catalog_actions=sorted(catalog_actions, key=lambda action: (action.code, action.target)),
591
+ human_decision_packets=human_decision_packets,
592
+ )
593
+ return plan.compact_payload()
594
+
595
+
596
+ def _raw_target(raw: str) -> str:
597
+ target = raw.split("|", 1)[0].split("#", 1)[0].strip()
598
+ return obsidian_target_name(target)
599
+
600
+
601
+ def _display_text(raw: str) -> str:
602
+ if "|" in raw:
603
+ return raw.rsplit("|", 1)[1].strip()
604
+ target = raw.split("#", 1)[0].strip()
605
+ return obsidian_target_name(target) if target else raw.strip()
606
+
607
+
608
+ def _rewrite_link(raw: str, new_target: str) -> str:
609
+ if "|" in raw:
610
+ display = raw.rsplit("|", 1)[1].strip()
611
+ return f"{new_target}|{display}" if display else new_target
612
+ return new_target
613
+
614
+
615
+ def _related_section_spans(text: str) -> list[tuple[int, int]]:
616
+ spans: list[tuple[int, int]] = []
617
+ for match in _RELATED_HEADING_RE.finditer(text):
618
+ next_h2 = _NEXT_H2_RE.search(text, match.end())
619
+ footer = _footer_match(text, match.end())
620
+ candidates = [item.start() for item in (next_h2, footer) if item is not None]
621
+ spans.append((match.start(), min(candidates) if candidates else len(text)))
622
+ return spans
623
+
624
+
625
+ def _footer_match(text: str, start: int = 0) -> re.Match[str] | None:
626
+ frontmatter = _FRONTMATTER_RE.match(text)
627
+ search_start = max(start, frontmatter.end() if frontmatter else 0)
628
+ return _FOOTER_RE.search(text, search_start)
629
+
630
+
631
+ def _protected_spans(text: str) -> list[tuple[int, int]]:
632
+ spans: list[tuple[int, int]] = []
633
+ for regex in (_FRONTMATTER_RE, _FENCED_RE, _INLINE_CODE_RE, _HTML_COMMENT_RE, _HEADING_LINE_RE, _TABLE_LINE_RE):
634
+ spans.extend((match.start(), match.end()) for match in regex.finditer(text))
635
+ footer = _footer_match(text)
636
+ if footer:
637
+ spans.append((footer.start(), len(text)))
638
+ spans.extend(_related_section_spans(text))
639
+ return sorted(spans)
640
+
641
+
642
+ def _inside(index: int, spans: list[tuple[int, int]]) -> bool:
643
+ return any(start <= index < end for start, end in spans)
644
+
645
+
646
+ def _apply_actions_to_text(text: str, actions: list[_ReferenceRepairAction]) -> tuple[str, list[_ReferenceRepairAppliedAction]]:
647
+ if not actions:
648
+ return text, []
649
+ protected = _protected_spans(text)
650
+ changed: list[_ReferenceRepairAppliedAction] = []
651
+ output: list[str] = []
652
+ cursor = 0
653
+ for match in _WIKILINK_RE.finditer(text):
654
+ if _inside(match.start(), protected):
655
+ continue
656
+ raw = match.group(2).strip()
657
+ target = _raw_target(raw)
658
+ action = next(
659
+ (
660
+ item
661
+ for item in actions
662
+ if item.safe_auto_apply and normalize_key(item.old_target or item.target) == normalize_key(target)
663
+ ),
664
+ None,
665
+ )
666
+ if action is None:
667
+ continue
668
+ if action.action == "rewrite_incoming_wikilink" and action.new_target:
669
+ replacement_raw = _rewrite_link(raw, action.new_target)
670
+ replacement = f"{match.group(1)}[[{replacement_raw}]]"
671
+ elif action.action == "unlink_incoming_wikilink":
672
+ replacement = _display_text(raw)
673
+ else:
674
+ continue
675
+ output.append(text[cursor : match.start()])
676
+ output.append(replacement)
677
+ cursor = match.end()
678
+ changed.append(
679
+ _ReferenceRepairAppliedAction(
680
+ action=action.action,
681
+ old_target=target,
682
+ new_target=action.new_target or None,
683
+ receipt_code=action.receipt_code,
684
+ )
685
+ )
686
+ if not changed:
687
+ return text, []
688
+ output.append(text[cursor:])
689
+ return "".join(output), changed
690
+
691
+
692
+ def apply_reference_repair_plan(wiki_dir: Path, plan: object) -> JsonObject:
693
+ """Apply safe automatic reference-repair actions outside protected zones."""
694
+ typed_plan = _ReferenceRepairPlan.model_validate(plan)
695
+ changed_files: list[str] = []
696
+ reports: list[_ReferenceRepairApplyReport] = []
697
+ for note in typed_plan.note_actions:
698
+ relative = note.path
699
+ if not relative or not note.actions:
700
+ continue
701
+ path = wiki_dir / relative
702
+ if not path.is_file():
703
+ reports.append(_ReferenceRepairApplyReport(path=relative, changed=False, error="file_missing"))
704
+ continue
705
+ original = path.read_text(encoding="utf-8")
706
+ updated, applied = _apply_actions_to_text(original, note.actions)
707
+ if updated == original:
708
+ reports.append(_ReferenceRepairApplyReport(path=relative, changed=False, actions=[]))
709
+ continue
710
+ atomic_write_text(path, updated)
711
+ changed_files.append(str(path))
712
+ reports.append(_ReferenceRepairApplyReport(path=relative, changed=True, actions=applied))
713
+ result = _ReferenceRepairApplyResult(
714
+ changed_file_count=len(changed_files),
715
+ changed_files=changed_files,
716
+ reports=reports,
717
+ )
718
+ return result.compact_payload()