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,1952 @@
1
+ """Wiki_Medicina style validation and deterministic fixes."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import hmac
6
+ import json
7
+ import os
8
+ import re
9
+ import secrets
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from pydantic import ConfigDict
15
+ from pydantic import ValidationError as PydanticValidationError
16
+
17
+ from mednotes.domains.wiki.capabilities.notes import note_style
18
+ from mednotes.domains.wiki.capabilities.notes.note_iter import iter_notes
19
+ from mednotes.domains.wiki.capabilities.notes.raw_chats import atomic_write_text, read_note_meta
20
+ from mednotes.domains.wiki.capabilities.specialist.plan_attestation import (
21
+ subagent_plan_attestation_blocked_reason,
22
+ subagent_plan_hash,
23
+ validate_subagent_plan_attestation,
24
+ )
25
+ from mednotes.domains.wiki.capabilities.specialist.specialist_receipts import (
26
+ validate_specialist_task_run_receipt_attestation,
27
+ )
28
+ from mednotes.domains.wiki.capabilities.specialist.specialist_runtime import (
29
+ specialist_dev_escape_enabled,
30
+ transcript_command_untrusted_gemini_binary,
31
+ )
32
+ from mednotes.domains.wiki.capabilities.vocabulary.link_terms import is_index_note_content, is_index_target
33
+ from mednotes.domains.wiki.common import FileWriteError, MissingPathError, ValidationError
34
+ from mednotes.domains.wiki.contracts.agents import SubagentBatchPlan
35
+ from mednotes.domains.wiki.contracts.specialist import SpecialistTaskRunReceipt
36
+ from mednotes.domains.wiki.contracts.style_rewrite import (
37
+ FixWikiStyleResult,
38
+ StyleRewriteApplyReceipt,
39
+ StyleRewriteAtomicApplyResult,
40
+ StyleRewriteManifest,
41
+ StyleRewriteOutputAttestation,
42
+ StyleRewriteOutputCollection,
43
+ StyleRewriteOutputFinalization,
44
+ StyleRewriteOutputReceipt,
45
+ )
46
+ from mednotes.domains.wiki.contracts.workflow_guardrails import error_context
47
+ from mednotes.domains.wiki.performance import cooperative_cpu_yield
48
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonValue, contract_error
49
+
50
+ STYLE_REWRITE_MANIFEST_SCHEMA = "medical-notes-workbench.style-rewrite-output-manifest.v1"
51
+ STYLE_REWRITE_APPLY_RECEIPT_SCHEMA = "medical-notes-workbench.style-rewrite-apply-receipt.v1"
52
+ STYLE_REWRITE_OUTPUT_RECEIPT_SCHEMA = "medical-notes-workbench.style-rewrite-output.v1"
53
+ STYLE_REWRITE_OUTPUT_ATTESTATION_SCHEMA = "medical-notes-workbench.style-rewrite-output-attestation.v1"
54
+ STYLE_REWRITE_OUTPUT_FINALIZATION_SCHEMA = "medical-notes-workbench.style-rewrite-output-finalization.v1"
55
+ STYLE_REWRITE_ATOMIC_APPLY_RESULT_SCHEMA = "medical-notes-workbench.style-rewrite-atomic-apply-result.v1"
56
+ STYLE_REWRITE_ATTESTATION_KIND = "workbench_hmac_sha256.v1"
57
+ RELATED_NOTES_HEADING_RE = re.compile(r"(?m)^##\s+(?:🔗\s+)?Notas Relacionadas\s*$")
58
+ H2_HEADING_RE = re.compile(r"(?m)^##\s+")
59
+ FOOTER_RULE_RE = re.compile(r"(?m)^---\s*$")
60
+
61
+
62
+ class _StyleRewriteWorkItemLens(ContractModel):
63
+ """Typed view over one style-rewrite work item from the attested plan."""
64
+
65
+ model_config = ConfigDict(extra="ignore", populate_by_name=True, validate_assignment=True)
66
+
67
+ work_id: str = ""
68
+ target_path: str = ""
69
+ target_hash_before: str = ""
70
+ temp_output: str = ""
71
+ output_path: str = ""
72
+ output_receipt_path: str = ""
73
+ output_attestation_path: str = ""
74
+ agent: str = ""
75
+ model_policy: str = ""
76
+ required_model_tier: str = ""
77
+
78
+ @property
79
+ def planned_output_path(self) -> str:
80
+ return self.temp_output.strip() or self.output_path.strip()
81
+
82
+ @property
83
+ def agent_or_default(self) -> str:
84
+ return self.agent.strip() or "med-knowledge-architect"
85
+
86
+ @property
87
+ def model_policy_or_default(self) -> str:
88
+ return self.model_policy.strip() or "medical_specialist_authoring.v1"
89
+
90
+ @property
91
+ def required_model_tier_or_default(self) -> str:
92
+ return self.required_model_tier.strip() or "specialist"
93
+
94
+
95
+ def _style_rewrite_work_item_lens(raw_item: JsonObject) -> _StyleRewriteWorkItemLens:
96
+ """Normalize raw plan JSON once before style-rewrite logic reads fields."""
97
+
98
+ return _StyleRewriteWorkItemLens.model_validate(raw_item)
99
+
100
+
101
+ def _optional_path_text(path: Path | None) -> str:
102
+ if path is None:
103
+ return ""
104
+ return str(path)
105
+
106
+
107
+ def _sha256_bytes(data: bytes) -> str:
108
+ return "sha256:" + hashlib.sha256(data).hexdigest()
109
+
110
+
111
+ def _canonical_payload_hash(payload: dict[str, Any]) -> str:
112
+ encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
113
+ return _sha256_bytes(encoded)
114
+
115
+
116
+ def _read_json_object(path: Path, *, label: str) -> dict[str, Any]:
117
+ if not path.exists():
118
+ raise MissingPathError(f"{label} not found: {path}")
119
+ try:
120
+ payload = json.loads(path.read_text(encoding="utf-8-sig"))
121
+ except json.JSONDecodeError as exc:
122
+ raise ValidationError(f"{label} is invalid JSON: {path}: {exc}") from exc
123
+ if not isinstance(payload, dict):
124
+ raise ValidationError(f"{label} must be a JSON object: {path}")
125
+ return payload
126
+
127
+
128
+ def _json_field(source: JsonObject, key: str, default: JsonValue = None) -> JsonValue:
129
+ return source.get(key, default)
130
+
131
+
132
+ def _validate_style_rewrite_manifest(payload: dict[str, Any]) -> StyleRewriteManifest:
133
+ try:
134
+ return StyleRewriteManifest.model_validate(payload)
135
+ except PydanticValidationError as exc:
136
+ raise contract_error(exc, prefix="style_rewrite_manifest_invalid") from exc
137
+
138
+
139
+ def _validate_style_rewrite_output_receipt(payload: JsonObject) -> StyleRewriteOutputReceipt:
140
+ try:
141
+ return StyleRewriteOutputReceipt.model_validate(payload)
142
+ except PydanticValidationError as exc:
143
+ raise contract_error(exc, prefix="style_rewrite_output_receipt_invalid") from exc
144
+
145
+
146
+ def _validate_style_rewrite_output_attestation(payload: dict[str, Any]) -> StyleRewriteOutputAttestation:
147
+ try:
148
+ return StyleRewriteOutputAttestation.model_validate(payload)
149
+ except PydanticValidationError as exc:
150
+ raise contract_error(exc, prefix="style_rewrite_output_attestation_invalid") from exc
151
+
152
+
153
+ def _validate_style_rewrite_plan(payload: dict[str, Any]) -> SubagentBatchPlan:
154
+ try:
155
+ plan = SubagentBatchPlan.model_validate(payload)
156
+ except PydanticValidationError as exc:
157
+ raise contract_error(exc, prefix="style_rewrite_plan_contract_invalid") from exc
158
+ if plan.phase != "style-rewrite":
159
+ raise ValidationError("style_rewrite_plan_contract_invalid: phase must be style-rewrite")
160
+ return plan
161
+
162
+
163
+ def _verify_style_rewrite_plan_attestation(payload: dict[str, Any]) -> str:
164
+ return validate_subagent_plan_attestation(payload)
165
+
166
+
167
+ def _plan_attestation_next_action(blocked_reason: str) -> str:
168
+ if blocked_reason == "subagent_plan_attestation_required":
169
+ return (
170
+ "Regere o plano pela rota oficial plan-subagents; plano JSON copiado, escrito ou editado pelo agente "
171
+ "não pode ser usado para finalizar, coletar ou aplicar outputs."
172
+ )
173
+ return "Regere o plano pela rota oficial plan-subagents; a assinatura/hash do plano não confere."
174
+
175
+
176
+ def _style_rewrite_attestation_key_path() -> Path:
177
+ configured = os.getenv("MEDNOTES_STYLE_REWRITE_ATTESTATION_KEY_PATH", "").strip()
178
+ if configured:
179
+ return Path(configured).expanduser()
180
+ return Path.home() / ".gemini" / "medical-notes-workbench" / "style-rewrite-attestation.key"
181
+
182
+
183
+ def _style_rewrite_attestation_key(*, create: bool) -> bytes:
184
+ configured = os.getenv("MEDNOTES_STYLE_REWRITE_ATTESTATION_KEY", "").strip()
185
+ if configured:
186
+ return configured.encode("utf-8")
187
+ key_path = _style_rewrite_attestation_key_path()
188
+ if key_path.exists():
189
+ return key_path.read_bytes().strip()
190
+ if not create:
191
+ raise MissingPathError(f"style rewrite attestation key not found: {key_path}")
192
+ key_path.parent.mkdir(parents=True, exist_ok=True)
193
+ key = secrets.token_hex(32).encode("ascii")
194
+ tmp_path = key_path.with_name(f"{key_path.name}.tmp")
195
+ tmp_path.write_bytes(key + b"\n")
196
+ try:
197
+ os.chmod(tmp_path, 0o600)
198
+ except OSError:
199
+ pass
200
+ os.replace(tmp_path, key_path)
201
+ return key
202
+
203
+
204
+ def _style_rewrite_attestation_signing_payload(payload: dict[str, Any]) -> bytes:
205
+ unsigned = {key: value for key, value in payload.items() if key != "signature"}
206
+ return json.dumps(unsigned, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
207
+
208
+
209
+ def _style_rewrite_attestation_signature(payload: dict[str, Any], *, create_key: bool) -> str:
210
+ digest = hmac.new(
211
+ _style_rewrite_attestation_key(create=create_key),
212
+ _style_rewrite_attestation_signing_payload(payload),
213
+ hashlib.sha256,
214
+ ).hexdigest()
215
+ return f"hmac-sha256:{digest}"
216
+
217
+
218
+ def _style_rewrite_verify_attestation_signature(payload: dict[str, Any]) -> bool:
219
+ try:
220
+ expected = _style_rewrite_attestation_signature(payload, create_key=False)
221
+ except MissingPathError:
222
+ return False
223
+ return hmac.compare_digest(str(payload.get("signature") or ""), expected)
224
+
225
+
226
+ def style_rewrite_agent_notice(next_action: str = "") -> str:
227
+ action = next_action.strip() or "repita pela rota oficial antes de aplicar."
228
+ return (
229
+ "Output de style-rewrite ignorado para proteger a Wiki. "
230
+ f"Não remende Markdown, manifest ou recibo manualmente; {action}"
231
+ )
232
+
233
+
234
+ def style_rewrite_agent_event(
235
+ *,
236
+ code: str,
237
+ root_cause_code: str,
238
+ next_action: str,
239
+ artifact_path: str = "",
240
+ ) -> dict[str, object]:
241
+ return {
242
+ "schema": "medical-notes-workbench.agent-event.v1",
243
+ "code": code,
244
+ "severity": "high",
245
+ "root_cause_code": root_cause_code,
246
+ "summary": "Style rewrite apply blocked by typed workflow guardrail.",
247
+ "action": next_action,
248
+ "next_action": next_action,
249
+ "artifact_path": artifact_path,
250
+ }
251
+
252
+
253
+ def _finalize_style_rewrite_apply_receipt(payload: JsonObject) -> JsonObject:
254
+ try:
255
+ receipt = StyleRewriteApplyReceipt.model_validate(payload)
256
+ except PydanticValidationError as exc:
257
+ raise contract_error(exc, prefix="style rewrite apply receipt invalid") from exc
258
+ return receipt.model_dump(mode="json", by_alias=True, exclude_none=True)
259
+
260
+
261
+ def finalize_style_rewrite_apply_receipt(payload: JsonObject) -> JsonObject:
262
+ return _finalize_style_rewrite_apply_receipt(payload)
263
+
264
+
265
+ def _finalize_style_rewrite_output_finalization(payload: dict[str, Any]) -> dict[str, Any]:
266
+ try:
267
+ finalization = StyleRewriteOutputFinalization.model_validate(payload)
268
+ except PydanticValidationError as exc:
269
+ raise contract_error(exc, prefix="style rewrite output finalization invalid") from exc
270
+ return finalization.model_dump(mode="json", by_alias=True, exclude_none=True)
271
+
272
+
273
+ def _finalize_style_rewrite_output_collection(payload: JsonObject) -> JsonObject:
274
+ try:
275
+ collection = StyleRewriteOutputCollection.model_validate(payload)
276
+ except PydanticValidationError as exc:
277
+ raise contract_error(exc, prefix="style rewrite output collection invalid") from exc
278
+ return collection.model_dump(mode="json", by_alias=True, exclude_none=True)
279
+
280
+
281
+ def _finalize_style_rewrite_atomic_apply_result(payload: JsonObject) -> JsonObject:
282
+ try:
283
+ result = StyleRewriteAtomicApplyResult.model_validate(payload)
284
+ except PydanticValidationError as exc:
285
+ raise contract_error(exc, prefix="style rewrite atomic apply result invalid") from exc
286
+ return result.model_dump(mode="json", by_alias=True, exclude_none=True)
287
+
288
+
289
+ def finalize_style_rewrite_atomic_apply_result(payload: JsonObject) -> JsonObject:
290
+ return _finalize_style_rewrite_atomic_apply_result(payload)
291
+
292
+
293
+ def _style_rewrite_blocked_receipt(
294
+ *,
295
+ blocked_reason: str,
296
+ next_action: str,
297
+ plan_path: Path | None = None,
298
+ output_manifest_path: Path | None = None,
299
+ work_id: str = "unknown",
300
+ target_path: Path | None = None,
301
+ output_path: Path | None = None,
302
+ source_plan_hash: str = "",
303
+ manifest_hash: str = "",
304
+ agent_event_code: str = "",
305
+ required_inputs: list[str] | None = None,
306
+ ) -> JsonObject:
307
+ agent_events = []
308
+ artifact_path = (
309
+ _optional_path_text(output_manifest_path)
310
+ or _optional_path_text(output_path)
311
+ or _optional_path_text(plan_path)
312
+ )
313
+ if agent_event_code:
314
+ agent_events.append(
315
+ style_rewrite_agent_event(
316
+ code=agent_event_code,
317
+ root_cause_code=blocked_reason,
318
+ next_action=next_action,
319
+ artifact_path=artifact_path,
320
+ )
321
+ )
322
+ return _finalize_style_rewrite_apply_receipt(
323
+ {
324
+ "schema": STYLE_REWRITE_APPLY_RECEIPT_SCHEMA,
325
+ "phase": "style_rewrite",
326
+ "status": "blocked",
327
+ "blocked_reason": blocked_reason,
328
+ "next_action": next_action,
329
+ "agent_notice": style_rewrite_agent_notice(next_action),
330
+ "required_inputs": required_inputs or ["plan", "manifest", "work_id"],
331
+ "human_decision_required": False,
332
+ "plan_path": _optional_path_text(plan_path),
333
+ "output_manifest_path": _optional_path_text(output_manifest_path),
334
+ "source_plan_hash": source_plan_hash,
335
+ "manifest_hash": manifest_hash,
336
+ "agent_events": agent_events,
337
+ "items": [
338
+ {
339
+ "work_id": work_id,
340
+ "target_path": _optional_path_text(target_path),
341
+ "output_path": _optional_path_text(output_path),
342
+ "status": "blocked",
343
+ "blocked_reason": blocked_reason,
344
+ "next_action": next_action,
345
+ "agent_notice": style_rewrite_agent_notice(next_action),
346
+ }
347
+ ],
348
+ "error_context": error_context(
349
+ phase="style_rewrite",
350
+ blocked_reason=blocked_reason,
351
+ root_cause=blocked_reason,
352
+ affected_artifact=artifact_path or "style_rewrite_apply",
353
+ error_summary="Style rewrite apply provenance could not be verified.",
354
+ suggested_fix=next_action,
355
+ next_action=next_action,
356
+ retry_scope="collect_style_rewrite_outputs_then_apply",
357
+ ),
358
+ }
359
+ )
360
+
361
+
362
+ def _style_report_error_message(report: dict[str, Any]) -> str:
363
+ messages = [str(item.get("message", item.get("code", ""))) for item in report.get("errors", [])]
364
+ return "Generated Wiki note does not match the Wiki_Medicina style contract: " + "; ".join(messages)
365
+
366
+
367
+ def _style_report_rewrite_message(report: dict[str, Any]) -> str:
368
+ warnings = report.get("warnings") if isinstance(report.get("warnings"), list) else []
369
+ codes = [str(item.get("code", "")) for item in warnings if isinstance(item, dict) and item.get("code")]
370
+ joined_codes = ", ".join(codes) if codes else "style_rewrite_required"
371
+ return (
372
+ "requires_llm_rewrite: Generated Wiki note needs med-knowledge-architect rewrite before publication; "
373
+ f"style issues: {joined_codes}."
374
+ )
375
+
376
+
377
+ def validate_wiki_note_contract(content: str, *, title: str, raw_file: Path) -> dict[str, Any]:
378
+ """Reject generated Wiki_Medicina notes that drift from the house style."""
379
+
380
+ report = note_style.validate_note_style(
381
+ content,
382
+ title=title,
383
+ raw_meta=read_note_meta(raw_file),
384
+ path=str(raw_file),
385
+ )
386
+ if report["errors"]:
387
+ raise ValidationError(_style_report_error_message(report))
388
+ if report.get("requires_llm_rewrite"):
389
+ raise ValidationError(_style_report_rewrite_message(report))
390
+ return report
391
+
392
+
393
+ def _require_existing_file(path: Path, *, label: str) -> None:
394
+ """Normalize filesystem probe failures at CLI validation boundaries."""
395
+
396
+ try:
397
+ exists = path.exists()
398
+ except OSError as exc:
399
+ raise MissingPathError(f"{label} path is invalid or too long: {path}") from exc
400
+ if not exists:
401
+ raise MissingPathError(f"{label} file not found: {path}")
402
+
403
+
404
+ def validate_note_style_file(content_path: Path, title: str, raw_file: Path | None = None) -> dict[str, Any]:
405
+ _require_existing_file(content_path, label="Content")
406
+ if raw_file is not None:
407
+ _require_existing_file(raw_file, label="Raw")
408
+ raw_meta = note_style.raw_meta_from_file(raw_file) if raw_file is not None else {}
409
+ return note_style.validate_note_style(
410
+ content_path.read_text(encoding="utf-8"),
411
+ title=title,
412
+ raw_meta=raw_meta,
413
+ path=str(content_path),
414
+ )
415
+
416
+
417
+ def fix_note_style_file(
418
+ content_path: Path,
419
+ title: str,
420
+ output_path: Path,
421
+ raw_file: Path | None = None,
422
+ ) -> JsonObject:
423
+ _require_existing_file(content_path, label="Content")
424
+ if raw_file is not None:
425
+ _require_existing_file(raw_file, label="Raw")
426
+ raw_meta = note_style.raw_meta_from_file(raw_file) if raw_file is not None else {}
427
+ fixed_content, report = note_style.fix_note_style(
428
+ content_path.read_text(encoding="utf-8"),
429
+ title=title,
430
+ raw_meta=raw_meta,
431
+ path=str(content_path),
432
+ )
433
+ output_path.parent.mkdir(parents=True, exist_ok=True)
434
+ atomic_write_text(output_path, fixed_content)
435
+ report["output_path"] = str(output_path)
436
+ report["wrote_output"] = True
437
+ return report
438
+
439
+
440
+ def validate_wiki_style(wiki_dir: Path) -> dict[str, Any]:
441
+ if not wiki_dir.exists():
442
+ raise MissingPathError(f"Wiki dir not found: {wiki_dir}")
443
+ if not wiki_dir.is_dir():
444
+ raise ValidationError(f"Wiki dir is not a directory: {wiki_dir}")
445
+ audit = note_style.validate_wiki_dir(wiki_dir)
446
+ reports = [
447
+ _downgrade_invalid_root_note_report(report, wiki_dir=wiki_dir, path=Path(str(report.get("path") or "")))
448
+ if isinstance(report, dict) and report.get("path")
449
+ else report
450
+ for report in audit.get("reports", [])
451
+ ]
452
+ return {
453
+ **audit,
454
+ "ok_count": sum(1 for item in reports if item["ok"]),
455
+ "error_count": sum(1 for item in reports if item["errors"]),
456
+ "warning_count": sum(1 for item in reports if item["warnings"]),
457
+ "reports": reports,
458
+ }
459
+
460
+
461
+ def _is_loose_root_note(wiki_dir: Path, path: Path) -> bool:
462
+ try:
463
+ rel = path.relative_to(wiki_dir)
464
+ except ValueError:
465
+ return False
466
+ return len(rel.parts) == 1 and not is_index_target(path.stem)
467
+
468
+
469
+ def _downgrade_invalid_root_note_report(report: dict[str, Any], *, wiki_dir: Path, path: Path) -> dict[str, Any]:
470
+ if not _is_loose_root_note(wiki_dir, path) or not report.get("errors"):
471
+ return report
472
+ warning = {
473
+ "code": "root_note.invalid_content",
474
+ "message": "root-level Markdown is not a valid Wiki note; leaving it as a warning until content is repaired or removed",
475
+ "severity": "warning",
476
+ "source": "fix_wiki_style",
477
+ }
478
+ return {
479
+ **report,
480
+ "ok": True,
481
+ "errors": [],
482
+ "warnings": [*report.get("warnings", []), warning],
483
+ "requires_llm_rewrite": False,
484
+ "rewrite_prompt": None,
485
+ "root_note_invalid": True,
486
+ "root_note_invalid_original_errors": report.get("errors", []),
487
+ }
488
+
489
+
490
+ def fix_wiki_style_result(wiki_dir: Path, apply: bool = False, backup: bool = False) -> FixWikiStyleResult:
491
+ """Return the typed deterministic style preview/apply result."""
492
+
493
+ backup = False
494
+ if not wiki_dir.exists():
495
+ raise MissingPathError(f"Wiki dir not found: {wiki_dir}")
496
+ if not wiki_dir.is_dir():
497
+ raise ValidationError(f"Wiki dir is not a directory: {wiki_dir}")
498
+ files = iter_notes(wiki_dir)
499
+ reports: list[dict[str, Any]] = []
500
+ changed_count = 0
501
+ written_count = 0
502
+ backup_paths: list[str] = []
503
+ write_errors: list[dict[str, Any]] = []
504
+ for index, path in enumerate(files, start=1):
505
+ cooperative_cpu_yield(index)
506
+ original = path.read_text(encoding="utf-8")
507
+ title = note_style.infer_title(original, path)
508
+ if is_index_target(path.stem) or is_index_note_content(original):
509
+ report = note_style.index_style_report(original, title=title, path=str(path))
510
+ report["changed"] = False
511
+ report["would_write"] = False
512
+ report["wrote"] = False
513
+ report["backup"] = None
514
+ report["write_error"] = None
515
+ reports.append(report)
516
+ continue
517
+ fixed, report = note_style.fix_note_style(original, title=title, path=str(path))
518
+ if _is_loose_root_note(wiki_dir, path) and report.get("errors"):
519
+ fixed = original
520
+ report = _downgrade_invalid_root_note_report(report, wiki_dir=wiki_dir, path=path)
521
+ changed = fixed != original
522
+ report["changed"] = changed
523
+ report["would_write"] = changed
524
+ report["wrote"] = False
525
+ report["backup"] = None
526
+ report["write_error"] = None
527
+ if changed:
528
+ changed_count += 1
529
+ if apply and changed:
530
+ try:
531
+ atomic_write_text(path, fixed)
532
+ except (FileWriteError, OSError) as exc:
533
+ report["backup"] = None
534
+ report["write_error"] = str(exc)
535
+ write_errors.append(
536
+ {
537
+ "path": str(path),
538
+ "backup": report["backup"],
539
+ "operation": "fix_wiki_style",
540
+ "error": str(exc),
541
+ }
542
+ )
543
+ else:
544
+ report["wrote"] = True
545
+ report["backup"] = None
546
+ written_count += 1
547
+ reports.append(report)
548
+ return FixWikiStyleResult.model_validate(
549
+ {
550
+ "schema": note_style.STYLE_FIX_SCHEMA,
551
+ "wiki_dir": str(wiki_dir),
552
+ "dry_run": not apply,
553
+ "apply": apply,
554
+ "backup": backup,
555
+ "file_count": len(files),
556
+ "changed_count": changed_count,
557
+ "written_count": written_count,
558
+ "error_count": sum(1 for item in reports if item["errors"]),
559
+ "warning_count": sum(1 for item in reports if item["warnings"]),
560
+ "write_error_count": len(write_errors),
561
+ "write_errors": write_errors,
562
+ "backup_paths": backup_paths,
563
+ "reports": reports,
564
+ }
565
+ )
566
+
567
+
568
+ def fix_wiki_style(wiki_dir: Path, apply: bool = False, backup: bool = False) -> JsonObject:
569
+ """Serialize the style result for CLI/adapter edges; domain callers use the model."""
570
+
571
+ return fix_wiki_style_result(wiki_dir, apply=apply, backup=backup).to_payload()
572
+
573
+
574
+ def _requires_style_rewrite(audit: dict[str, Any]) -> bool:
575
+ return any(report.get("requires_llm_rewrite") for report in audit.get("reports", []))
576
+
577
+
578
+ def _managed_related_notes_span(content: str) -> tuple[int, int] | None:
579
+ heading = RELATED_NOTES_HEADING_RE.search(content)
580
+ if heading is None:
581
+ return None
582
+ end_candidates = [
583
+ match.start()
584
+ for match in (
585
+ H2_HEADING_RE.search(content, heading.end()),
586
+ FOOTER_RULE_RE.search(content, heading.end()),
587
+ )
588
+ if match is not None
589
+ ]
590
+ end = min(end_candidates) if end_candidates else len(content)
591
+ return heading.start(), end
592
+
593
+
594
+ def _canonical_managed_related_notes_section(original_content: str) -> str:
595
+ span = _managed_related_notes_span(original_content)
596
+ if span is None:
597
+ return "## 🔗 Notas Relacionadas\n\n"
598
+ return original_content[span[0] : span[1]].rstrip() + "\n\n"
599
+
600
+
601
+ def _preserve_managed_related_notes_section(
602
+ *,
603
+ original_content: str,
604
+ rewritten_content: str,
605
+ ) -> tuple[str, bool]:
606
+ replacement = _canonical_managed_related_notes_section(original_content)
607
+ span = _managed_related_notes_span(rewritten_content)
608
+ if span is None:
609
+ updated = rewritten_content.rstrip() + "\n\n" + replacement
610
+ else:
611
+ updated = rewritten_content[: span[0]].rstrip() + "\n\n" + replacement + rewritten_content[span[1] :].lstrip("\n")
612
+ if not updated.endswith("\n"):
613
+ updated += "\n"
614
+ return updated, updated != rewritten_content
615
+
616
+
617
+ def _prepare_style_rewrite_content(
618
+ *,
619
+ target_path: Path,
620
+ original_content: str,
621
+ rewritten_content: str,
622
+ ) -> tuple[str, list[str]]:
623
+ title = note_style.infer_title(rewritten_content, target_path)
624
+ fixed, report = note_style.fix_note_style(rewritten_content, title=title, path=str(target_path))
625
+ fixes = [str(item) for item in report.get("fixes_applied", []) if str(item).strip()]
626
+ fixed, related_notes_changed = _preserve_managed_related_notes_section(
627
+ original_content=original_content,
628
+ rewritten_content=fixed,
629
+ )
630
+ if related_notes_changed:
631
+ fixes.append("preserve_managed_related_notes_section")
632
+ title = note_style.infer_title(fixed, target_path)
633
+ fixed, report = note_style.fix_note_style(fixed, title=title, path=str(target_path))
634
+ fixes.extend(str(item) for item in report.get("fixes_applied", []) if str(item).strip())
635
+ deduped_fixes: list[str] = []
636
+ for fix in fixes:
637
+ if fix not in deduped_fixes:
638
+ deduped_fixes.append(fix)
639
+ return fixed, deduped_fixes
640
+
641
+
642
+ def _normalize_style_rewrite_output_file(*, target_path: Path, output_path: Path) -> list[str]:
643
+ original = target_path.read_text(encoding="utf-8")
644
+ rewritten = output_path.read_text(encoding="utf-8")
645
+ fixed, fixes = _prepare_style_rewrite_content(
646
+ target_path=target_path,
647
+ original_content=original,
648
+ rewritten_content=rewritten,
649
+ )
650
+ if fixed != rewritten:
651
+ atomic_write_text(output_path, fixed)
652
+ return fixes
653
+
654
+
655
+ def apply_style_rewrite(
656
+ target_path: Path,
657
+ content_path: Path,
658
+ *,
659
+ dry_run: bool = False,
660
+ backup: bool = False,
661
+ rewritten_content: str | None = None,
662
+ ) -> JsonObject:
663
+ backup = False
664
+ if not target_path.exists():
665
+ raise MissingPathError(f"Target note not found: {target_path}")
666
+ if not content_path.exists():
667
+ raise MissingPathError(f"Rewritten content file not found: {content_path}")
668
+ original = target_path.read_text(encoding="utf-8")
669
+ rewritten = content_path.read_text(encoding="utf-8") if rewritten_content is None else rewritten_content
670
+ rewritten, deterministic_fixes = _prepare_style_rewrite_content(
671
+ target_path=target_path,
672
+ original_content=original,
673
+ rewritten_content=rewritten,
674
+ )
675
+ title = note_style.infer_title(rewritten, target_path)
676
+ original_title = note_style.infer_title(original, target_path)
677
+ if original_title != target_path.stem and title != original_title:
678
+ raise ValidationError(f"Rewritten note title changed from {original_title!r} to {title!r}")
679
+ report = note_style.validate_note_style(rewritten, title=title, path=str(target_path))
680
+ result: dict[str, Any] = {
681
+ "target_path": str(target_path),
682
+ "content_path": str(content_path),
683
+ "title": title,
684
+ "dry_run": dry_run,
685
+ "backup": backup,
686
+ "backup_path": None,
687
+ "changed": rewritten != original,
688
+ "written": False,
689
+ "validation": report,
690
+ "deterministic_fixes_applied": deterministic_fixes,
691
+ }
692
+ if report["errors"] or report.get("requires_llm_rewrite"):
693
+ return result
694
+ if not dry_run and rewritten != original:
695
+ atomic_write_text(target_path, rewritten)
696
+ result["written"] = True
697
+ result["backup_path"] = None
698
+ return result
699
+
700
+
701
+ def style_rewrite_manifest_required_receipt(
702
+ *,
703
+ target_path: Path | None = None,
704
+ content_path: Path | None = None,
705
+ ) -> JsonObject:
706
+ return _style_rewrite_blocked_receipt(
707
+ blocked_reason="style_rewrite_manifest_required",
708
+ next_action=(
709
+ "Coletar outputs de style-rewrite pela rota oficial e aplicar com plan, manifest e work_id. "
710
+ "Não aplique Markdown solto."
711
+ ),
712
+ target_path=target_path,
713
+ output_path=content_path,
714
+ agent_event_code="agent.style_rewrite_manifest_required",
715
+ )
716
+
717
+
718
+ def _style_rewrite_output_collection_blocked(
719
+ *,
720
+ blocked_reason: str,
721
+ next_action: str,
722
+ plan_path: Path,
723
+ manifest_path: Path,
724
+ source_plan_hash: str,
725
+ missing_outputs: list[dict[str, str]] | None = None,
726
+ missing_output_receipts: list[dict[str, str]] | None = None,
727
+ invalid_output_receipts: list[dict[str, str]] | None = None,
728
+ missing_output_attestations: list[dict[str, str]] | None = None,
729
+ invalid_output_attestations: list[dict[str, str]] | None = None,
730
+ required_inputs: list[str] | None = None,
731
+ ) -> JsonObject:
732
+ missing_outputs = missing_outputs or []
733
+ missing_output_receipts = missing_output_receipts or []
734
+ invalid_output_receipts = invalid_output_receipts or []
735
+ missing_output_attestations = missing_output_attestations or []
736
+ invalid_output_attestations = invalid_output_attestations or []
737
+ affected = (
738
+ missing_output_attestations[0].get("output_attestation_path", "")
739
+ if missing_output_attestations
740
+ else invalid_output_attestations[0].get("output_attestation_path", "")
741
+ if invalid_output_attestations
742
+ else missing_output_receipts[0].get("output_receipt_path", "")
743
+ if missing_output_receipts
744
+ else invalid_output_receipts[0].get("output_receipt_path", "")
745
+ if invalid_output_receipts
746
+ else missing_outputs[0].get("output_path", "")
747
+ if missing_outputs
748
+ else str(manifest_path)
749
+ )
750
+ required_inputs = required_inputs or ["style_rewrite_output_attestation"]
751
+ if missing_output_receipts or invalid_output_receipts:
752
+ required_inputs.append("style_rewrite_output_receipt")
753
+ return _finalize_style_rewrite_output_collection(
754
+ {
755
+ "schema": "medical-notes-workbench.style-rewrite-output-collection.v1",
756
+ "phase": "style_rewrite",
757
+ "status": "blocked",
758
+ "blocked_reason": blocked_reason,
759
+ "next_action": next_action,
760
+ "required_inputs": required_inputs,
761
+ "human_decision_required": False,
762
+ "plan_path": str(plan_path),
763
+ "manifest_path": str(manifest_path),
764
+ "source_plan_hash": source_plan_hash,
765
+ "missing_output_count": len(missing_outputs),
766
+ "missing_outputs": missing_outputs,
767
+ "missing_output_attestation_count": len(missing_output_attestations),
768
+ "missing_output_attestations": missing_output_attestations,
769
+ "invalid_output_attestation_count": len(invalid_output_attestations),
770
+ "invalid_output_attestations": invalid_output_attestations,
771
+ "missing_output_receipt_count": len(missing_output_receipts),
772
+ "missing_output_receipts": missing_output_receipts,
773
+ "invalid_output_receipt_count": len(invalid_output_receipts),
774
+ "invalid_output_receipts": invalid_output_receipts,
775
+ "agent_notice": style_rewrite_agent_notice(next_action),
776
+ "agent_events": [
777
+ style_rewrite_agent_event(
778
+ code=f"agent.{blocked_reason}",
779
+ root_cause_code=blocked_reason,
780
+ next_action=next_action,
781
+ artifact_path=affected,
782
+ )
783
+ ],
784
+ "error_context": error_context(
785
+ phase="style_rewrite",
786
+ blocked_reason=blocked_reason,
787
+ root_cause=blocked_reason,
788
+ affected_artifact=affected or str(manifest_path),
789
+ error_summary="Style rewrite output was not proven by a Workbench-signed attestation.",
790
+ suggested_fix=next_action,
791
+ next_action=next_action,
792
+ retry_scope="single_style_rewrite_work_item",
793
+ ),
794
+ }
795
+ )
796
+
797
+
798
+ def _style_rewrite_output_receipt_path(raw_item: JsonObject, output_path: Path) -> Path:
799
+ explicit = _style_rewrite_work_item_lens(raw_item).output_receipt_path.strip()
800
+ return Path(explicit) if explicit else output_path.with_suffix(output_path.suffix + ".receipt.json")
801
+
802
+
803
+ def _style_rewrite_output_attestation_path(raw_item: JsonObject, output_path: Path) -> Path:
804
+ explicit = _style_rewrite_work_item_lens(raw_item).output_attestation_path.strip()
805
+ return Path(explicit) if explicit else output_path.with_suffix(output_path.suffix + ".attestation.json")
806
+
807
+
808
+ def _style_rewrite_model_policy(raw_item: JsonObject) -> str:
809
+ return _style_rewrite_work_item_lens(raw_item).model_policy_or_default
810
+
811
+
812
+ def _style_rewrite_specialist_model_blocked(raw_item: JsonObject, *, actual_model: str) -> bool:
813
+ required_tier = _style_rewrite_work_item_lens(raw_item).required_model_tier.strip().lower()
814
+ if required_tier != "specialist":
815
+ return False
816
+ normalized_model = actual_model.strip().lower()
817
+ if not normalized_model:
818
+ return True
819
+ if normalized_model in {"unknown", "runtime_model_or_unknown", "not_reported", "auto"}:
820
+ return True
821
+ return any(token in normalized_model for token in ("flash", "lite", "nano"))
822
+
823
+
824
+ def _style_rewrite_specialist_model_provenance_unverified(
825
+ raw_item: JsonObject,
826
+ *,
827
+ model_verification_status: str,
828
+ ) -> bool:
829
+ required_tier = _style_rewrite_work_item_lens(raw_item).required_model_tier.strip().lower()
830
+ if required_tier != "specialist":
831
+ return False
832
+ if model_verification_status == "verified_by_workbench":
833
+ return False
834
+ return not _style_rewrite_allow_unverified_specialist_model()
835
+
836
+
837
+ def _style_rewrite_allow_unverified_specialist_model() -> bool:
838
+ enabled = os.getenv("MEDNOTES_ALLOW_UNVERIFIED_SPECIALIST_MODEL", "").strip().lower()
839
+ if enabled not in {"1", "true", "yes"}:
840
+ return False
841
+ return bool(os.getenv("MEDNOTES_ALLOW_UNVERIFIED_SPECIALIST_MODEL_REASON", "").strip())
842
+
843
+
844
+ def _style_rewrite_model_provenance_next_action() -> str:
845
+ return (
846
+ "Pare esta tentativa. Refaça este item pela rota oficial de autoria especializada, "
847
+ "com recibo/proveniência do modelo validado pelo Workbench; não aceite modelo Pro "
848
+ "apenas declarado pelo parent e não use escape de desenvolvedor."
849
+ )
850
+
851
+
852
+ def _style_rewrite_receipt_matches_work_item(
853
+ receipt: StyleRewriteOutputReceipt,
854
+ *,
855
+ raw_item: JsonObject,
856
+ work_id: str,
857
+ target_path: Path,
858
+ output_path: Path,
859
+ target_hash_before: str,
860
+ actual_output_hash: str,
861
+ ) -> str:
862
+ if receipt.work_id != work_id:
863
+ return "work_id"
864
+ if receipt.target_path != str(target_path):
865
+ return "target_path"
866
+ if receipt.target_hash_before != target_hash_before:
867
+ return "target_hash_before"
868
+ if receipt.output_path != str(output_path):
869
+ return "output_path"
870
+ if receipt.output_sha256 != actual_output_hash:
871
+ return "output_sha256"
872
+ item = _style_rewrite_work_item_lens(raw_item)
873
+ if receipt.agent != item.agent:
874
+ return "agent"
875
+ if receipt.model_policy != _style_rewrite_model_policy(raw_item):
876
+ return "model_policy"
877
+ if receipt.required_model_tier != item.required_model_tier:
878
+ return "required_model_tier"
879
+ return ""
880
+
881
+
882
+ def _style_rewrite_attestation_matches_work_item(
883
+ attestation: StyleRewriteOutputAttestation,
884
+ *,
885
+ raw_item: JsonObject,
886
+ source_plan_hash: str,
887
+ work_id: str,
888
+ target_path: Path,
889
+ output_path: Path,
890
+ target_hash_before: str,
891
+ actual_output_hash: str,
892
+ ) -> str:
893
+ if attestation.attestation_kind != STYLE_REWRITE_ATTESTATION_KIND:
894
+ return "attestation_kind"
895
+ if attestation.work_id != work_id:
896
+ return "work_id"
897
+ if attestation.source_plan_hash != source_plan_hash:
898
+ return "source_plan_hash"
899
+ if attestation.target_path != str(target_path):
900
+ return "target_path"
901
+ if attestation.target_hash_before != target_hash_before:
902
+ return "target_hash_before"
903
+ if attestation.output_path != str(output_path):
904
+ return "output_path"
905
+ if attestation.output_sha256 != actual_output_hash:
906
+ return "output_sha256"
907
+ item = _style_rewrite_work_item_lens(raw_item)
908
+ if attestation.agent != item.agent:
909
+ return "agent"
910
+ if attestation.model_policy != _style_rewrite_model_policy(raw_item):
911
+ return "model_policy"
912
+ if attestation.required_model_tier != item.required_model_tier:
913
+ return "required_model_tier"
914
+ return ""
915
+
916
+
917
+ def build_style_rewrite_output_attestation(
918
+ *,
919
+ raw_item: JsonObject,
920
+ source_plan_hash: str,
921
+ output_path: Path,
922
+ actual_model: str = "",
923
+ provider: str = "",
924
+ model_claim_source: str | None = None,
925
+ model_verification_status: str | None = None,
926
+ ) -> dict[str, Any]:
927
+ item = _style_rewrite_work_item_lens(raw_item)
928
+ target_path = Path(item.target_path)
929
+ output_hash = _sha256_bytes(output_path.read_bytes())
930
+ claim_source = model_claim_source or ("parent_cli_argument_unverified" if actual_model or provider else "not_reported")
931
+ verification_status = model_verification_status or "unverified_by_workbench"
932
+ payload: JsonObject = {
933
+ "schema": STYLE_REWRITE_OUTPUT_ATTESTATION_SCHEMA,
934
+ "phase": "style_rewrite",
935
+ "status": "completed",
936
+ "attestation_kind": STYLE_REWRITE_ATTESTATION_KIND,
937
+ "work_id": item.work_id,
938
+ "source_plan_hash": source_plan_hash,
939
+ "target_path": str(target_path),
940
+ "target_hash_before": item.target_hash_before,
941
+ "output_path": str(output_path),
942
+ "output_sha256": output_hash,
943
+ "agent": item.agent_or_default,
944
+ "model_policy": _style_rewrite_model_policy(raw_item),
945
+ "required_model_tier": item.required_model_tier_or_default,
946
+ "actual_model": actual_model,
947
+ "provider": provider,
948
+ "model_claim_source": claim_source,
949
+ "model_verification_status": verification_status,
950
+ "nonce": secrets.token_hex(16),
951
+ "issued_at": datetime.now(UTC).isoformat(timespec="seconds"),
952
+ }
953
+ payload["signature"] = _style_rewrite_attestation_signature(payload, create_key=True)
954
+ attestation = _validate_style_rewrite_output_attestation(payload)
955
+ return attestation.model_dump(mode="json", by_alias=True)
956
+
957
+
958
+ def write_style_rewrite_output_attestation(
959
+ *,
960
+ raw_item: JsonObject,
961
+ source_plan_hash: str,
962
+ output_path: Path,
963
+ attestation_path: Path | None = None,
964
+ actual_model: str = "",
965
+ provider: str = "",
966
+ model_claim_source: str | None = None,
967
+ model_verification_status: str | None = None,
968
+ ) -> dict[str, Any]:
969
+ attestation = build_style_rewrite_output_attestation(
970
+ raw_item=raw_item,
971
+ source_plan_hash=source_plan_hash,
972
+ output_path=output_path,
973
+ actual_model=actual_model,
974
+ provider=provider,
975
+ model_claim_source=model_claim_source,
976
+ model_verification_status=model_verification_status,
977
+ )
978
+ path = attestation_path or _style_rewrite_output_attestation_path(raw_item, output_path)
979
+ path.parent.mkdir(parents=True, exist_ok=True)
980
+ atomic_write_text(path, json.dumps(attestation, ensure_ascii=False, indent=2) + "\n")
981
+ return attestation
982
+
983
+
984
+ def write_style_rewrite_output_receipt(
985
+ *,
986
+ raw_item: JsonObject,
987
+ output_path: Path,
988
+ receipt_path: Path | None = None,
989
+ actual_model: str = "",
990
+ provider: str = "",
991
+ ) -> JsonObject:
992
+ item = _style_rewrite_work_item_lens(raw_item)
993
+ receipt = StyleRewriteOutputReceipt(
994
+ schema=STYLE_REWRITE_OUTPUT_RECEIPT_SCHEMA,
995
+ phase="style_rewrite",
996
+ status="completed",
997
+ work_id=item.work_id,
998
+ target_path=item.target_path,
999
+ target_hash_before=item.target_hash_before,
1000
+ output_path=str(output_path),
1001
+ output_sha256=_sha256_bytes(output_path.read_bytes()),
1002
+ agent="med-knowledge-architect",
1003
+ model_policy=_style_rewrite_model_policy(raw_item),
1004
+ required_model_tier=item.required_model_tier_or_default,
1005
+ actual_model=actual_model,
1006
+ provider=provider,
1007
+ ).to_payload()
1008
+ path = receipt_path or _style_rewrite_output_receipt_path(raw_item, output_path)
1009
+ path.parent.mkdir(parents=True, exist_ok=True)
1010
+ atomic_write_text(path, json.dumps(receipt, ensure_ascii=False, indent=2) + "\n")
1011
+ return receipt
1012
+
1013
+
1014
+ def _style_rewrite_output_finalization_blocked(
1015
+ *,
1016
+ blocked_reason: str,
1017
+ next_action: str,
1018
+ plan_path: Path,
1019
+ work_id: str,
1020
+ target_path: Path | None = None,
1021
+ output_path: Path | None = None,
1022
+ source_plan_hash: str = "",
1023
+ validation: JsonObject | None = None,
1024
+ required_inputs: list[str] | None = None,
1025
+ ) -> JsonObject:
1026
+ artifact_path = _optional_path_text(output_path) or _optional_path_text(target_path) or str(plan_path)
1027
+ return _finalize_style_rewrite_output_finalization({
1028
+ "schema": STYLE_REWRITE_OUTPUT_FINALIZATION_SCHEMA,
1029
+ "phase": "style_rewrite",
1030
+ "status": "blocked",
1031
+ "blocked_reason": blocked_reason,
1032
+ "next_action": next_action,
1033
+ "required_inputs": required_inputs or ["plan", "work_id", "temp_output"],
1034
+ "human_decision_required": False,
1035
+ "plan_path": str(plan_path),
1036
+ "work_id": work_id,
1037
+ "target_path": _optional_path_text(target_path),
1038
+ "output_path": _optional_path_text(output_path),
1039
+ "source_plan_hash": source_plan_hash,
1040
+ "validation": validation or {},
1041
+ "agent_notice": style_rewrite_agent_notice(next_action),
1042
+ "agent_events": [
1043
+ style_rewrite_agent_event(
1044
+ code=f"agent.{blocked_reason}",
1045
+ root_cause_code=blocked_reason,
1046
+ next_action=next_action,
1047
+ artifact_path=artifact_path,
1048
+ )
1049
+ ],
1050
+ "error_context": error_context(
1051
+ phase="style_rewrite",
1052
+ blocked_reason=blocked_reason,
1053
+ root_cause=blocked_reason,
1054
+ affected_artifact=artifact_path,
1055
+ error_summary="Style rewrite output could not be finalized by the Workbench attestation boundary.",
1056
+ suggested_fix=next_action,
1057
+ next_action=next_action,
1058
+ retry_scope="single_style_rewrite_work_item",
1059
+ ),
1060
+ })
1061
+
1062
+
1063
+ def _specialist_receipt_for_style_rewrite(
1064
+ path: Path | None,
1065
+ *,
1066
+ work_id: str,
1067
+ output_path: Path,
1068
+ ) -> SpecialistTaskRunReceipt | None:
1069
+ if path is None:
1070
+ return None
1071
+ raw = _read_json_object(path, label="specialist task run receipt")
1072
+ try:
1073
+ validate_specialist_task_run_receipt_attestation(raw)
1074
+ receipt = SpecialistTaskRunReceipt.from_operation_payload(raw)
1075
+ except PydanticValidationError as exc:
1076
+ raise contract_error(exc, prefix="specialist task run receipt invalid") from exc
1077
+ except ValidationError as exc:
1078
+ raise ValidationError(f"specialist task run receipt invalid: {exc}") from exc
1079
+ if receipt.status.value != "completed":
1080
+ raise ValidationError("specialist task run receipt status must be completed")
1081
+ if receipt.phase != "style_rewrite":
1082
+ raise ValidationError("specialist task run receipt phase must be style_rewrite")
1083
+ if receipt.work_id != work_id:
1084
+ raise ValidationError("specialist task run receipt work_id does not match style rewrite work_id")
1085
+ if Path(receipt.output_path) != output_path:
1086
+ raise ValidationError("specialist task run receipt output_path does not match style rewrite output")
1087
+ actual_output_hash = _sha256_bytes(output_path.read_bytes()) if output_path.exists() else ""
1088
+ if receipt.output_sha256 != actual_output_hash:
1089
+ raise ValidationError("specialist task run receipt output_sha256 does not match current output")
1090
+ runtime_error = _specialist_receipt_runtime_provenance_error(receipt)
1091
+ if runtime_error:
1092
+ raise ValidationError(runtime_error)
1093
+ return receipt
1094
+
1095
+
1096
+ def _specialist_receipt_runtime_provenance_error(receipt: SpecialistTaskRunReceipt) -> str:
1097
+ if "mock" in receipt.specialist_session_id.casefold():
1098
+ return "specialist task run receipt specialist_session_id appears to be mock data"
1099
+ transcript_path = Path(receipt.transcript_artifact_path)
1100
+ try:
1101
+ transcript = _read_json_object(transcript_path, label="specialist task run transcript")
1102
+ except (MissingPathError, ValidationError) as exc:
1103
+ return f"specialist task run transcript invalid: {exc}"
1104
+ transcript_text = json.dumps(transcript, ensure_ascii=False, sort_keys=True).casefold()
1105
+ if "mock-session-id" in transcript_text or "gemini_mock" in transcript_text:
1106
+ return "specialist task run transcript contains mock runtime evidence"
1107
+ if receipt.harness.value == "gemini_cli":
1108
+ untrusted_binary = transcript_command_untrusted_gemini_binary(transcript.get("command"))
1109
+ if untrusted_binary and not specialist_dev_escape_enabled():
1110
+ return f"specialist task run transcript used untrusted gemini binary: {untrusted_binary}"
1111
+ return ""
1112
+
1113
+
1114
+ def finalize_style_rewrite_output(
1115
+ *,
1116
+ plan_path: Path,
1117
+ work_id: str,
1118
+ actual_model: str = "",
1119
+ provider: str = "",
1120
+ specialist_run_receipt_path: Path | None = None,
1121
+ ) -> dict[str, Any]:
1122
+ try:
1123
+ plan_payload = _read_json_object(plan_path, label="style rewrite plan")
1124
+ except MissingPathError:
1125
+ return _style_rewrite_output_finalization_blocked(
1126
+ blocked_reason="style_rewrite_plan_missing",
1127
+ next_action=(
1128
+ "Regerar o plano de style-rewrite pela rota oficial de /mednotes:fix-wiki "
1129
+ "antes de finalizar outputs."
1130
+ ),
1131
+ plan_path=plan_path,
1132
+ work_id=work_id,
1133
+ required_inputs=["plan"],
1134
+ )
1135
+ _validate_style_rewrite_plan(plan_payload)
1136
+ try:
1137
+ source_plan_hash = _verify_style_rewrite_plan_attestation(plan_payload)
1138
+ except ValidationError as exc:
1139
+ blocked_reason = subagent_plan_attestation_blocked_reason(exc)
1140
+ return _style_rewrite_output_finalization_blocked(
1141
+ blocked_reason=blocked_reason,
1142
+ next_action=_plan_attestation_next_action(blocked_reason),
1143
+ plan_path=plan_path,
1144
+ work_id=work_id,
1145
+ source_plan_hash=subagent_plan_hash(plan_payload),
1146
+ required_inputs=["subagent_plan_attestation"],
1147
+ )
1148
+ work_item = _style_rewrite_work_item(plan_payload, work_id)
1149
+ if work_item is None:
1150
+ return _style_rewrite_output_finalization_blocked(
1151
+ blocked_reason="style_rewrite_plan_contract_invalid",
1152
+ next_action="Regerar o plano de style-rewrite; o work_id solicitado não está coberto.",
1153
+ plan_path=plan_path,
1154
+ work_id=work_id,
1155
+ source_plan_hash=source_plan_hash,
1156
+ )
1157
+ target_path = Path(str(work_item.get("target_path") or ""))
1158
+ output_path = Path(str(work_item.get("temp_output") or work_item.get("output_path") or ""))
1159
+ target_hash_before = str(work_item.get("target_hash_before") or "")
1160
+ if not output_path.exists():
1161
+ return _style_rewrite_output_finalization_blocked(
1162
+ blocked_reason="style_rewrite_output_missing",
1163
+ next_action="Relançar a autoria médica especializada para este work_item; o temp_output oficial não existe.",
1164
+ plan_path=plan_path,
1165
+ work_id=work_id,
1166
+ target_path=target_path,
1167
+ output_path=output_path,
1168
+ source_plan_hash=source_plan_hash,
1169
+ )
1170
+ try:
1171
+ specialist_receipt = _specialist_receipt_for_style_rewrite(
1172
+ specialist_run_receipt_path,
1173
+ work_id=work_id,
1174
+ output_path=output_path,
1175
+ )
1176
+ except (MissingPathError, ValidationError) as exc:
1177
+ return _style_rewrite_output_finalization_blocked(
1178
+ blocked_reason="specialist_task_run_receipt_invalid",
1179
+ next_action="Refaça a chamada ao especialista pela rota oficial e finalize usando o recibo Workbench válido.",
1180
+ plan_path=plan_path,
1181
+ work_id=work_id,
1182
+ target_path=target_path,
1183
+ output_path=specialist_run_receipt_path or output_path,
1184
+ source_plan_hash=source_plan_hash,
1185
+ validation={"specialist_task_run_receipt_error": str(exc)},
1186
+ required_inputs=["specialist_task_run_receipt"],
1187
+ )
1188
+ model_claim_source: str | None = None
1189
+ model_verification_status: str | None = None
1190
+ if specialist_receipt is not None:
1191
+ actual_model = specialist_receipt.observed_model
1192
+ provider = specialist_receipt.model_evidence.observed_provider_id if specialist_receipt.model_evidence else provider
1193
+ model_claim_source = "specialist_task_run_receipt"
1194
+ model_verification_status = "verified_by_workbench"
1195
+ if _style_rewrite_specialist_model_blocked(work_item, actual_model=actual_model):
1196
+ return _style_rewrite_output_finalization_blocked(
1197
+ blocked_reason="style_rewrite_specialist_model_required",
1198
+ next_action=(
1199
+ "Refaça este item pela rota oficial de autoria especializada e finalize com "
1200
+ "--specialist-run-receipt apontando para o recibo validado pelo Workbench; "
1201
+ "Flash, modelo desconhecido ou modelo declarado manualmente não pode finalizar autoria médica especializada."
1202
+ ),
1203
+ plan_path=plan_path,
1204
+ work_id=work_id,
1205
+ source_plan_hash=source_plan_hash,
1206
+ required_inputs=["specialist_model"],
1207
+ )
1208
+ actual_target_hash = _sha256_bytes(target_path.read_bytes()) if target_path.exists() else ""
1209
+ if actual_target_hash != target_hash_before:
1210
+ return _style_rewrite_output_finalization_blocked(
1211
+ blocked_reason="style_rewrite_stale_target_hash",
1212
+ next_action="Replanejar style-rewrite; a nota alvo mudou desde o plano.",
1213
+ plan_path=plan_path,
1214
+ work_id=work_id,
1215
+ target_path=target_path,
1216
+ output_path=output_path,
1217
+ source_plan_hash=source_plan_hash,
1218
+ )
1219
+ if _style_rewrite_specialist_model_provenance_unverified(
1220
+ work_item,
1221
+ model_verification_status=model_verification_status or "unverified_by_workbench",
1222
+ ):
1223
+ return _style_rewrite_output_finalization_blocked(
1224
+ blocked_reason="style_rewrite_model_provenance_unverified",
1225
+ next_action=_style_rewrite_model_provenance_next_action(),
1226
+ plan_path=plan_path,
1227
+ work_id=work_id,
1228
+ target_path=target_path,
1229
+ output_path=output_path,
1230
+ source_plan_hash=source_plan_hash,
1231
+ required_inputs=["specialist_model_provenance"],
1232
+ )
1233
+ deterministic_fixes = _normalize_style_rewrite_output_file(target_path=target_path, output_path=output_path)
1234
+ validation = apply_style_rewrite(target_path, output_path, dry_run=True)
1235
+ validation["deterministic_fixes_applied"] = deterministic_fixes
1236
+ if validation["validation"]["errors"]:
1237
+ return _style_rewrite_output_finalization_blocked(
1238
+ blocked_reason="style_rewrite_agent_contract_violation",
1239
+ next_action="Regenerar o rewrite pela rota de autoria médica especializada para este work_item.",
1240
+ plan_path=plan_path,
1241
+ work_id=work_id,
1242
+ target_path=target_path,
1243
+ output_path=output_path,
1244
+ source_plan_hash=source_plan_hash,
1245
+ validation=validation,
1246
+ )
1247
+ if validation["validation"].get("requires_llm_rewrite"):
1248
+ return _style_rewrite_output_finalization_blocked(
1249
+ blocked_reason="style_rewrite_still_requires_rewrite",
1250
+ next_action=(
1251
+ "Regenerar o rewrite pela rota de autoria médica especializada até "
1252
+ "validation.requires_llm_rewrite=false; não finalize output que ainda pede reescrita."
1253
+ ),
1254
+ plan_path=plan_path,
1255
+ work_id=work_id,
1256
+ target_path=target_path,
1257
+ output_path=output_path,
1258
+ source_plan_hash=source_plan_hash,
1259
+ validation=validation,
1260
+ )
1261
+ output_receipt_path = _style_rewrite_output_receipt_path(work_item, output_path)
1262
+ output_receipt = write_style_rewrite_output_receipt(
1263
+ raw_item=work_item,
1264
+ output_path=output_path,
1265
+ receipt_path=output_receipt_path,
1266
+ actual_model=actual_model,
1267
+ provider=provider,
1268
+ )
1269
+ attestation_path = _style_rewrite_output_attestation_path(work_item, output_path)
1270
+ attestation = write_style_rewrite_output_attestation(
1271
+ raw_item=work_item,
1272
+ source_plan_hash=source_plan_hash,
1273
+ output_path=output_path,
1274
+ attestation_path=attestation_path,
1275
+ actual_model=actual_model,
1276
+ provider=provider,
1277
+ model_claim_source=model_claim_source,
1278
+ model_verification_status=model_verification_status,
1279
+ )
1280
+ return _finalize_style_rewrite_output_finalization({
1281
+ "schema": STYLE_REWRITE_OUTPUT_FINALIZATION_SCHEMA,
1282
+ "phase": "style_rewrite",
1283
+ "status": "completed",
1284
+ "blocked_reason": "",
1285
+ "next_action": "",
1286
+ "required_inputs": [],
1287
+ "human_decision_required": False,
1288
+ "plan_path": str(plan_path),
1289
+ "work_id": work_id,
1290
+ "target_path": str(target_path),
1291
+ "output_path": str(output_path),
1292
+ "output_sha256": _sha256_bytes(output_path.read_bytes()),
1293
+ "output_receipt_path": str(output_receipt_path),
1294
+ "output_receipt_sha256": _sha256_bytes(output_receipt_path.read_bytes()),
1295
+ "output_attestation_path": str(attestation_path),
1296
+ "output_attestation_sha256": _sha256_bytes(attestation_path.read_bytes()),
1297
+ "source_plan_hash": source_plan_hash,
1298
+ "actual_model": actual_model,
1299
+ "provider": provider,
1300
+ "model_claim_source": attestation.get("model_claim_source", "not_reported"),
1301
+ "model_verification_status": attestation.get("model_verification_status", "unverified_by_workbench"),
1302
+ "validation": validation,
1303
+ "receipt": output_receipt,
1304
+ "attestation": attestation,
1305
+ })
1306
+
1307
+
1308
+ def collect_style_rewrite_outputs(plan_path: Path, manifest_path: Path, *, work_id: str = "") -> JsonObject:
1309
+ plan_payload = _read_json_object(plan_path, label="style rewrite plan")
1310
+ _validate_style_rewrite_plan(plan_payload)
1311
+ try:
1312
+ source_plan_hash = _verify_style_rewrite_plan_attestation(plan_payload)
1313
+ except ValidationError as exc:
1314
+ blocked_reason = subagent_plan_attestation_blocked_reason(exc)
1315
+ return _style_rewrite_output_collection_blocked(
1316
+ blocked_reason=blocked_reason,
1317
+ next_action=_plan_attestation_next_action(blocked_reason),
1318
+ plan_path=plan_path,
1319
+ manifest_path=manifest_path,
1320
+ source_plan_hash=subagent_plan_hash(plan_payload),
1321
+ required_inputs=["subagent_plan_attestation"],
1322
+ )
1323
+ manifest_items: list[dict[str, str]] = []
1324
+ missing_outputs: list[dict[str, str]] = []
1325
+ missing_output_receipts: list[dict[str, str]] = []
1326
+ invalid_output_receipts: list[dict[str, str]] = []
1327
+ missing_output_attestations: list[dict[str, str]] = []
1328
+ invalid_output_attestations: list[dict[str, str]] = []
1329
+ requested_work_id = work_id.strip()
1330
+ raw_work_items = plan_payload.get("work_items", [])
1331
+ if requested_work_id:
1332
+ raw_work_items = [
1333
+ item
1334
+ for item in raw_work_items
1335
+ if isinstance(item, dict) and str(item.get("work_id") or "") == requested_work_id
1336
+ ]
1337
+ if not raw_work_items:
1338
+ return _style_rewrite_output_collection_blocked(
1339
+ blocked_reason="style_rewrite_plan_contract_invalid",
1340
+ next_action="Regerar o plano de style-rewrite; o work_id solicitado não está coberto.",
1341
+ plan_path=plan_path,
1342
+ manifest_path=manifest_path,
1343
+ source_plan_hash=source_plan_hash,
1344
+ )
1345
+ for raw_item in raw_work_items:
1346
+ if not isinstance(raw_item, dict):
1347
+ continue
1348
+ item = _style_rewrite_work_item_lens(raw_item)
1349
+ work_id = item.work_id.strip()
1350
+ target_path = Path(item.target_path)
1351
+ output_path = Path(item.planned_output_path)
1352
+ target_hash_before = item.target_hash_before.strip()
1353
+ if not work_id or not target_path or not output_path:
1354
+ continue
1355
+ if not output_path.exists():
1356
+ missing_outputs.append({"work_id": work_id, "output_path": str(output_path)})
1357
+ continue
1358
+ actual_output_hash = _sha256_bytes(output_path.read_bytes())
1359
+ output_attestation_path = _style_rewrite_output_attestation_path(raw_item, output_path)
1360
+ if not output_attestation_path.exists():
1361
+ missing_output_attestations.append(
1362
+ {
1363
+ "work_id": work_id,
1364
+ "output_path": str(output_path),
1365
+ "output_attestation_path": str(output_attestation_path),
1366
+ }
1367
+ )
1368
+ continue
1369
+ try:
1370
+ raw_attestation = _read_json_object(output_attestation_path, label="style rewrite output attestation")
1371
+ output_attestation = _validate_style_rewrite_output_attestation(raw_attestation)
1372
+ mismatch = (
1373
+ ""
1374
+ if _style_rewrite_verify_attestation_signature(raw_attestation)
1375
+ else "signature"
1376
+ )
1377
+ if not mismatch:
1378
+ mismatch = _style_rewrite_attestation_matches_work_item(
1379
+ output_attestation,
1380
+ raw_item=raw_item,
1381
+ source_plan_hash=source_plan_hash,
1382
+ work_id=work_id,
1383
+ target_path=target_path,
1384
+ output_path=output_path,
1385
+ target_hash_before=target_hash_before,
1386
+ actual_output_hash=actual_output_hash,
1387
+ )
1388
+ if not mismatch and _style_rewrite_specialist_model_provenance_unverified(
1389
+ raw_item,
1390
+ model_verification_status=output_attestation.model_verification_status,
1391
+ ):
1392
+ mismatch = (
1393
+ "model_verification_status unverified_by_workbench is not accepted for specialist "
1394
+ "model output without official runtime provenance"
1395
+ )
1396
+ except (MissingPathError, ValidationError) as exc:
1397
+ invalid_output_attestations.append(
1398
+ {
1399
+ "work_id": work_id,
1400
+ "output_path": str(output_path),
1401
+ "output_attestation_path": str(output_attestation_path),
1402
+ "error": str(exc),
1403
+ }
1404
+ )
1405
+ continue
1406
+ if mismatch:
1407
+ invalid_output_attestations.append(
1408
+ {
1409
+ "work_id": work_id,
1410
+ "output_path": str(output_path),
1411
+ "output_attestation_path": str(output_attestation_path),
1412
+ "error": f"style rewrite output attestation does not match work item: {mismatch}",
1413
+ }
1414
+ )
1415
+ continue
1416
+ output_receipt_path = _style_rewrite_output_receipt_path(raw_item, output_path)
1417
+ output_receipt_sha256 = ""
1418
+ if output_receipt_path.exists():
1419
+ try:
1420
+ output_receipt = _validate_style_rewrite_output_receipt(
1421
+ _read_json_object(output_receipt_path, label="style rewrite output receipt")
1422
+ )
1423
+ receipt_mismatch = _style_rewrite_receipt_matches_work_item(
1424
+ output_receipt,
1425
+ raw_item=raw_item,
1426
+ work_id=work_id,
1427
+ target_path=target_path,
1428
+ output_path=output_path,
1429
+ target_hash_before=target_hash_before,
1430
+ actual_output_hash=actual_output_hash,
1431
+ )
1432
+ except (MissingPathError, ValidationError) as exc:
1433
+ invalid_output_receipts.append(
1434
+ {
1435
+ "work_id": work_id,
1436
+ "output_path": str(output_path),
1437
+ "output_receipt_path": str(output_receipt_path),
1438
+ "error": str(exc),
1439
+ }
1440
+ )
1441
+ continue
1442
+ if receipt_mismatch:
1443
+ invalid_output_receipts.append(
1444
+ {
1445
+ "work_id": work_id,
1446
+ "output_path": str(output_path),
1447
+ "output_receipt_path": str(output_receipt_path),
1448
+ "error": f"style rewrite output receipt does not match work item: {receipt_mismatch}",
1449
+ }
1450
+ )
1451
+ continue
1452
+ output_receipt_sha256 = _sha256_bytes(output_receipt_path.read_bytes())
1453
+ elif item.output_receipt_path.strip():
1454
+ missing_output_receipts.append(
1455
+ {
1456
+ "work_id": work_id,
1457
+ "output_path": str(output_path),
1458
+ "output_receipt_path": str(output_receipt_path),
1459
+ }
1460
+ )
1461
+ continue
1462
+ manifest_items.append(
1463
+ {
1464
+ "work_id": work_id,
1465
+ "target_path": str(target_path),
1466
+ "target_hash_before": target_hash_before,
1467
+ "output_path": str(output_path),
1468
+ "sha256": actual_output_hash,
1469
+ "output_attestation_path": str(output_attestation_path),
1470
+ "output_attestation_sha256": _sha256_bytes(output_attestation_path.read_bytes()),
1471
+ "output_receipt_path": str(output_receipt_path) if output_receipt_sha256 else "",
1472
+ "output_receipt_sha256": output_receipt_sha256,
1473
+ "agent": item.agent_or_default,
1474
+ "model_policy": _style_rewrite_model_policy(raw_item),
1475
+ "required_model_tier": item.required_model_tier_or_default,
1476
+ }
1477
+ )
1478
+ if (
1479
+ missing_outputs
1480
+ or missing_output_attestations
1481
+ or invalid_output_attestations
1482
+ or missing_output_receipts
1483
+ or invalid_output_receipts
1484
+ ):
1485
+ if invalid_output_attestations:
1486
+ unverified_model = any(
1487
+ "model_verification_status unverified_by_workbench" in item.get("error", "")
1488
+ for item in invalid_output_attestations
1489
+ )
1490
+ if unverified_model:
1491
+ blocked_reason = "style_rewrite_model_provenance_unverified"
1492
+ next_action = _style_rewrite_model_provenance_next_action()
1493
+ required_inputs = ["specialist_model_provenance"]
1494
+ else:
1495
+ blocked_reason = "style_rewrite_output_attestation_invalid"
1496
+ next_action = (
1497
+ "Regenerar o output pela porta oficial de style-rewrite para o work_item atual; "
1498
+ "não assine, copie ou remende Markdown manualmente."
1499
+ )
1500
+ required_inputs = None
1501
+ elif missing_output_attestations:
1502
+ blocked_reason = "style_rewrite_output_attestation_required"
1503
+ next_action = (
1504
+ "Regenerar o output pela porta oficial de style-rewrite para o work_item atual; "
1505
+ "o collect só aceita output com style-rewrite-output-attestation.v1 assinada pelo Workbench."
1506
+ )
1507
+ required_inputs = None
1508
+ elif invalid_output_receipts:
1509
+ blocked_reason = "style_rewrite_output_receipt_invalid"
1510
+ next_action = (
1511
+ "Regenerar o output pela porta oficial de style-rewrite para o work_item atual; "
1512
+ "não remende recibo legado ou Markdown manualmente."
1513
+ )
1514
+ required_inputs = None
1515
+ elif missing_output_receipts:
1516
+ blocked_reason = "style_rewrite_output_receipt_required"
1517
+ next_action = (
1518
+ "Regenerar o output pela porta oficial de style-rewrite para o work_item atual; "
1519
+ "o recibo legado declarado no plano não existe."
1520
+ )
1521
+ required_inputs = None
1522
+ else:
1523
+ blocked_reason = "style_rewrite_output_missing"
1524
+ next_action = (
1525
+ "Regenerar o output pela porta oficial de style-rewrite para o work_item atual e repetir "
1526
+ "collect-style-rewrite-outputs depois que o temp_output existir."
1527
+ )
1528
+ required_inputs = None
1529
+ return _style_rewrite_output_collection_blocked(
1530
+ blocked_reason=blocked_reason,
1531
+ next_action=next_action,
1532
+ plan_path=plan_path,
1533
+ manifest_path=manifest_path,
1534
+ source_plan_hash=source_plan_hash,
1535
+ missing_outputs=missing_outputs,
1536
+ missing_output_receipts=missing_output_receipts,
1537
+ invalid_output_receipts=invalid_output_receipts,
1538
+ missing_output_attestations=missing_output_attestations,
1539
+ invalid_output_attestations=invalid_output_attestations,
1540
+ required_inputs=required_inputs,
1541
+ )
1542
+ manifest = _validate_style_rewrite_manifest(
1543
+ {
1544
+ "schema": STYLE_REWRITE_MANIFEST_SCHEMA,
1545
+ "source_plan_hash": source_plan_hash,
1546
+ "batch_id": str(plan_payload.get("batch_id") or ""),
1547
+ "items": manifest_items,
1548
+ }
1549
+ )
1550
+ manifest_payload = manifest.model_dump(mode="json", by_alias=True, exclude_none=True)
1551
+ manifest_path.parent.mkdir(parents=True, exist_ok=True)
1552
+ atomic_write_text(manifest_path, json.dumps(manifest_payload, ensure_ascii=False, indent=2) + "\n")
1553
+ return _finalize_style_rewrite_output_collection(
1554
+ {
1555
+ "schema": "medical-notes-workbench.style-rewrite-output-collection.v1",
1556
+ "phase": "style_rewrite",
1557
+ "status": "completed",
1558
+ "blocked_reason": "",
1559
+ "next_action": "",
1560
+ "required_inputs": [],
1561
+ "human_decision_required": False,
1562
+ "plan_path": str(plan_path),
1563
+ "manifest_path": str(manifest_path),
1564
+ "source_plan_hash": source_plan_hash,
1565
+ "manifest_hash": manifest.fingerprint(),
1566
+ "item_count": len(manifest.items),
1567
+ "items": manifest_payload["items"],
1568
+ }
1569
+ )
1570
+
1571
+
1572
+ def _style_rewrite_work_item(plan_payload: dict[str, Any], work_id: str) -> dict[str, Any] | None:
1573
+ for item in plan_payload.get("work_items", []):
1574
+ if isinstance(item, dict) and str(item.get("work_id") or "") == work_id:
1575
+ return item
1576
+ return None
1577
+
1578
+
1579
+ def apply_style_rewrite_from_manifest(
1580
+ *,
1581
+ plan_path: Path,
1582
+ outputs_path: Path,
1583
+ work_id: str,
1584
+ dry_run: bool = False,
1585
+ backup: bool = False,
1586
+ ) -> dict[str, Any]:
1587
+ plan_payload = _read_json_object(plan_path, label="style rewrite plan")
1588
+ _validate_style_rewrite_plan(plan_payload)
1589
+ manifest_payload = _read_json_object(outputs_path, label="style rewrite manifest")
1590
+ manifest = _validate_style_rewrite_manifest(manifest_payload)
1591
+ try:
1592
+ source_plan_hash = _verify_style_rewrite_plan_attestation(plan_payload)
1593
+ except ValidationError as exc:
1594
+ blocked_reason = subagent_plan_attestation_blocked_reason(exc)
1595
+ manifest_hash = manifest.fingerprint()
1596
+ return _style_rewrite_blocked_receipt(
1597
+ blocked_reason=blocked_reason,
1598
+ next_action=_plan_attestation_next_action(blocked_reason),
1599
+ plan_path=plan_path,
1600
+ output_manifest_path=outputs_path,
1601
+ work_id=work_id,
1602
+ source_plan_hash=subagent_plan_hash(plan_payload),
1603
+ manifest_hash=manifest_hash,
1604
+ agent_event_code=f"agent.{blocked_reason}",
1605
+ required_inputs=["subagent_plan_attestation"],
1606
+ )
1607
+ manifest_hash = manifest.fingerprint()
1608
+ if manifest.source_plan_hash != source_plan_hash:
1609
+ return _style_rewrite_blocked_receipt(
1610
+ blocked_reason="style_rewrite_manifest_invalid",
1611
+ next_action="Recriar o manifest de style-rewrite a partir do plano atual antes de aplicar.",
1612
+ plan_path=plan_path,
1613
+ output_manifest_path=outputs_path,
1614
+ work_id=work_id,
1615
+ source_plan_hash=source_plan_hash,
1616
+ manifest_hash=manifest_hash,
1617
+ agent_event_code="agent.style_rewrite_manifest_plan_mismatch",
1618
+ )
1619
+ work_item = _style_rewrite_work_item(plan_payload, work_id)
1620
+ manifest_item = next((item for item in manifest.items if item.work_id == work_id), None)
1621
+ if work_item is None or manifest_item is None:
1622
+ return _style_rewrite_blocked_receipt(
1623
+ blocked_reason="style_rewrite_manifest_invalid",
1624
+ next_action="Recriar plano e manifest de style-rewrite; o work_id solicitado não está coberto.",
1625
+ plan_path=plan_path,
1626
+ output_manifest_path=outputs_path,
1627
+ work_id=work_id,
1628
+ source_plan_hash=source_plan_hash,
1629
+ manifest_hash=manifest_hash,
1630
+ agent_event_code="agent.style_rewrite_manifest_missing_work_id",
1631
+ )
1632
+ target_path = Path(str(work_item.get("target_path") or manifest_item.target_path))
1633
+ output_path = Path(str(manifest_item.output_path))
1634
+ expected_target_hash = str(work_item.get("target_hash_before") or "")
1635
+ if (
1636
+ str(manifest_item.target_path) != str(target_path)
1637
+ or str(manifest_item.target_hash_before) != expected_target_hash
1638
+ or str(manifest_item.required_model_tier) != str(work_item.get("required_model_tier") or "")
1639
+ or str(manifest_item.model_policy) != _style_rewrite_model_policy(work_item)
1640
+ or str(work_item.get("agent") or "") != "med-knowledge-architect"
1641
+ ):
1642
+ return _style_rewrite_blocked_receipt(
1643
+ blocked_reason="style_rewrite_plan_contract_invalid",
1644
+ next_action="Regerar o plano de style-rewrite pela rota oficial antes de aplicar.",
1645
+ plan_path=plan_path,
1646
+ output_manifest_path=outputs_path,
1647
+ work_id=work_id,
1648
+ target_path=target_path,
1649
+ output_path=output_path,
1650
+ source_plan_hash=source_plan_hash,
1651
+ manifest_hash=manifest_hash,
1652
+ agent_event_code="agent.style_rewrite_plan_contract_invalid",
1653
+ )
1654
+ actual_output_hash = _sha256_bytes(output_path.read_bytes()) if output_path.exists() else ""
1655
+ if actual_output_hash != manifest_item.sha256:
1656
+ return _style_rewrite_blocked_receipt(
1657
+ blocked_reason="style_rewrite_output_hash_mismatch",
1658
+ next_action="Recriar o manifest depois de regenerar o output pela rota de autoria médica especializada.",
1659
+ plan_path=plan_path,
1660
+ output_manifest_path=outputs_path,
1661
+ work_id=work_id,
1662
+ target_path=target_path,
1663
+ output_path=output_path,
1664
+ source_plan_hash=source_plan_hash,
1665
+ manifest_hash=manifest_hash,
1666
+ agent_event_code="agent.style_rewrite_output_hash_mismatch",
1667
+ )
1668
+ attestation_path = Path(str(manifest_item.output_attestation_path))
1669
+ actual_output_attestation_hash = _sha256_bytes(attestation_path.read_bytes()) if attestation_path.exists() else ""
1670
+ if actual_output_attestation_hash != manifest_item.output_attestation_sha256:
1671
+ return _style_rewrite_blocked_receipt(
1672
+ blocked_reason="style_rewrite_output_attestation_hash_mismatch",
1673
+ next_action="Recriar o manifest depois de regenerar o output pela porta oficial de style-rewrite.",
1674
+ plan_path=plan_path,
1675
+ output_manifest_path=outputs_path,
1676
+ work_id=work_id,
1677
+ target_path=target_path,
1678
+ output_path=attestation_path,
1679
+ source_plan_hash=source_plan_hash,
1680
+ manifest_hash=manifest_hash,
1681
+ agent_event_code="agent.style_rewrite_output_attestation_hash_mismatch",
1682
+ )
1683
+ output_attestation: StyleRewriteOutputAttestation | None = None
1684
+ try:
1685
+ raw_attestation = _read_json_object(attestation_path, label="style rewrite output attestation")
1686
+ output_attestation = _validate_style_rewrite_output_attestation(raw_attestation)
1687
+ attestation_mismatch = (
1688
+ ""
1689
+ if _style_rewrite_verify_attestation_signature(raw_attestation)
1690
+ else "signature"
1691
+ )
1692
+ if not attestation_mismatch:
1693
+ attestation_mismatch = _style_rewrite_attestation_matches_work_item(
1694
+ output_attestation,
1695
+ raw_item=work_item,
1696
+ source_plan_hash=source_plan_hash,
1697
+ work_id=work_id,
1698
+ target_path=target_path,
1699
+ output_path=output_path,
1700
+ target_hash_before=expected_target_hash,
1701
+ actual_output_hash=actual_output_hash,
1702
+ )
1703
+ except (MissingPathError, ValidationError) as exc:
1704
+ attestation_mismatch = str(exc)
1705
+ if attestation_mismatch:
1706
+ return _style_rewrite_blocked_receipt(
1707
+ blocked_reason="style_rewrite_output_attestation_invalid",
1708
+ next_action="Regenerar o output pela porta oficial de style-rewrite e coletar novo manifest.",
1709
+ plan_path=plan_path,
1710
+ output_manifest_path=outputs_path,
1711
+ work_id=work_id,
1712
+ target_path=target_path,
1713
+ output_path=attestation_path,
1714
+ source_plan_hash=source_plan_hash,
1715
+ manifest_hash=manifest_hash,
1716
+ agent_event_code="agent.style_rewrite_output_attestation_invalid",
1717
+ )
1718
+ if output_attestation is None:
1719
+ return _style_rewrite_blocked_receipt(
1720
+ blocked_reason="style_rewrite_output_attestation_invalid",
1721
+ next_action="Regenerar o output pela porta oficial de style-rewrite e coletar novo manifest.",
1722
+ plan_path=plan_path,
1723
+ output_manifest_path=outputs_path,
1724
+ work_id=work_id,
1725
+ target_path=target_path,
1726
+ output_path=attestation_path,
1727
+ source_plan_hash=source_plan_hash,
1728
+ manifest_hash=manifest_hash,
1729
+ agent_event_code="agent.style_rewrite_output_attestation_invalid",
1730
+ )
1731
+ if _style_rewrite_specialist_model_provenance_unverified(
1732
+ work_item,
1733
+ model_verification_status=output_attestation.model_verification_status,
1734
+ ):
1735
+ return _style_rewrite_blocked_receipt(
1736
+ blocked_reason="style_rewrite_model_provenance_unverified",
1737
+ next_action=_style_rewrite_model_provenance_next_action(),
1738
+ plan_path=plan_path,
1739
+ output_manifest_path=outputs_path,
1740
+ work_id=work_id,
1741
+ target_path=target_path,
1742
+ output_path=attestation_path,
1743
+ source_plan_hash=source_plan_hash,
1744
+ manifest_hash=manifest_hash,
1745
+ agent_event_code="agent.style_rewrite_model_provenance_unverified",
1746
+ required_inputs=["specialist_model_provenance"],
1747
+ )
1748
+ actual_target_hash = _sha256_bytes(target_path.read_bytes()) if target_path.exists() else ""
1749
+ if actual_target_hash != expected_target_hash:
1750
+ return _style_rewrite_blocked_receipt(
1751
+ blocked_reason="style_rewrite_stale_target_hash",
1752
+ next_action="Replanejar style-rewrite; a nota alvo mudou desde o plano.",
1753
+ plan_path=plan_path,
1754
+ output_manifest_path=outputs_path,
1755
+ work_id=work_id,
1756
+ target_path=target_path,
1757
+ output_path=output_path,
1758
+ source_plan_hash=source_plan_hash,
1759
+ manifest_hash=manifest_hash,
1760
+ agent_event_code="agent.style_rewrite_stale_target_hash",
1761
+ )
1762
+ applied = apply_style_rewrite(target_path, output_path, dry_run=dry_run, backup=backup)
1763
+ if applied["validation"]["errors"]:
1764
+ return _style_rewrite_blocked_receipt(
1765
+ blocked_reason="validation_errors",
1766
+ next_action="Regenerar o rewrite pela rota de autoria médica especializada e coletar novo manifest.",
1767
+ plan_path=plan_path,
1768
+ output_manifest_path=outputs_path,
1769
+ work_id=work_id,
1770
+ target_path=target_path,
1771
+ output_path=output_path,
1772
+ source_plan_hash=source_plan_hash,
1773
+ manifest_hash=manifest_hash,
1774
+ agent_event_code="agent.style_rewrite_validation_errors",
1775
+ )
1776
+ if applied["validation"].get("requires_llm_rewrite"):
1777
+ return _style_rewrite_blocked_receipt(
1778
+ blocked_reason="style_rewrite_still_requires_rewrite",
1779
+ next_action=(
1780
+ "Regenerar o rewrite pela rota de autoria médica especializada até "
1781
+ "validation.requires_llm_rewrite=false; não aplique output que ainda pede reescrita."
1782
+ ),
1783
+ plan_path=plan_path,
1784
+ output_manifest_path=outputs_path,
1785
+ work_id=work_id,
1786
+ target_path=target_path,
1787
+ output_path=output_path,
1788
+ source_plan_hash=source_plan_hash,
1789
+ manifest_hash=manifest_hash,
1790
+ agent_event_code="agent.style_rewrite_still_requires_rewrite",
1791
+ )
1792
+ item_status = "applied" if applied.get("written") else "idempotent"
1793
+ return _finalize_style_rewrite_apply_receipt(
1794
+ {
1795
+ "schema": STYLE_REWRITE_APPLY_RECEIPT_SCHEMA,
1796
+ "phase": "style_rewrite",
1797
+ "status": "completed",
1798
+ "blocked_reason": "",
1799
+ "next_action": "",
1800
+ "required_inputs": [],
1801
+ "human_decision_required": False,
1802
+ "plan_path": str(plan_path),
1803
+ "output_manifest_path": str(outputs_path),
1804
+ "source_plan_hash": source_plan_hash,
1805
+ "manifest_hash": manifest_hash,
1806
+ "items": [
1807
+ {
1808
+ "work_id": work_id,
1809
+ "target_path": str(target_path),
1810
+ "output_path": str(output_path),
1811
+ "status": item_status,
1812
+ "changed": bool(applied.get("changed")),
1813
+ "written": bool(applied.get("written")),
1814
+ "backup_path": applied.get("backup_path"),
1815
+ }
1816
+ ],
1817
+ }
1818
+ )
1819
+
1820
+
1821
+ def finalize_collect_apply_style_rewrite(
1822
+ *,
1823
+ plan_path: Path,
1824
+ manifest_path: Path,
1825
+ work_id: str,
1826
+ specialist_run_receipt_path: Path | None,
1827
+ backup: bool = False,
1828
+ ) -> JsonObject:
1829
+ if specialist_run_receipt_path is None:
1830
+ next_action = (
1831
+ "Pare esta tentativa. Refaça a chamada ao especialista pela rota oficial e aplique "
1832
+ "usando o recibo Workbench válido; não passe recibo vazio nem tente declarar modelo manualmente."
1833
+ )
1834
+ return _finalize_style_rewrite_atomic_apply_result(
1835
+ {
1836
+ "schema": STYLE_REWRITE_ATOMIC_APPLY_RESULT_SCHEMA,
1837
+ "phase": "style_rewrite",
1838
+ "status": "blocked",
1839
+ "blocked_reason": "specialist_task_run_receipt_invalid",
1840
+ "next_action": next_action,
1841
+ "required_inputs": ["specialist_task_run_receipt"],
1842
+ "human_decision_required": False,
1843
+ "plan_path": str(plan_path),
1844
+ "manifest_path": str(manifest_path),
1845
+ "work_id": work_id,
1846
+ "specialist_run_receipt_path": "",
1847
+ "finalization": None,
1848
+ "collection": None,
1849
+ "apply": None,
1850
+ "agent_notice": style_rewrite_agent_notice(next_action),
1851
+ "error_context": error_context(
1852
+ phase="style_rewrite",
1853
+ blocked_reason="specialist_task_run_receipt_invalid",
1854
+ root_cause="specialist_task_run_receipt_invalid",
1855
+ affected_artifact=str(manifest_path),
1856
+ error_summary="specialist_run_receipt_path ausente ou vazio.",
1857
+ suggested_fix=next_action,
1858
+ next_action=next_action,
1859
+ retry_scope="single_style_rewrite_work_item",
1860
+ missing_inputs=["specialist_task_run_receipt"],
1861
+ ),
1862
+ }
1863
+ )
1864
+ finalization_payload = finalize_style_rewrite_output(
1865
+ plan_path=plan_path,
1866
+ work_id=work_id,
1867
+ specialist_run_receipt_path=specialist_run_receipt_path,
1868
+ )
1869
+ finalization = StyleRewriteOutputFinalization.model_validate(finalization_payload)
1870
+ if finalization.status == "blocked":
1871
+ return _finalize_style_rewrite_atomic_apply_result(
1872
+ {
1873
+ "schema": STYLE_REWRITE_ATOMIC_APPLY_RESULT_SCHEMA,
1874
+ "phase": "style_rewrite",
1875
+ "status": "blocked",
1876
+ "blocked_reason": finalization.blocked_reason,
1877
+ "next_action": finalization.next_action,
1878
+ "required_inputs": finalization.required_inputs,
1879
+ "human_decision_required": finalization.human_decision_required,
1880
+ "plan_path": str(plan_path),
1881
+ "manifest_path": str(manifest_path),
1882
+ "work_id": work_id,
1883
+ "specialist_run_receipt_path": str(specialist_run_receipt_path),
1884
+ "finalization": finalization.model_dump(mode="json", by_alias=True, exclude_none=True),
1885
+ "collection": None,
1886
+ "apply": None,
1887
+ "agent_notice": "A etapa atômica parou antes de coletar/aplicar porque a finalização do output falhou.",
1888
+ "error_context": finalization.error_context.model_dump(mode="json", by_alias=True)
1889
+ if finalization.error_context
1890
+ else None,
1891
+ }
1892
+ )
1893
+
1894
+ collection_payload = collect_style_rewrite_outputs(plan_path, manifest_path, work_id=work_id)
1895
+ collection = StyleRewriteOutputCollection.model_validate(collection_payload)
1896
+ if collection.status == "blocked":
1897
+ return _finalize_style_rewrite_atomic_apply_result(
1898
+ {
1899
+ "schema": STYLE_REWRITE_ATOMIC_APPLY_RESULT_SCHEMA,
1900
+ "phase": "style_rewrite",
1901
+ "status": "blocked",
1902
+ "blocked_reason": collection.blocked_reason,
1903
+ "next_action": collection.next_action,
1904
+ "required_inputs": collection.required_inputs,
1905
+ "human_decision_required": collection.human_decision_required,
1906
+ "plan_path": str(plan_path),
1907
+ "manifest_path": str(manifest_path),
1908
+ "work_id": work_id,
1909
+ "specialist_run_receipt_path": str(specialist_run_receipt_path),
1910
+ "finalization": finalization.model_dump(mode="json", by_alias=True, exclude_none=True),
1911
+ "collection": collection.model_dump(mode="json", by_alias=True, exclude_none=True),
1912
+ "apply": None,
1913
+ "agent_notice": "A etapa atômica parou antes de aplicar porque a coleta tipada do output falhou.",
1914
+ "error_context": collection.error_context.model_dump(mode="json", by_alias=True)
1915
+ if collection.error_context
1916
+ else None,
1917
+ }
1918
+ )
1919
+
1920
+ apply_payload = apply_style_rewrite_from_manifest(
1921
+ plan_path=plan_path,
1922
+ outputs_path=manifest_path,
1923
+ work_id=work_id,
1924
+ dry_run=False,
1925
+ backup=backup,
1926
+ )
1927
+ apply_receipt = StyleRewriteApplyReceipt.model_validate(apply_payload)
1928
+ return _finalize_style_rewrite_atomic_apply_result(
1929
+ {
1930
+ "schema": STYLE_REWRITE_ATOMIC_APPLY_RESULT_SCHEMA,
1931
+ "phase": "style_rewrite",
1932
+ "status": apply_receipt.status,
1933
+ "blocked_reason": apply_receipt.blocked_reason,
1934
+ "next_action": apply_receipt.next_action,
1935
+ "required_inputs": apply_receipt.required_inputs,
1936
+ "human_decision_required": apply_receipt.human_decision_required,
1937
+ "plan_path": str(plan_path),
1938
+ "manifest_path": str(manifest_path),
1939
+ "work_id": work_id,
1940
+ "specialist_run_receipt_path": str(specialist_run_receipt_path),
1941
+ "finalization": finalization.model_dump(mode="json", by_alias=True, exclude_none=True),
1942
+ "collection": collection.model_dump(mode="json", by_alias=True, exclude_none=True),
1943
+ "apply": apply_receipt.model_dump(mode="json", by_alias=True, exclude_none=True),
1944
+ "agent_notice": (
1945
+ "Finalização, coleta e aplicação deste item foram executadas como uma etapa atômica "
1946
+ "para evitar corrida entre comandos dependentes."
1947
+ ),
1948
+ "error_context": apply_receipt.error_context.model_dump(mode="json", by_alias=True)
1949
+ if apply_receipt.error_context
1950
+ else None,
1951
+ }
1952
+ )