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,1139 @@
|
|
|
1
|
+
"""Staging and publishing generated Wiki notes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pydantic import ConfigDict, Field
|
|
11
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
12
|
+
|
|
13
|
+
from mednotes.domains.wiki.batch_state import (
|
|
14
|
+
batch_state_from,
|
|
15
|
+
file_sha256,
|
|
16
|
+
merge_batch_state,
|
|
17
|
+
require_compatible_batch_state,
|
|
18
|
+
)
|
|
19
|
+
from mednotes.domains.wiki.capabilities.graph.coverage import (
|
|
20
|
+
validate_raw_coverage,
|
|
21
|
+
validate_raw_coverage_structure,
|
|
22
|
+
)
|
|
23
|
+
from mednotes.domains.wiki.capabilities.markdown.markdown_query import (
|
|
24
|
+
MarkdownQueryUnavailable,
|
|
25
|
+
ensure_markdown_query_available,
|
|
26
|
+
markdown_query_blocked_payload,
|
|
27
|
+
)
|
|
28
|
+
from mednotes.domains.wiki.capabilities.notes.artifacts import validate_artifact_batch, validate_note_artifacts
|
|
29
|
+
from mednotes.domains.wiki.capabilities.notes.note_style.frontmatter import FrontmatterYamlUnavailable
|
|
30
|
+
from mednotes.domains.wiki.capabilities.notes.provenance import _apply_note_provenance_from_raw_files
|
|
31
|
+
from mednotes.domains.wiki.capabilities.notes.raw_chats import atomic_write_text, mutate_raw_frontmatter
|
|
32
|
+
from mednotes.domains.wiki.capabilities.publish.publish_receipts import build_publish_receipt_payload
|
|
33
|
+
from mednotes.domains.wiki.capabilities.style.style import validate_wiki_note_contract
|
|
34
|
+
from mednotes.domains.wiki.capabilities.vocabulary.link_terms import normalize_key
|
|
35
|
+
from mednotes.domains.wiki.capabilities.vocabulary.taxonomy import (
|
|
36
|
+
_validate_taxonomy_not_title,
|
|
37
|
+
normalize_taxonomy,
|
|
38
|
+
resolve_target_for_note,
|
|
39
|
+
resolve_taxonomy,
|
|
40
|
+
safe_title,
|
|
41
|
+
)
|
|
42
|
+
from mednotes.domains.wiki.common import CollisionError, MedOpsError, MissingPathError, ValidationError, _now_iso
|
|
43
|
+
from mednotes.domains.wiki.config import MedConfig, _path
|
|
44
|
+
from mednotes.domains.wiki.contracts.publish import PublishManifest, PublishManifestBatch
|
|
45
|
+
from mednotes.domains.wiki.contracts.raw_coverage import (
|
|
46
|
+
RawCoveragePlanBatch,
|
|
47
|
+
RawCoverageSummary,
|
|
48
|
+
coverage_summary_from_batches,
|
|
49
|
+
)
|
|
50
|
+
from mednotes.domains.wiki.contracts.workflow_guardrails import (
|
|
51
|
+
PUBLISH_REQUIRED_INPUTS,
|
|
52
|
+
annotate_payload,
|
|
53
|
+
note_target_index,
|
|
54
|
+
)
|
|
55
|
+
from mednotes.domains.wiki.flows.process_chats.process_chats_machine import (
|
|
56
|
+
ProcessChatsErrorContext,
|
|
57
|
+
ProcessChatsPublishRuntimeObservation,
|
|
58
|
+
ProcessChatsState,
|
|
59
|
+
)
|
|
60
|
+
from mednotes.domains.wiki.flows.process_chats.process_chats_runtime_result import (
|
|
61
|
+
process_chats_fsm_payload_from_publish_result,
|
|
62
|
+
)
|
|
63
|
+
from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter, contract_error
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _ArtifactBatchValidationFields(ContractModel):
|
|
67
|
+
"""Typed lens for child artifact validation reports aggregated by publish."""
|
|
68
|
+
|
|
69
|
+
model_config = ConfigDict(extra="ignore", populate_by_name=True, validate_assignment=True)
|
|
70
|
+
|
|
71
|
+
required: bool = Field(default=False, strict=True)
|
|
72
|
+
manifest_count: int = Field(default=0, ge=0, strict=True)
|
|
73
|
+
artifact_count: int = Field(default=0, ge=0, strict=True)
|
|
74
|
+
covered_artifact_count: int = Field(default=0, ge=0, strict=True)
|
|
75
|
+
missing_artifact_count: int = Field(default=0, ge=0, strict=True)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _ProcessChatsPublishSafetyFields(ContractModel):
|
|
79
|
+
"""Typed view used to decide whether publish mutated the vault."""
|
|
80
|
+
|
|
81
|
+
model_config = ConfigDict(extra="ignore", populate_by_name=True, validate_assignment=True)
|
|
82
|
+
|
|
83
|
+
created: list[object] = Field(default_factory=list)
|
|
84
|
+
processed_raw_count: int = Field(default=0, ge=0, strict=True)
|
|
85
|
+
manifest_hash: str = ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _ProcessChatsRuntimeObservationErrorFields(ContractModel):
|
|
89
|
+
"""Typed lens from broad diagnostic context into FSM error context."""
|
|
90
|
+
|
|
91
|
+
model_config = ConfigDict(extra="ignore", populate_by_name=True, validate_assignment=True)
|
|
92
|
+
|
|
93
|
+
root_cause: str = ""
|
|
94
|
+
blocked_reason: str = ""
|
|
95
|
+
affected_artifact: str = ""
|
|
96
|
+
next_action: str = ""
|
|
97
|
+
suggested_fix: str = ""
|
|
98
|
+
retry_scope: str = ""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _publish_json_object(value: object, *, prefix: str) -> JsonObject:
|
|
102
|
+
try:
|
|
103
|
+
return JsonObjectAdapter.validate_python(value)
|
|
104
|
+
except PydanticValidationError as exc:
|
|
105
|
+
raise contract_error(exc, prefix=prefix) from exc
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def resolve_collision(path: Path, mode: str, reserved: set[Path]) -> Path:
|
|
109
|
+
if mode not in {"abort", "suffix"}:
|
|
110
|
+
raise ValidationError(f"Invalid collision mode: {mode}")
|
|
111
|
+
if mode == "abort":
|
|
112
|
+
if path.exists() or path in reserved:
|
|
113
|
+
raise CollisionError(f"Target note already exists: {path}")
|
|
114
|
+
return path
|
|
115
|
+
|
|
116
|
+
candidate = path
|
|
117
|
+
idx = 2
|
|
118
|
+
while candidate.exists() or candidate in reserved:
|
|
119
|
+
candidate = path.with_name(f"{path.stem} ({idx}){path.suffix}")
|
|
120
|
+
idx += 1
|
|
121
|
+
return candidate
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def write_new_note(path: Path, content: str, dry_run: bool = False, create_parent: bool = False) -> None:
|
|
125
|
+
if dry_run:
|
|
126
|
+
return
|
|
127
|
+
if path.exists():
|
|
128
|
+
raise CollisionError(f"Target note already exists: {path}")
|
|
129
|
+
if create_parent:
|
|
130
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
elif not path.parent.exists():
|
|
132
|
+
raise MissingPathError(f"Target taxonomy directory does not exist: {path.parent}")
|
|
133
|
+
fd, tmp_name = tempfile.mkstemp(prefix=f".{path.stem}.", suffix=".tmp", dir=str(path.parent))
|
|
134
|
+
tmp = Path(tmp_name)
|
|
135
|
+
try:
|
|
136
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="") as fh:
|
|
137
|
+
fh.write(content)
|
|
138
|
+
if path.exists():
|
|
139
|
+
raise CollisionError(f"Target note appeared during write: {path}")
|
|
140
|
+
os.replace(tmp, path)
|
|
141
|
+
finally:
|
|
142
|
+
if tmp.exists():
|
|
143
|
+
tmp.unlink()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _load_manifest(path: Path) -> JsonObject:
|
|
147
|
+
if not path.exists():
|
|
148
|
+
raise MissingPathError(f"Manifest not found: {path}")
|
|
149
|
+
try:
|
|
150
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
151
|
+
except json.JSONDecodeError as exc:
|
|
152
|
+
raise ValidationError(f"Invalid manifest JSON: {exc}") from exc
|
|
153
|
+
if not isinstance(data, dict):
|
|
154
|
+
raise ValidationError("Manifest must be a JSON object")
|
|
155
|
+
return _publish_json_object(data, prefix="publish manifest")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _publish_batch_id(manifest: Path) -> str:
|
|
159
|
+
try:
|
|
160
|
+
return file_sha256(manifest)
|
|
161
|
+
except OSError:
|
|
162
|
+
return str(manifest)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _blocked_publish_contract_receipt(
|
|
166
|
+
*,
|
|
167
|
+
manifest: Path,
|
|
168
|
+
root_cause: str,
|
|
169
|
+
blocked_reason: str | None = None,
|
|
170
|
+
error_summary: str,
|
|
171
|
+
next_action: str,
|
|
172
|
+
) -> JsonObject:
|
|
173
|
+
blocked = blocked_reason or root_cause
|
|
174
|
+
error_context = _publish_json_object({
|
|
175
|
+
"phase": "publish_dry_run",
|
|
176
|
+
"blocked_reason": blocked,
|
|
177
|
+
"root_cause": root_cause,
|
|
178
|
+
"affected_artifact": str(manifest),
|
|
179
|
+
"error_summary": error_summary,
|
|
180
|
+
"suggested_fix": next_action,
|
|
181
|
+
"next_action": next_action,
|
|
182
|
+
"retry_scope": "recreate_publish_manifest_then_retry",
|
|
183
|
+
}, prefix="publish blocked receipt")
|
|
184
|
+
return _publish_json_object(annotate_payload(
|
|
185
|
+
{
|
|
186
|
+
"dry_run": True,
|
|
187
|
+
"backup": False,
|
|
188
|
+
"manifest": str(manifest),
|
|
189
|
+
"created": [],
|
|
190
|
+
"raw_updates": [],
|
|
191
|
+
"error_context": error_context,
|
|
192
|
+
"publish_receipt": build_publish_receipt_payload(
|
|
193
|
+
status="blocked",
|
|
194
|
+
batch_id=_publish_batch_id(manifest),
|
|
195
|
+
published_count=0,
|
|
196
|
+
skipped_count=0,
|
|
197
|
+
items=[],
|
|
198
|
+
next_action=next_action,
|
|
199
|
+
error_context=error_context,
|
|
200
|
+
),
|
|
201
|
+
"runtime_observation": _process_chats_runtime_observation_payload(
|
|
202
|
+
source_state=ProcessChatsState.NOTE_VALIDATION_RUNNING,
|
|
203
|
+
validation_coverage_gap=blocked == "coverage_path_missing",
|
|
204
|
+
validation_manifest_mismatch=blocked != "coverage_path_missing",
|
|
205
|
+
reason_code=blocked,
|
|
206
|
+
next_action=next_action,
|
|
207
|
+
manifest_path=str(manifest),
|
|
208
|
+
error_context=error_context,
|
|
209
|
+
),
|
|
210
|
+
},
|
|
211
|
+
phase="publish_dry_run",
|
|
212
|
+
status="blocked",
|
|
213
|
+
blocked_reason=blocked,
|
|
214
|
+
next_action=next_action,
|
|
215
|
+
required_inputs=PUBLISH_REQUIRED_INPUTS,
|
|
216
|
+
human_decision_required=False,
|
|
217
|
+
), prefix="publish blocked receipt")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _process_chats_runtime_observation_payload(
|
|
221
|
+
*,
|
|
222
|
+
source_state: ProcessChatsState,
|
|
223
|
+
preview_ready: bool = False,
|
|
224
|
+
publish_completed: bool = False,
|
|
225
|
+
link_completed: bool = False,
|
|
226
|
+
link_blocked: bool = False,
|
|
227
|
+
rollback_recorded: bool = False,
|
|
228
|
+
blocked: bool = False,
|
|
229
|
+
quota_wait: bool = False,
|
|
230
|
+
validation_coverage_gap: bool = False,
|
|
231
|
+
validation_manifest_mismatch: bool = False,
|
|
232
|
+
validation_content_invalid: bool = False,
|
|
233
|
+
publish_dry_run_receipt_required: bool = False,
|
|
234
|
+
publish_stale_receipt: bool = False,
|
|
235
|
+
publish_duplicate_target: bool = False,
|
|
236
|
+
publish_provenance_gap: bool = False,
|
|
237
|
+
reason_code: str = "",
|
|
238
|
+
next_action: str = "",
|
|
239
|
+
manifest_path: str = "",
|
|
240
|
+
dry_run_receipt_path: str = "",
|
|
241
|
+
receipt_id: str = "",
|
|
242
|
+
published_count: int = 0,
|
|
243
|
+
link_trigger_context_path: str = "",
|
|
244
|
+
link_receipt_id: str = "",
|
|
245
|
+
link_changed_files: Sequence[str] | None = None,
|
|
246
|
+
error_context: JsonObject | ProcessChatsErrorContext | None = None,
|
|
247
|
+
) -> JsonObject:
|
|
248
|
+
"""Build the canonical process-chats observation at the producer boundary."""
|
|
249
|
+
|
|
250
|
+
typed_error_context = (
|
|
251
|
+
_process_chats_runtime_error_context(error_context, fallback_artifact=manifest_path)
|
|
252
|
+
if isinstance(error_context, dict)
|
|
253
|
+
else error_context
|
|
254
|
+
)
|
|
255
|
+
return ProcessChatsPublishRuntimeObservation(
|
|
256
|
+
source_state=source_state,
|
|
257
|
+
preview_ready=preview_ready,
|
|
258
|
+
publish_completed=publish_completed,
|
|
259
|
+
link_completed=link_completed,
|
|
260
|
+
link_blocked=link_blocked,
|
|
261
|
+
rollback_recorded=rollback_recorded,
|
|
262
|
+
blocked=blocked,
|
|
263
|
+
quota_wait=quota_wait,
|
|
264
|
+
validation_coverage_gap=validation_coverage_gap,
|
|
265
|
+
validation_manifest_mismatch=validation_manifest_mismatch,
|
|
266
|
+
validation_content_invalid=validation_content_invalid,
|
|
267
|
+
publish_dry_run_receipt_required=publish_dry_run_receipt_required,
|
|
268
|
+
publish_stale_receipt=publish_stale_receipt,
|
|
269
|
+
publish_duplicate_target=publish_duplicate_target,
|
|
270
|
+
publish_provenance_gap=publish_provenance_gap,
|
|
271
|
+
reason_code=reason_code,
|
|
272
|
+
next_action=next_action,
|
|
273
|
+
manifest_path=manifest_path,
|
|
274
|
+
dry_run_receipt_path=dry_run_receipt_path,
|
|
275
|
+
receipt_id=receipt_id,
|
|
276
|
+
published_count=published_count,
|
|
277
|
+
link_trigger_context_path=link_trigger_context_path,
|
|
278
|
+
link_receipt_id=link_receipt_id,
|
|
279
|
+
link_changed_files=list(link_changed_files or []),
|
|
280
|
+
error_context=typed_error_context,
|
|
281
|
+
).to_payload()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _process_chats_runtime_error_context(
|
|
285
|
+
error_context: JsonObject,
|
|
286
|
+
*,
|
|
287
|
+
fallback_artifact: str,
|
|
288
|
+
) -> ProcessChatsErrorContext:
|
|
289
|
+
"""Conform broad publish diagnostics to the strict FSM error context."""
|
|
290
|
+
|
|
291
|
+
fields = _ProcessChatsRuntimeObservationErrorFields.model_validate(error_context)
|
|
292
|
+
root_cause = fields.root_cause or fields.blocked_reason or "process_chats_blocked"
|
|
293
|
+
next_action = fields.next_action or fields.suggested_fix or "Retomar /mednotes:process-chats pela rota oficial."
|
|
294
|
+
return ProcessChatsErrorContext(
|
|
295
|
+
root_cause=root_cause,
|
|
296
|
+
affected_artifact=fields.affected_artifact or fallback_artifact or "process-chats",
|
|
297
|
+
retry_scope=fields.retry_scope or "process_chats_official_retry",
|
|
298
|
+
next_action=next_action,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _load_publish_manifest(path: Path) -> PublishManifest:
|
|
303
|
+
try:
|
|
304
|
+
return PublishManifest.model_validate(_load_manifest(path))
|
|
305
|
+
except PydanticValidationError as exc:
|
|
306
|
+
raise contract_error(exc, prefix="publish manifest") from exc
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _paths_match(left: str, right: Path) -> bool:
|
|
310
|
+
left_path = _path(left)
|
|
311
|
+
try:
|
|
312
|
+
return left_path.resolve() == right.resolve()
|
|
313
|
+
except OSError:
|
|
314
|
+
return str(left_path) == str(right)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _same_path(left: Path, right: Path) -> bool:
|
|
318
|
+
try:
|
|
319
|
+
return left.resolve() == right.resolve()
|
|
320
|
+
except OSError:
|
|
321
|
+
return str(left) == str(right)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _note_target_key(path: Path) -> str:
|
|
325
|
+
return normalize_key(path.stem)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _wiki_note_targets(wiki_dir: Path) -> dict[str, list[Path]]:
|
|
329
|
+
raw_targets = note_target_index(wiki_dir, as_relative=False)
|
|
330
|
+
return {key: [path for path in values if isinstance(path, Path)] for key, values in raw_targets.items()}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _display_path(path: Path, wiki_dir: Path) -> str:
|
|
334
|
+
try:
|
|
335
|
+
return path.relative_to(wiki_dir).as_posix()
|
|
336
|
+
except ValueError:
|
|
337
|
+
return str(path)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _validate_normalized_target_available(
|
|
341
|
+
target: Path,
|
|
342
|
+
wiki_dir: Path,
|
|
343
|
+
existing_targets: dict[str, list[Path]],
|
|
344
|
+
reserved_targets: dict[str, Path],
|
|
345
|
+
) -> None:
|
|
346
|
+
target_key = _note_target_key(target)
|
|
347
|
+
reserved = reserved_targets.get(target_key)
|
|
348
|
+
if reserved is not None and not _same_path(reserved, target):
|
|
349
|
+
raise CollisionError(
|
|
350
|
+
"Target note would duplicate another note in this publish batch after "
|
|
351
|
+
f"Obsidian target normalization: {_display_path(target, wiki_dir)} conflicts with "
|
|
352
|
+
f"{_display_path(reserved, wiki_dir)}"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
conflicts = [path for path in existing_targets.get(target_key, []) if not _same_path(path, target)]
|
|
356
|
+
if conflicts:
|
|
357
|
+
conflict_list = ", ".join(_display_path(path, wiki_dir) for path in conflicts[:5])
|
|
358
|
+
extra = "" if len(conflicts) <= 5 else f" and {len(conflicts) - 5} more"
|
|
359
|
+
raise CollisionError(
|
|
360
|
+
"Target note would duplicate an existing Obsidian target after accent/case "
|
|
361
|
+
f"normalization: {_display_path(target, wiki_dir)} conflicts with {conflict_list}{extra}. "
|
|
362
|
+
"Use the existing note or merge/rename before publishing."
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _manifest_note_count(manifest: PublishManifest) -> int:
|
|
367
|
+
return sum(len(batch.notes) for batch in manifest.batches)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _staged_manifest_counts(data: JsonObject, *, pending_note: bool = False) -> tuple[int, int]:
|
|
371
|
+
"""Count a manifest being staged before it is valid enough to publish."""
|
|
372
|
+
batches = data["batches"] if "batches" in data else None
|
|
373
|
+
if not isinstance(batches, list):
|
|
374
|
+
raise ValidationError("publish manifest must use canonical batches[]")
|
|
375
|
+
note_count = 1 if pending_note else 0
|
|
376
|
+
for batch in batches:
|
|
377
|
+
if not isinstance(batch, dict):
|
|
378
|
+
raise ValidationError("Each manifest batch must be an object")
|
|
379
|
+
notes = batch["notes"] if "notes" in batch else None
|
|
380
|
+
if not isinstance(notes, list):
|
|
381
|
+
raise ValidationError("manifest batch notes must be a list")
|
|
382
|
+
note_count += len(notes)
|
|
383
|
+
return note_count, len(batches)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _require_no_pending_human_decision(manifest: PublishManifest, *, label: str) -> None:
|
|
387
|
+
if manifest.pending_human_decision():
|
|
388
|
+
raise ValidationError(
|
|
389
|
+
f"human_decision_required: {label} contains pending human_decision_packet; "
|
|
390
|
+
"resolve the decision, update the manifest/note_plan, and rerun publish-batch --dry-run."
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _raw_files_from_summary(summary: JsonObject | RawCoverageSummary, primary_raw_file: Path) -> list[Path]:
|
|
395
|
+
values = summary.raw_files if isinstance(summary, RawCoverageSummary) else (
|
|
396
|
+
summary["raw_files"] if "raw_files" in summary else None
|
|
397
|
+
)
|
|
398
|
+
raw_files: list[Path] = []
|
|
399
|
+
if isinstance(values, list) and values:
|
|
400
|
+
raw_files = [_path(str(value)) for value in values if str(value).strip()]
|
|
401
|
+
else:
|
|
402
|
+
raw_files = [primary_raw_file]
|
|
403
|
+
seen: set[str] = set()
|
|
404
|
+
unique: list[Path] = []
|
|
405
|
+
for path in raw_files:
|
|
406
|
+
key = str(path)
|
|
407
|
+
if key in seen:
|
|
408
|
+
continue
|
|
409
|
+
seen.add(key)
|
|
410
|
+
if not path.exists():
|
|
411
|
+
raise MissingPathError(f"Raw file not found: {path}")
|
|
412
|
+
unique.append(path)
|
|
413
|
+
unique = unique or [primary_raw_file]
|
|
414
|
+
if not any(_same_path(path, primary_raw_file) for path in unique):
|
|
415
|
+
raise ValidationError("provenance_gap: raw_files must include the primary raw_file")
|
|
416
|
+
return unique
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _raw_files_from_batch(batch: PublishManifestBatch, primary_raw_file: Path) -> list[Path]:
|
|
420
|
+
if not batch.raw_files:
|
|
421
|
+
return [primary_raw_file]
|
|
422
|
+
return _raw_files_from_summary(
|
|
423
|
+
JsonObjectAdapter.validate_python({"raw_files": batch.raw_files}),
|
|
424
|
+
primary_raw_file,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _prepare_note_content(
|
|
429
|
+
content: str,
|
|
430
|
+
*,
|
|
431
|
+
title: str,
|
|
432
|
+
raw_files: list[Path],
|
|
433
|
+
coverage_summary: JsonObject | None = None,
|
|
434
|
+
) -> str:
|
|
435
|
+
try:
|
|
436
|
+
result = _apply_note_provenance_from_raw_files(
|
|
437
|
+
content,
|
|
438
|
+
raw_files=raw_files,
|
|
439
|
+
title=title,
|
|
440
|
+
coverage_summary=coverage_summary,
|
|
441
|
+
)
|
|
442
|
+
except FrontmatterYamlUnavailable as exc:
|
|
443
|
+
raise ValidationError(f"{exc.blocked_reason}: {exc.next_action}") from exc
|
|
444
|
+
except ValueError as exc:
|
|
445
|
+
raise ValidationError(f"chat_provenance_invalid: {exc}") from exc
|
|
446
|
+
return str(result["text"])
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _artifact_batch_for_raw_files(
|
|
450
|
+
artifact_note_inputs: list[dict[str, str]],
|
|
451
|
+
*,
|
|
452
|
+
raw_files: list[Path],
|
|
453
|
+
artifact_dir: Path | None,
|
|
454
|
+
) -> JsonObject:
|
|
455
|
+
if len(raw_files) == 1:
|
|
456
|
+
return validate_artifact_batch(
|
|
457
|
+
artifact_note_inputs,
|
|
458
|
+
raw_file=raw_files[0],
|
|
459
|
+
artifact_dir=artifact_dir,
|
|
460
|
+
)
|
|
461
|
+
reports = [
|
|
462
|
+
validate_artifact_batch(
|
|
463
|
+
artifact_note_inputs,
|
|
464
|
+
raw_file=raw_file,
|
|
465
|
+
artifact_dir=artifact_dir,
|
|
466
|
+
)
|
|
467
|
+
for raw_file in raw_files
|
|
468
|
+
]
|
|
469
|
+
report_fields = [_ArtifactBatchValidationFields.model_validate(report) for report in reports]
|
|
470
|
+
return JsonObjectAdapter.validate_python({
|
|
471
|
+
"schema": "medical-notes-workbench.artifact-html-validation.multi-source.v1",
|
|
472
|
+
"scope": "multi_source_raw_chat_batch",
|
|
473
|
+
"required": any(report.required for report in report_fields),
|
|
474
|
+
"raw_file_count": len(raw_files),
|
|
475
|
+
"manifest_count": sum(report.manifest_count for report in report_fields),
|
|
476
|
+
"artifact_count": sum(report.artifact_count for report in report_fields),
|
|
477
|
+
"covered_artifact_count": sum(report.covered_artifact_count for report in report_fields),
|
|
478
|
+
"missing_artifact_count": sum(report.missing_artifact_count for report in report_fields),
|
|
479
|
+
"reports": reports,
|
|
480
|
+
"errors": [],
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _batch_for_stage(data: JsonObject, raw_file: Path) -> JsonObject:
|
|
485
|
+
raw_text = str(raw_file)
|
|
486
|
+
batches = data["batches"] if "batches" in data else None
|
|
487
|
+
if not isinstance(batches, list):
|
|
488
|
+
raise ValidationError("publish manifest must use canonical batches[]")
|
|
489
|
+
for batch in batches:
|
|
490
|
+
if not isinstance(batch, dict):
|
|
491
|
+
raise ValidationError("Each manifest batch must be an object")
|
|
492
|
+
if "raw_file" in batch and _paths_match(str(batch["raw_file"]), raw_file):
|
|
493
|
+
notes = batch["notes"] if "notes" in batch else None
|
|
494
|
+
if not isinstance(notes, list):
|
|
495
|
+
raise ValidationError("manifest batch notes must be a list")
|
|
496
|
+
validated = JsonObjectAdapter.validate_python(batch)
|
|
497
|
+
batch.clear()
|
|
498
|
+
batch.update(validated)
|
|
499
|
+
return batch
|
|
500
|
+
new_batch: JsonObject = {"raw_file": raw_text, "notes": []}
|
|
501
|
+
batches.append(new_batch)
|
|
502
|
+
return new_batch
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def plan_publish_batch(
|
|
506
|
+
manifest: PublishManifest,
|
|
507
|
+
config: MedConfig,
|
|
508
|
+
collision: str,
|
|
509
|
+
allow_new_taxonomy_leaf: bool = True,
|
|
510
|
+
require_coverage: bool = True,
|
|
511
|
+
) -> list[JsonObject]:
|
|
512
|
+
planned_batches: list[JsonObject] = []
|
|
513
|
+
reserved: set[Path] = set()
|
|
514
|
+
reserved_targets: dict[str, Path] = {}
|
|
515
|
+
existing_targets = _wiki_note_targets(config.wiki_dir)
|
|
516
|
+
_require_no_pending_human_decision(manifest, label="manifest")
|
|
517
|
+
for batch in manifest.batches:
|
|
518
|
+
batch_payload = batch.to_payload()
|
|
519
|
+
raw_file = _path(batch.raw_file)
|
|
520
|
+
if not raw_file.exists():
|
|
521
|
+
raise MissingPathError(f"Raw file not found: {raw_file}")
|
|
522
|
+
notes: list[JsonObject] = []
|
|
523
|
+
coverage_path_value = batch.coverage_path
|
|
524
|
+
if require_coverage and not coverage_path_value:
|
|
525
|
+
raise ValidationError(
|
|
526
|
+
"Manifest batch missing coverage_path; create an exhaustive raw coverage inventory "
|
|
527
|
+
"and stage notes with stage-note --coverage <coverage.json>"
|
|
528
|
+
)
|
|
529
|
+
coverage_structure: JsonObject | None = None
|
|
530
|
+
raw_files = _raw_files_from_batch(batch, raw_file)
|
|
531
|
+
if coverage_path_value:
|
|
532
|
+
coverage_path = _path(coverage_path_value)
|
|
533
|
+
coverage_structure = validate_raw_coverage_structure(
|
|
534
|
+
coverage_path,
|
|
535
|
+
raw_file,
|
|
536
|
+
require_triage_note_plan=require_coverage,
|
|
537
|
+
)
|
|
538
|
+
raw_files = _raw_files_from_summary(coverage_structure, raw_file)
|
|
539
|
+
artifact_note_inputs: list[dict[str, str]] = []
|
|
540
|
+
for item in batch.notes:
|
|
541
|
+
content_path = _path(item.content_path)
|
|
542
|
+
if not content_path.exists():
|
|
543
|
+
raise MissingPathError(f"Content file not found: {content_path}")
|
|
544
|
+
content = content_path.read_text(encoding="utf-8")
|
|
545
|
+
prepared_content = _prepare_note_content(
|
|
546
|
+
content,
|
|
547
|
+
title=item.title,
|
|
548
|
+
raw_files=raw_files,
|
|
549
|
+
coverage_summary=coverage_structure,
|
|
550
|
+
)
|
|
551
|
+
validate_wiki_note_contract(prepared_content, title=item.title, raw_file=raw_file)
|
|
552
|
+
artifact_validation = validate_note_artifacts(
|
|
553
|
+
prepared_content,
|
|
554
|
+
raw_file=raw_file,
|
|
555
|
+
artifact_dir=config.artifact_dir,
|
|
556
|
+
)
|
|
557
|
+
artifact_note_inputs.append(
|
|
558
|
+
{
|
|
559
|
+
"title": item.title,
|
|
560
|
+
"content_path": str(content_path),
|
|
561
|
+
"content": prepared_content,
|
|
562
|
+
}
|
|
563
|
+
)
|
|
564
|
+
target, taxonomy_resolution = resolve_target_for_note(
|
|
565
|
+
config.wiki_dir,
|
|
566
|
+
item.taxonomy,
|
|
567
|
+
item.title,
|
|
568
|
+
allow_new_taxonomy_leaf=allow_new_taxonomy_leaf,
|
|
569
|
+
)
|
|
570
|
+
target = resolve_collision(target, collision, reserved)
|
|
571
|
+
_validate_normalized_target_available(target, config.wiki_dir, existing_targets, reserved_targets)
|
|
572
|
+
reserved.add(target)
|
|
573
|
+
reserved_targets[_note_target_key(target)] = target
|
|
574
|
+
notes.append(
|
|
575
|
+
{
|
|
576
|
+
"taxonomy": taxonomy_resolution.taxonomy,
|
|
577
|
+
"taxonomy_requested": taxonomy_resolution.requested_taxonomy,
|
|
578
|
+
"taxonomy_canonicalized": list(taxonomy_resolution.canonicalized),
|
|
579
|
+
"taxonomy_new_dirs": list(taxonomy_resolution.new_dirs),
|
|
580
|
+
"title": item.title,
|
|
581
|
+
"content_path": str(content_path),
|
|
582
|
+
"target_path": str(target),
|
|
583
|
+
"artifact_validation": artifact_validation,
|
|
584
|
+
}
|
|
585
|
+
)
|
|
586
|
+
planned_batch: JsonObject = {
|
|
587
|
+
"raw_file": str(raw_file),
|
|
588
|
+
"raw_files": [str(path) for path in raw_files],
|
|
589
|
+
"notes": notes,
|
|
590
|
+
"artifact_validation": _artifact_batch_for_raw_files(
|
|
591
|
+
artifact_note_inputs,
|
|
592
|
+
raw_files=raw_files,
|
|
593
|
+
artifact_dir=config.artifact_dir,
|
|
594
|
+
),
|
|
595
|
+
}
|
|
596
|
+
if coverage_path_value:
|
|
597
|
+
coverage_path = _path(coverage_path_value)
|
|
598
|
+
planned_batch["coverage_path"] = str(coverage_path)
|
|
599
|
+
coverage_summary = validate_raw_coverage(
|
|
600
|
+
coverage_path,
|
|
601
|
+
raw_file,
|
|
602
|
+
[str(note["title"]) for note in notes],
|
|
603
|
+
require_triage_note_plan=require_coverage,
|
|
604
|
+
)
|
|
605
|
+
raw_files = _raw_files_from_summary(coverage_summary, raw_file)
|
|
606
|
+
planned_batch["raw_files"] = [str(path) for path in raw_files]
|
|
607
|
+
require_compatible_batch_state(
|
|
608
|
+
batch_payload,
|
|
609
|
+
coverage_summary,
|
|
610
|
+
left_label="manifest batch",
|
|
611
|
+
right_label="coverage inventory",
|
|
612
|
+
)
|
|
613
|
+
planned_batch["coverage"] = coverage_summary
|
|
614
|
+
coverage_state_basis = {**coverage_summary, **batch_payload}
|
|
615
|
+
if not coverage_state_basis.get("coverage_hash"):
|
|
616
|
+
coverage_state_basis["coverage_hash"] = file_sha256(coverage_path)
|
|
617
|
+
batch_state = batch_state_from(coverage_state_basis)
|
|
618
|
+
if batch_state:
|
|
619
|
+
planned_batch["batch_state"] = batch_state
|
|
620
|
+
planned_batches.append(planned_batch)
|
|
621
|
+
return planned_batches
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def taxonomy_new_leaf_authorization_from_plan(plan: Sequence[JsonObject]) -> JsonObject:
|
|
625
|
+
notes: list[JsonObject] = []
|
|
626
|
+
for batch in plan:
|
|
627
|
+
for item in batch.get("notes", []):
|
|
628
|
+
new_dirs = [str(value) for value in item.get("taxonomy_new_dirs", [])]
|
|
629
|
+
if not new_dirs:
|
|
630
|
+
continue
|
|
631
|
+
notes.append(
|
|
632
|
+
{
|
|
633
|
+
"target_path": str(item["target_path"]),
|
|
634
|
+
"taxonomy": str(item["taxonomy"]),
|
|
635
|
+
"taxonomy_requested": str(item.get("taxonomy_requested", "")),
|
|
636
|
+
"taxonomy_new_dirs": new_dirs,
|
|
637
|
+
}
|
|
638
|
+
)
|
|
639
|
+
return JsonObjectAdapter.validate_python({
|
|
640
|
+
"required": bool(notes),
|
|
641
|
+
"authorized_by_dry_run_receipt": bool(notes),
|
|
642
|
+
"note_count": len(notes),
|
|
643
|
+
"notes": notes,
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _raw_coverage_plan_batches(plan: Sequence[object]) -> list[RawCoveragePlanBatch]:
|
|
648
|
+
"""Validate publish's planned-batch projection before deriving coverage truth."""
|
|
649
|
+
|
|
650
|
+
try:
|
|
651
|
+
return [RawCoveragePlanBatch.model_validate(batch) for batch in plan]
|
|
652
|
+
except PydanticValidationError as exc:
|
|
653
|
+
raise contract_error(exc, prefix="publish.raw_coverage_plan_batch_invalid") from exc
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def coverage_summary_from_plan(plan: Sequence[object]) -> JsonObject:
|
|
657
|
+
return coverage_summary_from_batches(_raw_coverage_plan_batches(plan)).to_payload()
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def taxonomy_new_leaf_authorization_for_manifest(
|
|
661
|
+
manifest: Path,
|
|
662
|
+
config: MedConfig,
|
|
663
|
+
*,
|
|
664
|
+
collision: str = "abort",
|
|
665
|
+
allow_new_taxonomy_leaf: bool = True,
|
|
666
|
+
require_coverage: bool = True,
|
|
667
|
+
) -> JsonObject:
|
|
668
|
+
publish_manifest = _load_publish_manifest(manifest)
|
|
669
|
+
if require_coverage:
|
|
670
|
+
publish_manifest.require_coverage()
|
|
671
|
+
plan = plan_publish_batch(
|
|
672
|
+
publish_manifest,
|
|
673
|
+
config,
|
|
674
|
+
collision,
|
|
675
|
+
allow_new_taxonomy_leaf=allow_new_taxonomy_leaf,
|
|
676
|
+
require_coverage=require_coverage,
|
|
677
|
+
)
|
|
678
|
+
return taxonomy_new_leaf_authorization_from_plan(plan)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _missing_parent_dirs_before_write(target: Path, wiki_dir: Path) -> list[Path]:
|
|
682
|
+
missing: list[Path] = []
|
|
683
|
+
current = target.parent
|
|
684
|
+
while current != wiki_dir and not current.exists():
|
|
685
|
+
missing.append(current)
|
|
686
|
+
current = current.parent
|
|
687
|
+
return missing
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _unique_paths(paths: list[Path]) -> list[Path]:
|
|
691
|
+
seen: set[str] = set()
|
|
692
|
+
unique: list[Path] = []
|
|
693
|
+
for path in paths:
|
|
694
|
+
key = str(path)
|
|
695
|
+
if key in seen:
|
|
696
|
+
continue
|
|
697
|
+
seen.add(key)
|
|
698
|
+
unique.append(path)
|
|
699
|
+
return unique
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _rollback_publish_failure(
|
|
703
|
+
created_paths: list[Path],
|
|
704
|
+
raw_originals: dict[Path, str],
|
|
705
|
+
raw_restore_order: list[Path],
|
|
706
|
+
parent_dirs_to_prune: list[Path],
|
|
707
|
+
) -> dict[str, list[str]]:
|
|
708
|
+
rollback = {
|
|
709
|
+
"deleted_notes": [],
|
|
710
|
+
"restored_raw_files": [],
|
|
711
|
+
"removed_dirs": [],
|
|
712
|
+
"rollback_errors": [],
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
for path in reversed(created_paths):
|
|
716
|
+
try:
|
|
717
|
+
if path.exists():
|
|
718
|
+
path.unlink()
|
|
719
|
+
rollback["deleted_notes"].append(str(path))
|
|
720
|
+
except Exception as exc: # pragma: no cover - exercised only on OS-level rollback failures
|
|
721
|
+
rollback["rollback_errors"].append(f"delete note {path}: {exc}")
|
|
722
|
+
|
|
723
|
+
for raw_file in reversed(_unique_paths(raw_restore_order)):
|
|
724
|
+
original = raw_originals.get(raw_file)
|
|
725
|
+
if original is None:
|
|
726
|
+
continue
|
|
727
|
+
try:
|
|
728
|
+
atomic_write_text(raw_file, original)
|
|
729
|
+
rollback["restored_raw_files"].append(str(raw_file))
|
|
730
|
+
except Exception as exc: # pragma: no cover - exercised only on OS-level rollback failures
|
|
731
|
+
rollback["rollback_errors"].append(f"restore raw chat {raw_file}: {exc}")
|
|
732
|
+
|
|
733
|
+
dirs = sorted(_unique_paths(parent_dirs_to_prune), key=lambda item: len(item.parts), reverse=True)
|
|
734
|
+
for directory in dirs:
|
|
735
|
+
try:
|
|
736
|
+
if directory.exists() and directory.is_dir() and not any(directory.iterdir()):
|
|
737
|
+
directory.rmdir()
|
|
738
|
+
rollback["removed_dirs"].append(str(directory))
|
|
739
|
+
except Exception as exc: # pragma: no cover - exercised only on OS-level rollback failures
|
|
740
|
+
rollback["rollback_errors"].append(f"remove directory {directory}: {exc}")
|
|
741
|
+
|
|
742
|
+
return rollback
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _format_rollback_message(exc: Exception, rollback: dict[str, list[str]]) -> str:
|
|
746
|
+
return (
|
|
747
|
+
"Batch publish failed; automatic rollback attempted. "
|
|
748
|
+
f"Original error: {exc}. "
|
|
749
|
+
f"Deleted notes: {rollback['deleted_notes']}. "
|
|
750
|
+
f"Restored raw chats: {rollback['restored_raw_files']}. "
|
|
751
|
+
f"Removed empty directories: {rollback['removed_dirs']}. "
|
|
752
|
+
f"Rollback errors: {rollback['rollback_errors']}."
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def publish_batch(
|
|
757
|
+
manifest: Path,
|
|
758
|
+
config: MedConfig,
|
|
759
|
+
collision: str = "abort",
|
|
760
|
+
dry_run: bool = False,
|
|
761
|
+
backup: bool = False,
|
|
762
|
+
allow_new_taxonomy_leaf: bool = True,
|
|
763
|
+
require_coverage: bool = True,
|
|
764
|
+
) -> JsonObject:
|
|
765
|
+
result = publish_batch_operation_result(
|
|
766
|
+
manifest,
|
|
767
|
+
config,
|
|
768
|
+
collision=collision,
|
|
769
|
+
dry_run=dry_run,
|
|
770
|
+
backup=backup,
|
|
771
|
+
allow_new_taxonomy_leaf=allow_new_taxonomy_leaf,
|
|
772
|
+
require_coverage=require_coverage,
|
|
773
|
+
)
|
|
774
|
+
return process_chats_fsm_payload_from_publish_result(
|
|
775
|
+
result,
|
|
776
|
+
run_id=_process_chats_run_id(manifest, result),
|
|
777
|
+
version_control_safety=_process_chats_version_control_safety(result, applying=not dry_run),
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def publish_batch_operation_result(
|
|
782
|
+
manifest: Path,
|
|
783
|
+
config: MedConfig,
|
|
784
|
+
collision: str = "abort",
|
|
785
|
+
dry_run: bool = False,
|
|
786
|
+
backup: bool = False,
|
|
787
|
+
allow_new_taxonomy_leaf: bool = True,
|
|
788
|
+
require_coverage: bool = True,
|
|
789
|
+
) -> JsonObject:
|
|
790
|
+
backup = False
|
|
791
|
+
try:
|
|
792
|
+
typed_manifest = _load_publish_manifest(manifest)
|
|
793
|
+
if require_coverage:
|
|
794
|
+
typed_manifest.require_coverage()
|
|
795
|
+
except (PydanticValidationError, ValidationError, ValueError) as exc:
|
|
796
|
+
blocked_reason = "coverage_path_missing" if "coverage_path" in str(exc) else "manifest_invalid"
|
|
797
|
+
return _blocked_publish_contract_receipt(
|
|
798
|
+
manifest=manifest,
|
|
799
|
+
root_cause="publish.manifest_contract_invalid",
|
|
800
|
+
blocked_reason=blocked_reason,
|
|
801
|
+
error_summary=str(exc),
|
|
802
|
+
next_action="Recriar manifest, note_plan e coverage pela rota oficial antes de publicar.",
|
|
803
|
+
)
|
|
804
|
+
if not dry_run:
|
|
805
|
+
try:
|
|
806
|
+
ensure_markdown_query_available(
|
|
807
|
+
wiki_dir=config.wiki_dir,
|
|
808
|
+
raw_dir=config.raw_dir,
|
|
809
|
+
state_dir=config.state_dir,
|
|
810
|
+
)
|
|
811
|
+
except MarkdownQueryUnavailable as exc:
|
|
812
|
+
error_context = {
|
|
813
|
+
"blocked_reason": exc.blocked_reason,
|
|
814
|
+
"root_cause": "markdown_query_index_unavailable",
|
|
815
|
+
"affected_artifact": "markdown_query_index",
|
|
816
|
+
"error_summary": str(exc),
|
|
817
|
+
"suggested_fix": exc.next_action,
|
|
818
|
+
"next_action": exc.next_action,
|
|
819
|
+
"retry_scope": "setup_markdown_query_index_then_retry",
|
|
820
|
+
"details": exc.payload,
|
|
821
|
+
}
|
|
822
|
+
return annotate_payload(
|
|
823
|
+
{
|
|
824
|
+
**markdown_query_blocked_payload(
|
|
825
|
+
phase="publish_apply",
|
|
826
|
+
required_inputs=PUBLISH_REQUIRED_INPUTS,
|
|
827
|
+
),
|
|
828
|
+
"error_context": error_context,
|
|
829
|
+
"publish_receipt": build_publish_receipt_payload(
|
|
830
|
+
status="blocked",
|
|
831
|
+
batch_id=_publish_batch_id(manifest),
|
|
832
|
+
published_count=0,
|
|
833
|
+
skipped_count=0,
|
|
834
|
+
items=[],
|
|
835
|
+
next_action=exc.next_action,
|
|
836
|
+
error_context=error_context,
|
|
837
|
+
),
|
|
838
|
+
"runtime_observation": _process_chats_runtime_observation_payload(
|
|
839
|
+
source_state=ProcessChatsState.PUBLISH_APPLY_REQUESTED,
|
|
840
|
+
blocked=True,
|
|
841
|
+
publish_stale_receipt=True,
|
|
842
|
+
reason_code=exc.blocked_reason,
|
|
843
|
+
next_action=exc.next_action,
|
|
844
|
+
manifest_path=str(manifest),
|
|
845
|
+
receipt_id=_publish_batch_id(manifest),
|
|
846
|
+
error_context=error_context,
|
|
847
|
+
),
|
|
848
|
+
},
|
|
849
|
+
phase="publish_apply",
|
|
850
|
+
status="blocked",
|
|
851
|
+
blocked_reason=exc.blocked_reason,
|
|
852
|
+
next_action=exc.next_action,
|
|
853
|
+
required_inputs=PUBLISH_REQUIRED_INPUTS,
|
|
854
|
+
human_decision_required=False,
|
|
855
|
+
)
|
|
856
|
+
plan = plan_publish_batch(
|
|
857
|
+
typed_manifest,
|
|
858
|
+
config,
|
|
859
|
+
collision,
|
|
860
|
+
allow_new_taxonomy_leaf=allow_new_taxonomy_leaf,
|
|
861
|
+
require_coverage=require_coverage,
|
|
862
|
+
)
|
|
863
|
+
new_leaf_authorization = taxonomy_new_leaf_authorization_from_plan(plan)
|
|
864
|
+
created: list[str] = []
|
|
865
|
+
created_paths: list[Path] = []
|
|
866
|
+
parent_dirs_to_prune: list[Path] = []
|
|
867
|
+
raw_restore_order: list[Path] = []
|
|
868
|
+
raw_updates: list[JsonObject] = []
|
|
869
|
+
if dry_run:
|
|
870
|
+
return annotate_payload({
|
|
871
|
+
"dry_run": True,
|
|
872
|
+
"backup": backup,
|
|
873
|
+
"manifest": str(manifest),
|
|
874
|
+
"manifest_hash": file_sha256(manifest),
|
|
875
|
+
"allow_new_taxonomy_leaf": allow_new_taxonomy_leaf,
|
|
876
|
+
"require_coverage": require_coverage,
|
|
877
|
+
"batch_state": [batch["batch_state"] for batch in plan if batch.get("batch_state")],
|
|
878
|
+
"coverage_summary": coverage_summary_from_plan(plan),
|
|
879
|
+
"new_taxonomy_leaf_authorization": new_leaf_authorization,
|
|
880
|
+
"planned_batches": plan,
|
|
881
|
+
"created": [],
|
|
882
|
+
"raw_updates": [],
|
|
883
|
+
"publish_receipt": build_publish_receipt_payload(
|
|
884
|
+
status="ready_to_publish",
|
|
885
|
+
batch_id=_publish_batch_id(manifest),
|
|
886
|
+
published_count=0,
|
|
887
|
+
skipped_count=0,
|
|
888
|
+
items=[],
|
|
889
|
+
next_action="Revisar o plano e então rodar publish-batch sem --dry-run com o mesmo manifest.",
|
|
890
|
+
),
|
|
891
|
+
"runtime_observation": _process_chats_runtime_observation_payload(
|
|
892
|
+
source_state=ProcessChatsState.STAGING_MANIFEST_READY,
|
|
893
|
+
preview_ready=True,
|
|
894
|
+
reason_code="ready_to_publish",
|
|
895
|
+
next_action="Revisar o plano e então rodar publish-batch sem --dry-run com o mesmo manifest.",
|
|
896
|
+
manifest_path=str(manifest),
|
|
897
|
+
dry_run_receipt_path=str(manifest),
|
|
898
|
+
receipt_id=_publish_batch_id(manifest),
|
|
899
|
+
),
|
|
900
|
+
},
|
|
901
|
+
phase="publish_dry_run",
|
|
902
|
+
status="ready_to_publish",
|
|
903
|
+
next_action="Revisar o plano e então rodar publish-batch sem --dry-run com o mesmo manifest.",
|
|
904
|
+
required_inputs=PUBLISH_REQUIRED_INPUTS,
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
raw_files_to_update = _unique_paths(
|
|
908
|
+
[
|
|
909
|
+
Path(raw_file)
|
|
910
|
+
for batch in plan
|
|
911
|
+
for raw_file in (batch.get("raw_files") or [batch["raw_file"]])
|
|
912
|
+
]
|
|
913
|
+
)
|
|
914
|
+
raw_originals = {
|
|
915
|
+
raw_file: raw_file.read_text(encoding="utf-8")
|
|
916
|
+
for raw_file in raw_files_to_update
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
try:
|
|
920
|
+
for batch in plan:
|
|
921
|
+
batch_raw_files = [Path(path) for path in (batch.get("raw_files") or [batch["raw_file"]])]
|
|
922
|
+
coverage_value = batch.get("coverage")
|
|
923
|
+
coverage_summary = JsonObjectAdapter.validate_python(coverage_value) if isinstance(coverage_value, dict) else None
|
|
924
|
+
for item in batch["notes"]:
|
|
925
|
+
content = Path(item["content_path"]).read_text(encoding="utf-8")
|
|
926
|
+
prepared_content = _prepare_note_content(
|
|
927
|
+
content,
|
|
928
|
+
title=str(item["title"]),
|
|
929
|
+
raw_files=batch_raw_files,
|
|
930
|
+
coverage_summary=coverage_summary,
|
|
931
|
+
)
|
|
932
|
+
target_path = Path(item["target_path"])
|
|
933
|
+
parent_dirs_to_prune.extend(_missing_parent_dirs_before_write(target_path, config.wiki_dir))
|
|
934
|
+
write_new_note(target_path, prepared_content, create_parent=bool(item.get("taxonomy_new_dirs")))
|
|
935
|
+
created_paths.append(target_path)
|
|
936
|
+
created.append(item["target_path"])
|
|
937
|
+
for raw_file in raw_files_to_update:
|
|
938
|
+
raw_restore_order.append(raw_file)
|
|
939
|
+
raw_updates.append(
|
|
940
|
+
mutate_raw_frontmatter(
|
|
941
|
+
raw_file,
|
|
942
|
+
{"status": "processado", "processed_at": _now_iso()},
|
|
943
|
+
dry_run=False,
|
|
944
|
+
backup=backup,
|
|
945
|
+
)
|
|
946
|
+
)
|
|
947
|
+
except Exception as exc:
|
|
948
|
+
rollback = _rollback_publish_failure(
|
|
949
|
+
created_paths,
|
|
950
|
+
raw_originals,
|
|
951
|
+
raw_restore_order,
|
|
952
|
+
parent_dirs_to_prune,
|
|
953
|
+
)
|
|
954
|
+
raise MedOpsError(_format_rollback_message(exc, rollback)) from exc
|
|
955
|
+
|
|
956
|
+
return annotate_payload({
|
|
957
|
+
"dry_run": False,
|
|
958
|
+
"backup": backup,
|
|
959
|
+
"manifest": str(manifest),
|
|
960
|
+
"manifest_hash": file_sha256(manifest),
|
|
961
|
+
"allow_new_taxonomy_leaf": allow_new_taxonomy_leaf,
|
|
962
|
+
"require_coverage": require_coverage,
|
|
963
|
+
"batch_state": [batch["batch_state"] for batch in plan if batch.get("batch_state")],
|
|
964
|
+
"coverage_summary": coverage_summary_from_plan(plan),
|
|
965
|
+
"created": created,
|
|
966
|
+
"raw_updates": raw_updates,
|
|
967
|
+
"created_count": len(created),
|
|
968
|
+
"processed_raw_count": len(raw_updates),
|
|
969
|
+
"new_taxonomy_leaf_authorization": {
|
|
970
|
+
**new_leaf_authorization,
|
|
971
|
+
"authorized_by_dry_run_receipt": new_leaf_authorization["required"],
|
|
972
|
+
},
|
|
973
|
+
"publish_receipt": build_publish_receipt_payload(
|
|
974
|
+
status="published",
|
|
975
|
+
batch_id=_publish_batch_id(manifest),
|
|
976
|
+
published_count=len(created),
|
|
977
|
+
skipped_count=0,
|
|
978
|
+
items=[{"path": path, "status": "published"} for path in created],
|
|
979
|
+
next_action=(
|
|
980
|
+
"Rodar run-linker --diagnose para gerar o diagnóstico do grafo e, se seguro, "
|
|
981
|
+
"run-linker --apply --diagnosis <link-diagnosis.json>."
|
|
982
|
+
),
|
|
983
|
+
),
|
|
984
|
+
"runtime_observation": _process_chats_runtime_observation_payload(
|
|
985
|
+
source_state=ProcessChatsState.PUBLISH_APPLY_REQUESTED,
|
|
986
|
+
publish_completed=True,
|
|
987
|
+
reason_code="published",
|
|
988
|
+
next_action=(
|
|
989
|
+
"Rodar run-linker --diagnose para gerar o diagnóstico do grafo e, se seguro, "
|
|
990
|
+
"run-linker --apply --diagnosis <link-diagnosis.json>."
|
|
991
|
+
),
|
|
992
|
+
manifest_path=str(manifest),
|
|
993
|
+
receipt_id=_publish_batch_id(manifest),
|
|
994
|
+
published_count=len(created),
|
|
995
|
+
),
|
|
996
|
+
},
|
|
997
|
+
phase="publish_apply",
|
|
998
|
+
status="published",
|
|
999
|
+
next_action=(
|
|
1000
|
+
"Rodar run-linker --diagnose para gerar o diagnóstico do grafo e, se seguro, "
|
|
1001
|
+
"run-linker --apply --diagnosis <link-diagnosis.json>."
|
|
1002
|
+
),
|
|
1003
|
+
required_inputs=PUBLISH_REQUIRED_INPUTS,
|
|
1004
|
+
)
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def _process_chats_run_id(manifest: Path, result: JsonObject) -> str:
|
|
1008
|
+
fields = _ProcessChatsPublishSafetyFields.model_validate(result)
|
|
1009
|
+
basis = fields.manifest_hash
|
|
1010
|
+
if not basis:
|
|
1011
|
+
try:
|
|
1012
|
+
basis = file_sha256(manifest)
|
|
1013
|
+
except OSError:
|
|
1014
|
+
basis = manifest.name
|
|
1015
|
+
safe = "".join(ch if ch.isalnum() or ch in {"-", "_"} else "-" for ch in basis)[:48].strip("-")
|
|
1016
|
+
return f"process-chats-{safe or 'run'}"
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def _process_chats_version_control_safety(result: JsonObject, *, applying: bool) -> JsonObject:
|
|
1020
|
+
fields = _ProcessChatsPublishSafetyFields.model_validate(result)
|
|
1021
|
+
mutated = applying and (bool(fields.created) or fields.processed_raw_count > 0)
|
|
1022
|
+
return {
|
|
1023
|
+
"resource_guard_active": mutated,
|
|
1024
|
+
"run_start_seen": mutated,
|
|
1025
|
+
"run_finish_seen": mutated,
|
|
1026
|
+
"restore_point_before": "vault-guard" if mutated else "",
|
|
1027
|
+
"restore_point_after": "vault-guard" if mutated else "",
|
|
1028
|
+
"sync_status": "not_checked",
|
|
1029
|
+
"backup_online": "not_checked",
|
|
1030
|
+
"direct_mutation_forbidden": True,
|
|
1031
|
+
"mutation_without_guard": False,
|
|
1032
|
+
"rollback_declared": mutated,
|
|
1033
|
+
"no_resource_mutation": not mutated,
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def stage_note(
|
|
1038
|
+
manifest: Path,
|
|
1039
|
+
raw_file: Path,
|
|
1040
|
+
taxonomy: str,
|
|
1041
|
+
title: str,
|
|
1042
|
+
content_path: Path,
|
|
1043
|
+
dry_run: bool = False,
|
|
1044
|
+
config: MedConfig | None = None,
|
|
1045
|
+
allow_new_taxonomy_leaf: bool = True,
|
|
1046
|
+
coverage_path: Path | None = None,
|
|
1047
|
+
) -> JsonObject:
|
|
1048
|
+
taxonomy_resolution = (
|
|
1049
|
+
resolve_taxonomy(config.wiki_dir, taxonomy, title=title, allow_new_leaf=allow_new_taxonomy_leaf)
|
|
1050
|
+
if config is not None
|
|
1051
|
+
else None
|
|
1052
|
+
)
|
|
1053
|
+
canonical_taxonomy = taxonomy_resolution.taxonomy if taxonomy_resolution else "/".join(normalize_taxonomy(taxonomy))
|
|
1054
|
+
_validate_taxonomy_not_title(tuple(canonical_taxonomy.split("/")), title)
|
|
1055
|
+
filename = safe_title(title)
|
|
1056
|
+
if taxonomy_resolution is not None and config is not None:
|
|
1057
|
+
target = config.wiki_dir.joinpath(*taxonomy_resolution.parts, f"{filename}.md")
|
|
1058
|
+
_validate_normalized_target_available(target, config.wiki_dir, _wiki_note_targets(config.wiki_dir), {})
|
|
1059
|
+
if not raw_file.exists():
|
|
1060
|
+
raise MissingPathError(f"Raw file not found: {raw_file}")
|
|
1061
|
+
if not content_path.exists():
|
|
1062
|
+
raise MissingPathError(f"Content file not found: {content_path}")
|
|
1063
|
+
content = content_path.read_text(encoding="utf-8")
|
|
1064
|
+
if manifest.exists():
|
|
1065
|
+
data = _load_manifest(manifest)
|
|
1066
|
+
_load_publish_manifest(manifest)
|
|
1067
|
+
else:
|
|
1068
|
+
data = {"schema": "medical-notes-workbench.publish-manifest.v1", "batches": []}
|
|
1069
|
+
item = {
|
|
1070
|
+
"taxonomy": canonical_taxonomy,
|
|
1071
|
+
"title": title,
|
|
1072
|
+
"content_path": str(content_path),
|
|
1073
|
+
"safe_filename": f"{filename}.md",
|
|
1074
|
+
}
|
|
1075
|
+
batch = _batch_for_stage(data, raw_file)
|
|
1076
|
+
notes = batch["notes"]
|
|
1077
|
+
coverage_summary: JsonObject | None = None
|
|
1078
|
+
raw_files = [raw_file]
|
|
1079
|
+
if coverage_path is not None:
|
|
1080
|
+
coverage_summary = validate_raw_coverage_structure(coverage_path, raw_file)
|
|
1081
|
+
existing_coverage = batch.get("coverage_path")
|
|
1082
|
+
if existing_coverage and not _paths_match(str(existing_coverage), coverage_path):
|
|
1083
|
+
raise ValidationError(
|
|
1084
|
+
f"Manifest batch already has a different coverage_path: {existing_coverage}"
|
|
1085
|
+
)
|
|
1086
|
+
batch["coverage_path"] = str(coverage_path)
|
|
1087
|
+
raw_files = _raw_files_from_summary(coverage_summary, raw_file)
|
|
1088
|
+
batch["raw_files"] = [str(path) for path in raw_files]
|
|
1089
|
+
merge_batch_state(
|
|
1090
|
+
batch,
|
|
1091
|
+
coverage_summary,
|
|
1092
|
+
target_label="manifest batch",
|
|
1093
|
+
source_label="coverage inventory",
|
|
1094
|
+
)
|
|
1095
|
+
prepared_content = _prepare_note_content(
|
|
1096
|
+
content,
|
|
1097
|
+
title=title,
|
|
1098
|
+
raw_files=raw_files,
|
|
1099
|
+
coverage_summary=coverage_summary,
|
|
1100
|
+
)
|
|
1101
|
+
validate_wiki_note_contract(prepared_content, title=title, raw_file=raw_file)
|
|
1102
|
+
artifact_validation = validate_note_artifacts(
|
|
1103
|
+
prepared_content,
|
|
1104
|
+
raw_file=raw_file,
|
|
1105
|
+
artifact_dir=config.artifact_dir if config is not None else None,
|
|
1106
|
+
)
|
|
1107
|
+
if not dry_run:
|
|
1108
|
+
manifest.parent.mkdir(parents=True, exist_ok=True)
|
|
1109
|
+
notes.append(item)
|
|
1110
|
+
atomic_write_text(manifest, json.dumps(data, ensure_ascii=False, indent=2) + "\n")
|
|
1111
|
+
note_count, batch_count = _staged_manifest_counts(data, pending_note=dry_run)
|
|
1112
|
+
result: JsonObject = {
|
|
1113
|
+
"manifest": str(manifest),
|
|
1114
|
+
"dry_run": dry_run,
|
|
1115
|
+
"staged": item,
|
|
1116
|
+
"artifact_validation": artifact_validation,
|
|
1117
|
+
"note_count": note_count,
|
|
1118
|
+
"batch_count": batch_count,
|
|
1119
|
+
}
|
|
1120
|
+
if coverage_path is not None:
|
|
1121
|
+
result["coverage_path"] = str(coverage_path)
|
|
1122
|
+
if coverage_summary is not None:
|
|
1123
|
+
result["raw_files"] = [str(path) for path in _raw_files_from_summary(coverage_summary, raw_file)]
|
|
1124
|
+
batch_state = batch_state_from(coverage_summary)
|
|
1125
|
+
if batch_state:
|
|
1126
|
+
result["batch_state"] = batch_state
|
|
1127
|
+
if taxonomy_resolution is not None and config is not None:
|
|
1128
|
+
result["taxonomy_resolution"] = taxonomy_resolution.to_json(config.wiki_dir, title=title)
|
|
1129
|
+
return annotate_payload(
|
|
1130
|
+
result,
|
|
1131
|
+
phase="stage_note",
|
|
1132
|
+
status="preview_ready",
|
|
1133
|
+
next_action=(
|
|
1134
|
+
"Adicionar as demais notas/coberturas ao manifest antes do publish-batch --dry-run."
|
|
1135
|
+
if not dry_run
|
|
1136
|
+
else "Se a nota estiver correta, repetir stage-note sem --dry-run."
|
|
1137
|
+
),
|
|
1138
|
+
required_inputs=["raw_file", "taxonomy", "title", "content_path", "coverage_path"],
|
|
1139
|
+
)
|