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.
- package/.opencode/agents/med-chat-triager.md +204 -0
- package/.opencode/agents/med-flashcard-maker.md +63 -0
- package/.opencode/agents/med-knowledge-architect.md +230 -0
- package/.opencode/agents/med-link-graph-curator.md +177 -0
- package/.opencode/agents/med-publish-guard.md +62 -0
- package/.opencode/commands/flashcards.md +25 -0
- package/.opencode/commands/mednotes/create.md +25 -0
- package/.opencode/commands/mednotes/enrich.md +27 -0
- package/.opencode/commands/mednotes/fix-wiki.md +27 -0
- package/.opencode/commands/mednotes/history.md +22 -0
- package/.opencode/commands/mednotes/link-body.md +25 -0
- package/.opencode/commands/mednotes/link-related.md +27 -0
- package/.opencode/commands/mednotes/link.md +27 -0
- package/.opencode/commands/mednotes/pdf-library.md +27 -0
- package/.opencode/commands/mednotes/process-chats.md +23 -0
- package/.opencode/commands/mednotes/setup.md +21 -0
- package/.opencode/commands/mednotes/status.md +27 -0
- package/.opencode/commands/mednotes/telemetry.md +27 -0
- package/.opencode/commands/report.md +26 -0
- package/.opencode/mednotes/AGENTS.md +57 -0
- package/.opencode/mednotes/agents/med-chat-triager.md +197 -0
- package/.opencode/mednotes/agents/med-flashcard-maker.md +56 -0
- package/.opencode/mednotes/agents/med-knowledge-architect.md +224 -0
- package/.opencode/mednotes/agents/med-link-graph-curator.md +171 -0
- package/.opencode/mednotes/agents/med-publish-guard.md +55 -0
- package/.opencode/mednotes/contracts/.gitkeep +1 -0
- package/.opencode/mednotes/contracts/agents.json +116 -0
- package/.opencode/mednotes/contracts/opencode-plugin.json +70 -0
- package/.opencode/mednotes/docs/agent-prompt-hardening.md +567 -0
- package/.opencode/mednotes/docs/agent-role-contracts.md +94 -0
- package/.opencode/mednotes/docs/anki-mcp-twenty-rules.md +214 -0
- package/.opencode/mednotes/docs/anki-templates/README.md +39 -0
- package/.opencode/mednotes/docs/anki-templates/cloze.back.html +23 -0
- package/.opencode/mednotes/docs/anki-templates/cloze.front.html +14 -0
- package/.opencode/mednotes/docs/anki-templates/qa.back.html +24 -0
- package/.opencode/mednotes/docs/anki-templates/qa.front.html +14 -0
- package/.opencode/mednotes/docs/anki-templates/style.css +182 -0
- package/.opencode/mednotes/docs/atomicity-splitting-policy.md +113 -0
- package/.opencode/mednotes/docs/extension-docs.md +40 -0
- package/.opencode/mednotes/docs/flashcard-ingestion.md +278 -0
- package/.opencode/mednotes/docs/knowledge-architect.md +208 -0
- package/.opencode/mednotes/docs/merge-policy.md +110 -0
- package/.opencode/mednotes/docs/public-vocabulary.md +104 -0
- package/.opencode/mednotes/docs/semantic-linker.md +141 -0
- package/.opencode/mednotes/docs/taxonomy-policy.md +90 -0
- package/.opencode/mednotes/docs/triage-policy.md +187 -0
- package/.opencode/mednotes/docs/vault-version-control.md +758 -0
- package/.opencode/mednotes/docs/vocabulary-db-recovery.md +58 -0
- package/.opencode/mednotes/docs/workflow-output-contract.md +779 -0
- package/.opencode/mednotes/hooks/hooks.json +79 -0
- package/.opencode/mednotes/package-lock.json +6361 -0
- package/.opencode/mednotes/package.json +15 -0
- package/.opencode/mednotes/pyproject.toml +48 -0
- package/.opencode/mednotes/scripts/bootstrap_windows_python_uv.cmd +13 -0
- package/.opencode/mednotes/scripts/bootstrap_windows_python_uv.ps1 +172 -0
- package/.opencode/mednotes/scripts/enrich_notes.py +23 -0
- package/.opencode/mednotes/scripts/full_reset_windows_python_uv.cmd +13 -0
- package/.opencode/mednotes/scripts/hooks/antigravity_hook_status.mjs +212 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/antigravity.mjs +169 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/harness_payload.mjs +103 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/opencode_plugin.mjs +341 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/opencode_user_config_sync.mjs +177 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/anki_preflight.mjs +214 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/cli.mjs +143 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/diagnostics.mjs +11 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/domain/agent_directive_core.mjs +160 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/fsm_directive.mjs +1470 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/hook_errors.mjs +120 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/retention.mjs +114 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/runtime.mjs +174 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/telemetry_capture.mjs +511 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/vault_guard.mjs +624 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook.mjs +5 -0
- package/.opencode/mednotes/scripts/mednotes/_runtime_paths.py +24 -0
- package/.opencode/mednotes/scripts/mednotes/anki_model_validator.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/capture_extension_diff.py +1562 -0
- package/.opencode/mednotes/scripts/mednotes/feedback_report.py +16 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_index.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_pipeline.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_report.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_sources.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/obsidian/README.md +6 -0
- package/.opencode/mednotes/scripts/mednotes/obsidian_note_utils.py +20 -0
- package/.opencode/mednotes/scripts/mednotes/pdf_library/cli.py +16 -0
- package/.opencode/mednotes/scripts/mednotes/project_fsm.py +229 -0
- package/.opencode/mednotes/scripts/mednotes/setup_telemetry_email.py +404 -0
- package/.opencode/mednotes/scripts/mednotes/sync_anki_twenty_rules.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/sync_opencode_user_config.py +36 -0
- package/.opencode/mednotes/scripts/mednotes/wiki/cli.py +20 -0
- package/.opencode/mednotes/scripts/mednotes/wiki_graph.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/wiki_tree.py +134 -0
- package/.opencode/mednotes/scripts/reset_windows_python_uv.ps1 +625 -0
- package/.opencode/mednotes/scripts/run_python.mjs +109 -0
- package/.opencode/mednotes/scripts/vault/vault_commit.ps1 +19 -0
- package/.opencode/mednotes/scripts/vault/vault_commit.sh +18 -0
- package/.opencode/mednotes/scripts/vault/vault_git.ps1 +19 -0
- package/.opencode/mednotes/scripts/vault/vault_git.py +3107 -0
- package/.opencode/mednotes/scripts/vault/vault_git.sh +18 -0
- package/.opencode/mednotes/scripts/vault/vault_precommit.ps1 +19 -0
- package/.opencode/mednotes/scripts/vault/vault_precommit.sh +18 -0
- package/.opencode/mednotes/skills/THIRD_PARTY_NOTICES.md +45 -0
- package/.opencode/mednotes/skills/create-medical-flashcards/SKILL.md +113 -0
- package/.opencode/mednotes/skills/create-medical-note/SKILL.md +90 -0
- package/.opencode/mednotes/skills/enrich-medical-note/SKILL.md +120 -0
- package/.opencode/mednotes/skills/fix-medical-wiki/SKILL.md +559 -0
- package/.opencode/mednotes/skills/link-medical-wiki/SKILL.md +224 -0
- package/.opencode/mednotes/skills/obsidian-cli/SKILL.md +118 -0
- package/.opencode/mednotes/skills/obsidian-markdown/SKILL.md +207 -0
- package/.opencode/mednotes/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
- package/.opencode/mednotes/skills/obsidian-markdown/references/EMBEDS.md +63 -0
- package/.opencode/mednotes/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
- package/.opencode/mednotes/skills/obsidian-ops/SKILL.md +136 -0
- package/.opencode/mednotes/skills/pdf-library/SKILL.md +45 -0
- package/.opencode/mednotes/skills/process-medical-chats/SKILL.md +246 -0
- package/.opencode/mednotes/skills/workflow-report/SKILL.md +100 -0
- package/.opencode/mednotes/src/mednotes/__init__.py +5 -0
- package/.opencode/mednotes/src/mednotes/domains/__init__.py +5 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/README.md +26 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/__init__.py +2 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/build_demo_apkg.py +177 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/contracts.py +385 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/flashcards_machine.py +522 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/fsm.py +817 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/index.py +630 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/install_models.py +445 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/model.py +359 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/obsidian_links.py +135 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/obsidian_note_utils.py +546 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/pipeline.py +580 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/report.py +510 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/sources.py +682 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/sync_rules.py +184 -0
- package/.opencode/mednotes/src/mednotes/domains/history/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/history/history_fsm.py +852 -0
- package/.opencode/mednotes/src/mednotes/domains/history/history_machine.py +453 -0
- package/.opencode/mednotes/src/mednotes/domains/setup/__init__.py +7 -0
- package/.opencode/mednotes/src/mednotes/domains/setup/setup_fsm.py +808 -0
- package/.opencode/mednotes/src/mednotes/domains/setup/setup_machine.py +973 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/README.md +64 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/api.py +668 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/batch_state.py +102 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/atomicity/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/atomicity/atomicity.py +877 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/body_link/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/body_link/body_linker.py +1562 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/effect_adapters.py +949 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/fix_wiki_runtime_adapters.py +433 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/coverage.py +413 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/graph.py +396 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/graph_fixes.py +161 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/hygiene/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/hygiene/hygiene.py +483 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/__init__.py +2 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/anchors.py +185 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/__init__.py +0 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/cache.py +223 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/config.py +131 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/download.py +224 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/frontmatter.py +59 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/insert.py +227 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/local_import.py +54 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/__init__.py +42 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/web_profiles.py +99 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/web_search.py +203 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/wikimedia.py +102 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_db_adapter.mjs +434 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_node_runtime.py +274 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_query.py +227 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/artifacts.py +605 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/canonical_merge.py +277 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/markdown_zones.py +85 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/meaning_planner.py +307 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_iter.py +67 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_merge.py +278 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_plan.py +409 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_policy.py +22 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/__init__.py +79 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/fixes.py +264 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/frontmatter.py +435 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/models.py +208 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/prompts.py +37 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/tables.py +236 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/validate.py +404 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/provenance.py +478 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/raw_chats.py +273 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/sources_backfill.py +235 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/__init__.py +10 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/anchors.py +16 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/captions.py +47 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/cli.py +179 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/cloud.py +52 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/config.py +196 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/context_packets.py +76 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/db.py +81 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/doctor.py +102 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/figure_ids.py +42 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/ingest.py +326 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/insert.py +316 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/mentions.py +57 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/ocr.py +71 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/paths.py +35 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/pdf_engine.py +77 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/schema.py +155 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/search.py +188 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/app.py +89 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/image_backend.py +29 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/state.py +65 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish.py +1139 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish_receipts.py +365 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish_recovery.py +240 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_behavior_corpus.py +2069 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_report_validation.py +4448 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_run_audit.py +852 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/architect_prompt_eval.py +341 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/body_linker_eval.py +240 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/curator_output_validation.py +175 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/curator_prompt_eval.py +865 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/triager_prompt_eval.py +1295 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/related_notes.py +1920 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/related_notes_headless.py +1186 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/plan_attestation.py +148 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_receipts.py +360 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_runtime.py +52 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_task_runner.py +2470 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/style/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/style/style.py +1952 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/subagents/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/subagents/agents.py +1767 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/alias_projection.py +331 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/link_terms.py +151 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/llm_disambiguation.py +182 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/__init__.py +116 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/audit.py +201 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/migration.py +314 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/normalize.py +72 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/policy.py +135 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/resolve.py +413 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/schema.py +157 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/status.py +137 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_bootstrap.py +509 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_curator_batch.py +1115 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_ingestion.py +632 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_map.py +930 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_recovery.py +1388 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/cli.py +6665 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/common.py +69 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/config.py +210 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/__init__.py +74 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agent_report.py +242 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agent_run_audit.py +196 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agents.py +601 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/curator.py +256 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/effect_payloads.py +519 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/happy_path.py +190 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/link_git.py +110 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/link_runtime_artifact.py +52 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/note_plan.py +75 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/paths.py +114 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/public_report.py +53 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/publish.py +111 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/raw_coverage.py +217 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes.py +136 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes_headless.py +153 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes_runtime.py +395 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/schema_registry.py +637 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/specialist.py +432 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/status.py +62 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/style_rewrite.py +568 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/vocabulary_ingestion.py +223 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_blockers.py +510 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_guardrails.py +637 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_outcomes.py +121 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_receipts.py +100 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/__main__.py +4 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/cli.py +275 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/__init__.py +2 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/candidates.py +193 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/cli.py +189 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/gemini.py +220 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/inputs.py +120 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/models.py +34 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/parsing.py +48 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/prompts.py +216 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/quality.py +54 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/reporting.py +24 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/runner.py +433 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/utils.py +39 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/vault_guard_bridge.py +17 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_context_packets.py +454 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_decision_projection.py +133 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_effects.py +1260 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_fsm.py +2768 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_machine.py +1588 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_plan.py +306 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_primary_objective.py +316 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_problem.py +153 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_receipt_evidence.py +306 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_states.py +290 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_user_report.py +342 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/health.py +6332 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_fsm.py +1119 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_git.py +638 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_machine.py +1106 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_retry_governance.py +374 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_runtime_result.py +485 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_triggers.py +183 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/linking.py +2758 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/reference_repair.py +718 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/related_notes_fsm.py +1855 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link_related/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link_related/link_related_machine.py +834 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_fsm.py +1592 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_machine.py +3097 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_primary_objective.py +28 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_runtime_result.py +185 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/performance.py +97 -0
- package/.opencode/mednotes/src/mednotes/kernel/__init__.py +6 -0
- package/.opencode/mednotes/src/mednotes/kernel/agent_directive.py +336 -0
- package/.opencode/mednotes/src/mednotes/kernel/base.py +51 -0
- package/.opencode/mednotes/src/mednotes/kernel/blockers.py +39 -0
- package/.opencode/mednotes/src/mednotes/kernel/effect_executor.py +55 -0
- package/.opencode/mednotes/src/mednotes/kernel/effect_intent.py +69 -0
- package/.opencode/mednotes/src/mednotes/kernel/effects.py +160 -0
- package/.opencode/mednotes/src/mednotes/kernel/errors.py +38 -0
- package/.opencode/mednotes/src/mednotes/kernel/fsm_event.py +35 -0
- package/.opencode/mednotes/src/mednotes/kernel/fsm_model.py +55 -0
- package/.opencode/mednotes/src/mednotes/kernel/fsm_transition_result.py +75 -0
- package/.opencode/mednotes/src/mednotes/kernel/guardrails.py +188 -0
- package/.opencode/mednotes/src/mednotes/kernel/progress.py +319 -0
- package/.opencode/mednotes/src/mednotes/kernel/public_report.py +346 -0
- package/.opencode/mednotes/src/mednotes/kernel/state_machine.py +164 -0
- package/.opencode/mednotes/src/mednotes/kernel/workflow.py +619 -0
- package/.opencode/mednotes/src/mednotes/platform/__init__.py +5 -0
- package/.opencode/mednotes/src/mednotes/platform/backup_policy.py +382 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/__init__.py +62 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/cli.py +275 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/contracts.py +83 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/core.py +4168 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/integrity.py +989 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/operational_contract.py +2293 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/telemetry.py +875 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/telemetry_config.py +65 -0
- package/.opencode/mednotes/src/mednotes/platform/opencode_runtime_config.py +182 -0
- package/.opencode/mednotes/src/mednotes/platform/paths/__init__.py +1560 -0
- package/.opencode/mednotes/src/mednotes/platform/secrets.py +89 -0
- package/.opencode/mednotes/src/mednotes/platform/user_config.py +103 -0
- package/.opencode/mednotes/src/mednotes/platform/vault_guard.py +214 -0
- package/.opencode/mednotes/uv.lock +932 -0
- package/.opencode/mednotes.generated.json +395 -0
- package/.opencode/opencode.json +31 -0
- package/.opencode/plugins/mednotes-fsm.mjs +7 -0
- package/.opencode/plugins/mednotes_hook/adapters/antigravity.mjs +169 -0
- package/.opencode/plugins/mednotes_hook/adapters/harness_payload.mjs +103 -0
- package/.opencode/plugins/mednotes_hook/adapters/opencode_plugin.mjs +341 -0
- package/.opencode/plugins/mednotes_hook/adapters/opencode_user_config_sync.mjs +177 -0
- package/.opencode/plugins/mednotes_hook/anki_preflight.mjs +214 -0
- package/.opencode/plugins/mednotes_hook/cli.mjs +143 -0
- package/.opencode/plugins/mednotes_hook/diagnostics.mjs +11 -0
- package/.opencode/plugins/mednotes_hook/domain/agent_directive_core.mjs +160 -0
- package/.opencode/plugins/mednotes_hook/fsm_directive.mjs +1470 -0
- package/.opencode/plugins/mednotes_hook/hook_errors.mjs +120 -0
- package/.opencode/plugins/mednotes_hook/retention.mjs +114 -0
- package/.opencode/plugins/mednotes_hook/runtime.mjs +174 -0
- package/.opencode/plugins/mednotes_hook/telemetry_capture.mjs +511 -0
- package/.opencode/plugins/mednotes_hook/vault_guard.mjs +624 -0
- package/AGENTS.md +57 -0
- package/README.md +194 -0
- package/adapters/antigravity/agents.json +80 -0
- package/adapters/antigravity/templates/med-chat-triager.md +214 -0
- package/adapters/antigravity/templates/med-flashcard-maker.md +72 -0
- package/adapters/antigravity/templates/med-knowledge-architect.md +241 -0
- package/adapters/antigravity/templates/med-link-graph-curator.md +187 -0
- package/adapters/antigravity/templates/med-publish-guard.md +71 -0
- package/adapters/gemini-cli/gemini-extension.json +14 -0
- package/adapters/gemini-cli/package.json +15 -0
- package/adapters/gemini-cli/pyproject.toml +48 -0
- package/bin/mednotes-opencode.mjs +155 -0
- package/contracts/agents.json +116 -0
- package/core/agents/med-chat-triager.md +197 -0
- package/core/agents/med-flashcard-maker.md +56 -0
- package/core/agents/med-knowledge-architect.md +224 -0
- package/core/agents/med-link-graph-curator.md +171 -0
- package/core/agents/med-publish-guard.md +55 -0
- package/core/commands/flashcards.toml +22 -0
- package/core/commands/mednotes/create.toml +22 -0
- package/core/commands/mednotes/enrich.toml +24 -0
- package/core/commands/mednotes/fix-wiki.toml +24 -0
- package/core/commands/mednotes/history.toml +19 -0
- package/core/commands/mednotes/link-body.toml +22 -0
- package/core/commands/mednotes/link-related.toml +24 -0
- package/core/commands/mednotes/link.toml +24 -0
- package/core/commands/mednotes/pdf-library.toml +24 -0
- package/core/commands/mednotes/process-chats.toml +20 -0
- package/core/commands/mednotes/setup.toml +18 -0
- package/core/commands/mednotes/status.toml +24 -0
- package/core/commands/mednotes/telemetry.toml +24 -0
- package/core/commands/report.toml +23 -0
- package/core/skills/THIRD_PARTY_NOTICES.md +45 -0
- package/core/skills/create-medical-flashcards/SKILL.md +113 -0
- package/core/skills/create-medical-note/SKILL.md +90 -0
- package/core/skills/enrich-medical-note/SKILL.md +120 -0
- package/core/skills/fix-medical-wiki/SKILL.md +559 -0
- package/core/skills/link-medical-wiki/SKILL.md +224 -0
- package/core/skills/obsidian-cli/SKILL.md +118 -0
- package/core/skills/obsidian-markdown/SKILL.md +207 -0
- package/core/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
- package/core/skills/obsidian-markdown/references/EMBEDS.md +63 -0
- package/core/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
- package/core/skills/obsidian-ops/SKILL.md +136 -0
- package/core/skills/pdf-library/SKILL.md +45 -0
- package/core/skills/process-medical-chats/SKILL.md +246 -0
- package/core/skills/workflow-report/SKILL.md +100 -0
- 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
|
+
)
|