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,1115 @@
|
|
|
1
|
+
"""Batch planning/apply helpers for med-link-graph-curator vocabulary work."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sqlite3
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import ConfigDict, Field
|
|
13
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
14
|
+
|
|
15
|
+
from mednotes.domains.wiki.capabilities.vocabulary.vocabulary_ingestion import apply_semantic_ingestion
|
|
16
|
+
from mednotes.domains.wiki.capabilities.vocabulary.vocabulary_map import initialize_vocabulary_db
|
|
17
|
+
from mednotes.domains.wiki.common import DOCS_RELPATH, ValidationError, wiki_cli_command
|
|
18
|
+
from mednotes.domains.wiki.contracts.curator import (
|
|
19
|
+
CuratorApplyReceipt,
|
|
20
|
+
CuratorBatchPlan,
|
|
21
|
+
CuratorIgnoredOutputNotice,
|
|
22
|
+
CuratorManifest,
|
|
23
|
+
CuratorManifestItem,
|
|
24
|
+
CuratorPromptEvalReport,
|
|
25
|
+
NoteSemanticIngestionOutput,
|
|
26
|
+
)
|
|
27
|
+
from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter, JsonValue, contract_error
|
|
28
|
+
from mednotes.platform.paths import extension_root as _resolve_extension_root
|
|
29
|
+
|
|
30
|
+
VOCABULARY_CURATOR_BATCH_PLAN_SCHEMA = "medical-notes-workbench.vocabulary-curator-batch-plan.v1"
|
|
31
|
+
VOCABULARY_CURATOR_BATCH_OUTPUT_MANIFEST_SCHEMA = (
|
|
32
|
+
"medical-notes-workbench.vocabulary-curator-batch-output-manifest.v1"
|
|
33
|
+
)
|
|
34
|
+
VOCABULARY_CURATOR_BATCH_RECEIPT_SCHEMA = "medical-notes-workbench.vocabulary-curator-batch-receipt.v1"
|
|
35
|
+
AGENT_WORK_PACKET_SCHEMA = "medical-notes-workbench.agent-work-packet.v1"
|
|
36
|
+
NOTE_SEMANTIC_INGESTION_SCHEMA = "medical-notes-workbench.note-semantic-ingestion.v1"
|
|
37
|
+
CURATOR_PROMPT_EVAL_SCHEMA = "medical-notes-workbench.curator-prompt-eval.v1"
|
|
38
|
+
CURATOR_PROMPT_IDENTITY_SCHEMA = "medical-notes-workbench.curator-prompt-identity.v1"
|
|
39
|
+
DEV_ESCAPE_ENV = "MEDNOTES_ALLOW_DEV_ESCAPE"
|
|
40
|
+
|
|
41
|
+
ALLOWED_CURATOR_ACTIONS = ["read_note", "write_semantic_ingestion_output", "defer_work_item"]
|
|
42
|
+
FORBIDDEN_CURATOR_ACTIONS = [
|
|
43
|
+
"direct_sql_mutation",
|
|
44
|
+
"markdown_edit",
|
|
45
|
+
"subagent_call",
|
|
46
|
+
"generated_write_script",
|
|
47
|
+
"manual_manifest_editing",
|
|
48
|
+
"hardcoded_local_path",
|
|
49
|
+
"mass_markdown_rewrite",
|
|
50
|
+
]
|
|
51
|
+
CURATOR_STOP_CONDITIONS = [
|
|
52
|
+
"schema_drift",
|
|
53
|
+
"sqlite_integrity_error",
|
|
54
|
+
"queue_inconsistent",
|
|
55
|
+
"path_mismatch",
|
|
56
|
+
"path_case_mismatch",
|
|
57
|
+
"content_hash_mismatch",
|
|
58
|
+
"timeout_or_max_turns",
|
|
59
|
+
"missing_official_command",
|
|
60
|
+
]
|
|
61
|
+
CURATOR_QUALITY_RUBRIC = {
|
|
62
|
+
"primary_meaning_atomicity": "primary_meaning must describe exactly one atomic medical concept represented by the note.",
|
|
63
|
+
"atomicity_signal": "non_atomic_note deferred work must include body-based semantic_signal; DB decides split_required vs candidate/defer.",
|
|
64
|
+
"alias_precision": "aliases must be medically useful, strict, and not broader than the note concept.",
|
|
65
|
+
"link_policy_conservatism": "use direct only for one surface, one meaning, one canonical note; otherwise requires_context/blocked/defer.",
|
|
66
|
+
"defer_when_uncertain": "split, duplicate, missing canonical note, or low confidence must become deferred_work_items, not guessed output.",
|
|
67
|
+
"evidence_redaction": "summaries/receipts must not include raw clinical prose, Markdown body, images, HTML, embeddings, or tokens.",
|
|
68
|
+
}
|
|
69
|
+
CURATOR_OUTPUT_CONTRACT = {
|
|
70
|
+
"must_include": [
|
|
71
|
+
"schema",
|
|
72
|
+
"workflow",
|
|
73
|
+
"phase",
|
|
74
|
+
"agent",
|
|
75
|
+
"source_workflow",
|
|
76
|
+
"note_path",
|
|
77
|
+
"content_hash",
|
|
78
|
+
"primary_meaning",
|
|
79
|
+
"aliases",
|
|
80
|
+
"deferred_work_items",
|
|
81
|
+
"confidence",
|
|
82
|
+
"agent_metrics",
|
|
83
|
+
],
|
|
84
|
+
"must_not_include": ["raw_markdown", "clinical_body", "html", "images", "embeddings", "api_keys"],
|
|
85
|
+
}
|
|
86
|
+
COMPLEX_QUEUE_FLAGS = {
|
|
87
|
+
"ambiguous_surface",
|
|
88
|
+
"duplicate_candidate",
|
|
89
|
+
"needs_merge",
|
|
90
|
+
"requires_context",
|
|
91
|
+
"suspected_non_atomic",
|
|
92
|
+
"split_candidate",
|
|
93
|
+
"yaml_alias_conflict",
|
|
94
|
+
}
|
|
95
|
+
CURATOR_PROMPT_SOURCE_PATHS = [
|
|
96
|
+
"agents/med-link-graph-curator.md",
|
|
97
|
+
"docs/agent-role-contracts.md",
|
|
98
|
+
"docs/merge-policy.md",
|
|
99
|
+
"docs/semantic-linker.md",
|
|
100
|
+
"docs/atomicity-splitting-policy.md",
|
|
101
|
+
"docs/agent-prompt-hardening.md",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def agent_output_ignored_notice(next_action: str = "") -> str:
|
|
106
|
+
action = next_action.strip() or "repita pela rota oficial antes de aplicar."
|
|
107
|
+
return f"ATENÇÃO: este output será ignorado e não será aplicado. Use a rota oficial: {action}"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def curator_agent_event(
|
|
111
|
+
*,
|
|
112
|
+
code: str,
|
|
113
|
+
root_cause_code: str,
|
|
114
|
+
next_action: str,
|
|
115
|
+
artifact_path: str = "",
|
|
116
|
+
reason: str = "",
|
|
117
|
+
severity: str = "medium",
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
sample: dict[str, str] = {"root_cause_code": root_cause_code}
|
|
120
|
+
if reason:
|
|
121
|
+
sample["reason"] = reason
|
|
122
|
+
return {
|
|
123
|
+
"schema": "medical-notes-workbench.agent-event.v1",
|
|
124
|
+
"type": "curator_contract_bypass",
|
|
125
|
+
"code": code,
|
|
126
|
+
"severity": severity,
|
|
127
|
+
"root_cause_code": root_cause_code,
|
|
128
|
+
"workflow": "/mednotes:link",
|
|
129
|
+
"phase": "vocabulary_curation",
|
|
130
|
+
"recovery_command": next_action,
|
|
131
|
+
"artifact_path": artifact_path,
|
|
132
|
+
"redacted_sample": sample,
|
|
133
|
+
"next_action": next_action,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class _CuratorPromptIdentityFields(ContractModel):
|
|
138
|
+
"""Typed view of prompt identity hashes used to bind eval reports to plans."""
|
|
139
|
+
|
|
140
|
+
model_config = ConfigDict(extra="ignore", populate_by_name=True, validate_assignment=True)
|
|
141
|
+
|
|
142
|
+
aggregate_hash: str = ""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class _CuratorPromptEvalFingerprintsFields(ContractModel):
|
|
146
|
+
"""Typed view of the fingerprints that make prompt eval reports replay-safe."""
|
|
147
|
+
|
|
148
|
+
model_config = ConfigDict(extra="ignore", populate_by_name=True, validate_assignment=True)
|
|
149
|
+
|
|
150
|
+
plan_hash: str = ""
|
|
151
|
+
manifest_hash: str = ""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class _CuratorPromptEvalAggregateFields(ContractModel):
|
|
155
|
+
"""Typed aggregate counters used to decide whether curator apply can run."""
|
|
156
|
+
|
|
157
|
+
model_config = ConfigDict(extra="ignore", populate_by_name=True, validate_assignment=True)
|
|
158
|
+
|
|
159
|
+
score: JsonValue = None
|
|
160
|
+
issue_count: int = Field(default=0, ge=0, strict=True)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class _CuratorPromptEvalItemFields(ContractModel):
|
|
164
|
+
"""Per-output prompt-eval status consumed by the apply gate."""
|
|
165
|
+
|
|
166
|
+
model_config = ConfigDict(extra="ignore", populate_by_name=True, validate_assignment=True)
|
|
167
|
+
|
|
168
|
+
status: str = ""
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class _CuratorPromptEvalBlockFields(ContractModel):
|
|
172
|
+
"""Prompt-eval summary shape used to build a blocked apply receipt."""
|
|
173
|
+
|
|
174
|
+
model_config = ConfigDict(extra="ignore", populate_by_name=True, validate_assignment=True)
|
|
175
|
+
|
|
176
|
+
status: str = "blocked"
|
|
177
|
+
blocked_reason: str = ""
|
|
178
|
+
next_action: str = ""
|
|
179
|
+
agent_event: JsonObject | None = None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class _CuratorManifestPathFields(ContractModel):
|
|
183
|
+
"""Optional manifest path evidence for prompt-eval skip notices."""
|
|
184
|
+
|
|
185
|
+
model_config = ConfigDict(extra="ignore", populate_by_name=True, validate_assignment=True)
|
|
186
|
+
|
|
187
|
+
path: str = ""
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _pending_queue_rows(db_path: Path, *, limit: int) -> list[sqlite3.Row]:
|
|
191
|
+
initialize_vocabulary_db(db_path)
|
|
192
|
+
with sqlite3.connect(db_path) as conn:
|
|
193
|
+
conn.row_factory = sqlite3.Row
|
|
194
|
+
return list(
|
|
195
|
+
conn.execute(
|
|
196
|
+
"""
|
|
197
|
+
SELECT q.note_id, q.note_path, q.content_hash, q.queue_flags_json,
|
|
198
|
+
q.assigned_agent, q.status, n.title
|
|
199
|
+
FROM note_semantic_ingestion_queue q
|
|
200
|
+
LEFT JOIN notes n ON n.id = q.note_id
|
|
201
|
+
WHERE q.status IN ('pending', 'claimed')
|
|
202
|
+
ORDER BY q.updated_at ASC, q.note_path ASC
|
|
203
|
+
LIMIT ?
|
|
204
|
+
""",
|
|
205
|
+
(limit,),
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _slug(value: str) -> str:
|
|
211
|
+
return re.sub(r"[^a-z0-9]+", "-", value.casefold()).strip("-") or "note"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _canonical_payload_hash(payload: Any) -> str:
|
|
215
|
+
encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
216
|
+
return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def curator_plan_hash(plan: dict[str, Any]) -> str:
|
|
220
|
+
hash_material: dict[str, Any] = {
|
|
221
|
+
"schema": plan.get("schema"),
|
|
222
|
+
"phase": plan.get("phase"),
|
|
223
|
+
"status": plan.get("status"),
|
|
224
|
+
"skipped_reason": plan.get("skipped_reason"),
|
|
225
|
+
}
|
|
226
|
+
work_items = plan.get("work_items")
|
|
227
|
+
normalized_items: list[Any] = []
|
|
228
|
+
if isinstance(work_items, list):
|
|
229
|
+
corpus_keys = (
|
|
230
|
+
"schema",
|
|
231
|
+
"work_id",
|
|
232
|
+
"app",
|
|
233
|
+
"workflow",
|
|
234
|
+
"phase",
|
|
235
|
+
"note_path",
|
|
236
|
+
"note_path_exists",
|
|
237
|
+
"path_case_check",
|
|
238
|
+
"content_hash",
|
|
239
|
+
"title",
|
|
240
|
+
"queue_flags",
|
|
241
|
+
"difficulty_route",
|
|
242
|
+
"expected_output_schema",
|
|
243
|
+
)
|
|
244
|
+
for item in work_items:
|
|
245
|
+
if isinstance(item, dict):
|
|
246
|
+
normalized_items.append({key: item.get(key) for key in corpus_keys if key in item})
|
|
247
|
+
else:
|
|
248
|
+
normalized_items.append(item)
|
|
249
|
+
hash_material["work_items"] = normalized_items
|
|
250
|
+
return _canonical_payload_hash(hash_material)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _sha256_bytes(content: bytes) -> str:
|
|
254
|
+
return f"sha256:{hashlib.sha256(content).hexdigest()}"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _default_extension_root() -> Path:
|
|
258
|
+
return _resolve_extension_root()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _source_fingerprint(*, extension_root: Path, relative_path: str) -> dict[str, Any]:
|
|
262
|
+
path = extension_root / relative_path
|
|
263
|
+
if not path.is_file():
|
|
264
|
+
return {
|
|
265
|
+
"path": relative_path,
|
|
266
|
+
"exists": False,
|
|
267
|
+
"sha256": "",
|
|
268
|
+
"byte_count": 0,
|
|
269
|
+
"word_count": 0,
|
|
270
|
+
}
|
|
271
|
+
content = path.read_bytes()
|
|
272
|
+
text = content.decode("utf-8", errors="replace")
|
|
273
|
+
return {
|
|
274
|
+
"path": relative_path,
|
|
275
|
+
"exists": True,
|
|
276
|
+
"sha256": _sha256_bytes(content),
|
|
277
|
+
"byte_count": len(content),
|
|
278
|
+
"word_count": len(text.split()),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def build_curator_prompt_identity(*, extension_root: Path | None = None) -> dict[str, Any]:
|
|
283
|
+
root = extension_root or _default_extension_root()
|
|
284
|
+
sources = [
|
|
285
|
+
_source_fingerprint(extension_root=root, relative_path=relative_path)
|
|
286
|
+
for relative_path in CURATOR_PROMPT_SOURCE_PATHS
|
|
287
|
+
]
|
|
288
|
+
aggregate_material = [
|
|
289
|
+
{"path": source["path"], "exists": source["exists"], "sha256": source["sha256"]}
|
|
290
|
+
for source in sources
|
|
291
|
+
]
|
|
292
|
+
return {
|
|
293
|
+
"schema": CURATOR_PROMPT_IDENTITY_SCHEMA,
|
|
294
|
+
"agent": "med-link-graph-curator",
|
|
295
|
+
"aggregate_hash": _canonical_payload_hash(aggregate_material),
|
|
296
|
+
"sources": sources,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _queue_flags(value: Any) -> list[str]:
|
|
301
|
+
try:
|
|
302
|
+
parsed = json.loads(str(value or "[]"))
|
|
303
|
+
except json.JSONDecodeError:
|
|
304
|
+
return []
|
|
305
|
+
if not isinstance(parsed, list):
|
|
306
|
+
return []
|
|
307
|
+
return [str(item) for item in parsed if str(item)]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _path_case_check(path: Path) -> dict[str, str]:
|
|
311
|
+
if not path.exists():
|
|
312
|
+
return {"status": "missing", "expected_path": str(path), "actual_path": ""}
|
|
313
|
+
current = path
|
|
314
|
+
parts: list[str] = []
|
|
315
|
+
while current.parent != current:
|
|
316
|
+
parts.append(current.name)
|
|
317
|
+
current = current.parent
|
|
318
|
+
if current.exists():
|
|
319
|
+
break
|
|
320
|
+
actual = current
|
|
321
|
+
for part in reversed(parts):
|
|
322
|
+
try:
|
|
323
|
+
matches = [child.name for child in actual.iterdir() if child.name.casefold() == part.casefold()]
|
|
324
|
+
except OSError:
|
|
325
|
+
return {"status": "unknown", "expected_path": str(path), "actual_path": str(path)}
|
|
326
|
+
if not matches:
|
|
327
|
+
return {"status": "missing", "expected_path": str(path), "actual_path": ""}
|
|
328
|
+
actual = actual / matches[0]
|
|
329
|
+
status = "exact" if str(actual) == str(path) else "case_mismatch"
|
|
330
|
+
return {"status": status, "expected_path": str(path), "actual_path": str(actual)}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _difficulty_route(*, flags: list[str], path_case_check: dict[str, str]) -> dict[str, Any]:
|
|
334
|
+
path_status = str(path_case_check.get("status") or "")
|
|
335
|
+
if path_status in {"missing", "case_mismatch"}:
|
|
336
|
+
return {
|
|
337
|
+
"route": "blocked_preflight",
|
|
338
|
+
"max_turns": 2,
|
|
339
|
+
"focus": ["return_blocked_output", path_status],
|
|
340
|
+
"efficiency_rule": "Do not read or reason semantically until the parent fixes path/hash preflight.",
|
|
341
|
+
}
|
|
342
|
+
if set(flags) & COMPLEX_QUEUE_FLAGS:
|
|
343
|
+
return {
|
|
344
|
+
"route": "complex_semantic_review",
|
|
345
|
+
"max_turns": 12,
|
|
346
|
+
"focus": [
|
|
347
|
+
"split_warning_or_deferred_work_expected",
|
|
348
|
+
"alias_ambiguity_review",
|
|
349
|
+
"duplicate_or_merge_detection",
|
|
350
|
+
],
|
|
351
|
+
"efficiency_rule": "Spend budget on classification and deferral; do not solve merge/split inside this packet.",
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
"route": "simple_atomic",
|
|
355
|
+
"max_turns": 8,
|
|
356
|
+
"focus": ["primary_meaning", "strict_aliases", "direct_vs_requires_context"],
|
|
357
|
+
"efficiency_rule": "Produce the smallest valid semantic-ingestion object; avoid broad taxonomy or note rewriting.",
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _work_item_from_row(
|
|
362
|
+
row: sqlite3.Row,
|
|
363
|
+
*,
|
|
364
|
+
db_path: Path,
|
|
365
|
+
output_dir: Path,
|
|
366
|
+
index: int,
|
|
367
|
+
prompt_identity: dict[str, Any],
|
|
368
|
+
) -> dict[str, Any]:
|
|
369
|
+
note_path = str(row["note_path"])
|
|
370
|
+
path = Path(note_path)
|
|
371
|
+
title = str(row["title"] or Path(note_path).stem)
|
|
372
|
+
work_id = f"vocab-curation-{index:03d}-{_slug(title)}"
|
|
373
|
+
flags = _queue_flags(row["queue_flags_json"])
|
|
374
|
+
path_check = _path_case_check(path)
|
|
375
|
+
route = _difficulty_route(flags=flags, path_case_check=path_check)
|
|
376
|
+
return {
|
|
377
|
+
"schema": AGENT_WORK_PACKET_SCHEMA,
|
|
378
|
+
"work_id": work_id,
|
|
379
|
+
"app": "medical-notes-workbench",
|
|
380
|
+
"workflow": "/mednotes:link",
|
|
381
|
+
"phase": "vocabulary_curation",
|
|
382
|
+
"agent": str(row["assigned_agent"] or "med-link-graph-curator"),
|
|
383
|
+
"source_workflow": "/mednotes:link",
|
|
384
|
+
"db_path": str(db_path),
|
|
385
|
+
"note_path": note_path,
|
|
386
|
+
"note_path_exists": path.exists(),
|
|
387
|
+
"path_case_check": path_check,
|
|
388
|
+
"content_hash": str(row["content_hash"]),
|
|
389
|
+
"title": title,
|
|
390
|
+
"queue_flags": flags,
|
|
391
|
+
"prompt_identity": dict(prompt_identity),
|
|
392
|
+
"difficulty_route": route,
|
|
393
|
+
"quality_rubric": dict(CURATOR_QUALITY_RUBRIC),
|
|
394
|
+
"output_contract": dict(CURATOR_OUTPUT_CONTRACT),
|
|
395
|
+
"allowed_actions": list(ALLOWED_CURATOR_ACTIONS),
|
|
396
|
+
"forbidden_actions": list(FORBIDDEN_CURATOR_ACTIONS),
|
|
397
|
+
"stop_conditions": list(CURATOR_STOP_CONDITIONS),
|
|
398
|
+
"retry_scope": "single_work_item",
|
|
399
|
+
"max_turns_policy": {"max_turns": 12, "on_exhaustion": "return_deferred_work_item"},
|
|
400
|
+
"expected_output_schema": NOTE_SEMANTIC_INGESTION_SCHEMA,
|
|
401
|
+
"output_path": str(output_dir / f"{work_id}.semantic-ingestion.json"),
|
|
402
|
+
"error_context": {
|
|
403
|
+
"phase": "vocabulary_curation",
|
|
404
|
+
"retry_scope": "single_work_item",
|
|
405
|
+
"next_action": "return blocked/deferred item; parent will decide official recovery command",
|
|
406
|
+
},
|
|
407
|
+
"instructions": [
|
|
408
|
+
"Read exactly this note.",
|
|
409
|
+
"Return medical-notes-workbench.note-semantic-ingestion.v1 with workflow=/mednotes:link, phase=vocabulary_curation, agent=med-link-graph-curator and source_workflow=/mednotes:link.",
|
|
410
|
+
"Do not call subagents.",
|
|
411
|
+
"Do not invoke @generalist",
|
|
412
|
+
"Only the parent orchestrator may launch med-link-graph-curator directly",
|
|
413
|
+
"Use deferred_work_items for duplicate, split, missing canonical note or merge work.",
|
|
414
|
+
f"For atomicity, follow {DOCS_RELPATH}/atomicity-splitting-policy.md; include body-based semantic_signal and never use title-only atomicity claims.",
|
|
415
|
+
],
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def build_vocabulary_curator_batch_plan(
|
|
420
|
+
*,
|
|
421
|
+
db_path: Path,
|
|
422
|
+
batch_id: str,
|
|
423
|
+
output_dir: Path,
|
|
424
|
+
limit: int = 20,
|
|
425
|
+
) -> dict[str, Any]:
|
|
426
|
+
if limit < 1:
|
|
427
|
+
raise ValidationError("vocabulary curator batch limit must be at least 1")
|
|
428
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
429
|
+
rows = _pending_queue_rows(db_path, limit=limit)
|
|
430
|
+
prompt_identity = build_curator_prompt_identity()
|
|
431
|
+
work_items = [
|
|
432
|
+
_work_item_from_row(
|
|
433
|
+
row,
|
|
434
|
+
db_path=db_path,
|
|
435
|
+
output_dir=output_dir,
|
|
436
|
+
index=index,
|
|
437
|
+
prompt_identity=prompt_identity,
|
|
438
|
+
)
|
|
439
|
+
for index, row in enumerate(rows, start=1)
|
|
440
|
+
]
|
|
441
|
+
return {
|
|
442
|
+
"schema": VOCABULARY_CURATOR_BATCH_PLAN_SCHEMA,
|
|
443
|
+
"phase": "vocabulary_curation",
|
|
444
|
+
"status": "ready" if work_items else "skipped",
|
|
445
|
+
"skipped_reason": "" if work_items else "no_pending_semantic_ingestion",
|
|
446
|
+
"batch_id": batch_id,
|
|
447
|
+
"db_path": str(db_path),
|
|
448
|
+
"prompt_identity": prompt_identity,
|
|
449
|
+
"prompt_eval_report_path": str(output_dir.parent / "curator-prompt-eval.json"),
|
|
450
|
+
"item_count": len(work_items),
|
|
451
|
+
"work_items": work_items,
|
|
452
|
+
"parallel_safe": len(work_items) > 1,
|
|
453
|
+
"max_concurrency": min(5, max(1, len(work_items))) if work_items else 0,
|
|
454
|
+
"rules": [
|
|
455
|
+
"Parent orchestrator must launch med-link-graph-curator directly",
|
|
456
|
+
"Never delegate vocabulary curation orchestration to @generalist",
|
|
457
|
+
"Spawn at most one med-link-graph-curator per work_item.",
|
|
458
|
+
"The subagent must not call another subagent.",
|
|
459
|
+
"The subagent writes only output_path.",
|
|
460
|
+
"Parent runs eval-curator-batch --report before apply-curator-batch.",
|
|
461
|
+
"Parent applies outputs with apply-curator-batch --prompt-eval.",
|
|
462
|
+
],
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _read_json_object(path: Path, *, label: str) -> JsonObject:
|
|
467
|
+
try:
|
|
468
|
+
raw = path.read_bytes()
|
|
469
|
+
except FileNotFoundError as exc:
|
|
470
|
+
raise ValidationError(f"{label} not found: {path}") from exc
|
|
471
|
+
if raw.startswith((b"\xff\xfe", b"\xfe\xff")):
|
|
472
|
+
raise ValidationError(
|
|
473
|
+
f"artifact_encoding.unsupported_utf16: {label} {path} is UTF-16; "
|
|
474
|
+
"regenerate it with collect-curator-outputs/eval-curator-batch or write UTF-8 without BOM."
|
|
475
|
+
)
|
|
476
|
+
try:
|
|
477
|
+
text = raw.decode("utf-8-sig")
|
|
478
|
+
except UnicodeDecodeError as exc:
|
|
479
|
+
raise ValidationError(f"artifact_encoding.invalid_utf8: {label} {path} must be UTF-8: {exc}") from exc
|
|
480
|
+
try:
|
|
481
|
+
payload = json.loads(text)
|
|
482
|
+
except json.JSONDecodeError as exc:
|
|
483
|
+
raise ValidationError(f"{label} is invalid JSON: {path}: {exc}") from exc
|
|
484
|
+
if not isinstance(payload, dict):
|
|
485
|
+
raise ValidationError(f"{label} must be a JSON object: {path}")
|
|
486
|
+
return JsonObjectAdapter.validate_python(payload)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _validate_curator_batch_plan(plan: JsonObject) -> CuratorBatchPlan:
|
|
490
|
+
try:
|
|
491
|
+
return CuratorBatchPlan.model_validate(plan)
|
|
492
|
+
except PydanticValidationError as exc:
|
|
493
|
+
raise contract_error(exc, prefix="curator batch plan invalid") from exc
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _validate_curator_manifest(manifest: JsonObject) -> CuratorManifest:
|
|
497
|
+
try:
|
|
498
|
+
return CuratorManifest.model_validate(manifest)
|
|
499
|
+
except PydanticValidationError as exc:
|
|
500
|
+
raise contract_error(exc, prefix="curator batch manifest invalid") from exc
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _validate_curator_prompt_eval_report(report: JsonObject) -> CuratorPromptEvalReport:
|
|
504
|
+
try:
|
|
505
|
+
return CuratorPromptEvalReport.model_validate(report)
|
|
506
|
+
except PydanticValidationError as exc:
|
|
507
|
+
raise contract_error(exc, prefix="curator prompt eval invalid") from exc
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _validate_note_semantic_ingestion_output(payload: JsonObject) -> NoteSemanticIngestionOutput:
|
|
511
|
+
try:
|
|
512
|
+
return NoteSemanticIngestionOutput.model_validate(payload)
|
|
513
|
+
except PydanticValidationError as exc:
|
|
514
|
+
for error in exc.errors():
|
|
515
|
+
loc = tuple(error.get("loc", ()))
|
|
516
|
+
if len(loc) >= 3 and loc[0] == "aliases" and loc[2] == "text":
|
|
517
|
+
alias_index = loc[1]
|
|
518
|
+
aliases = payload.get("aliases")
|
|
519
|
+
alias = aliases[alias_index] if isinstance(alias_index, int) and isinstance(aliases, list) else None
|
|
520
|
+
if isinstance(alias, dict) and alias.get("surface"):
|
|
521
|
+
raise ValidationError(
|
|
522
|
+
f"semantic ingestion aliases[{alias_index}].text is required; "
|
|
523
|
+
"use aliases[].text, not aliases[].surface"
|
|
524
|
+
) from exc
|
|
525
|
+
first_error = exc.errors()[0] if exc.errors() else {}
|
|
526
|
+
loc = ".".join(str(part) for part in first_error.get("loc", ())) or "$"
|
|
527
|
+
msg = str(first_error.get("msg") or str(exc))
|
|
528
|
+
invalid_input = first_error.get("input")
|
|
529
|
+
input_suffix = f" (input={invalid_input!r})" if invalid_input is not None else ""
|
|
530
|
+
raise ValidationError(f"subagent_output_contract.invalid: {loc}: {msg}{input_suffix}") from exc
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _finalize_curator_apply_receipt(payload: JsonObject) -> JsonObject:
|
|
534
|
+
try:
|
|
535
|
+
receipt = CuratorApplyReceipt.model_validate(payload)
|
|
536
|
+
except PydanticValidationError as exc:
|
|
537
|
+
raise contract_error(exc, prefix="curator apply receipt invalid") from exc
|
|
538
|
+
return receipt.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _json_text_field(payload: JsonObject, field_name: str, default: str = "") -> str:
|
|
542
|
+
value = payload.get(field_name)
|
|
543
|
+
if value is None:
|
|
544
|
+
return default
|
|
545
|
+
return str(value)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _json_object_field(payload: JsonObject, field_name: str) -> JsonObject:
|
|
549
|
+
value = payload.get(field_name)
|
|
550
|
+
if isinstance(value, dict):
|
|
551
|
+
return JsonObjectAdapter.validate_python(value)
|
|
552
|
+
return {}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _manifest_items(manifest: dict[str, Any]) -> list[dict[str, str]]:
|
|
556
|
+
typed_manifest = _validate_curator_manifest(manifest)
|
|
557
|
+
return [item.model_dump(mode="json", exclude_defaults=True) for item in typed_manifest.items]
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _prompt_eval_summary(
|
|
561
|
+
*,
|
|
562
|
+
plan: JsonObject,
|
|
563
|
+
manifest: JsonObject,
|
|
564
|
+
prompt_eval_path: Path | None,
|
|
565
|
+
skip_prompt_eval: bool,
|
|
566
|
+
skip_prompt_eval_reason: str = "",
|
|
567
|
+
) -> JsonObject:
|
|
568
|
+
if prompt_eval_path is not None and skip_prompt_eval:
|
|
569
|
+
raise ValidationError("curator_prompt_eval_options_conflict: --prompt-eval and --skip-prompt-eval are mutually exclusive")
|
|
570
|
+
if skip_prompt_eval:
|
|
571
|
+
reason = skip_prompt_eval_reason.strip()
|
|
572
|
+
manifest_fields = _CuratorManifestPathFields.model_validate(manifest)
|
|
573
|
+
if not reason:
|
|
574
|
+
raise ValidationError("curator_prompt_eval_skip_reason_required: skip prompt eval requires an explicit reason")
|
|
575
|
+
if os.environ.get(DEV_ESCAPE_ENV) != "1":
|
|
576
|
+
next_action = (
|
|
577
|
+
"Use eval-curator-batch --report e aplique com --prompt-eval. "
|
|
578
|
+
"--skip-prompt-eval não aplica outputs de curadoria em workflows reais."
|
|
579
|
+
)
|
|
580
|
+
return {
|
|
581
|
+
"status": "dev_escape_disabled",
|
|
582
|
+
"blocked_reason": "curator_prompt_eval.dev_escape_disabled",
|
|
583
|
+
"required_env": DEV_ESCAPE_ENV,
|
|
584
|
+
"reason": reason,
|
|
585
|
+
"next_action": next_action,
|
|
586
|
+
"agent_event": curator_agent_event(
|
|
587
|
+
code="agent.curator_prompt_eval_skip_attempt",
|
|
588
|
+
root_cause_code="curator_prompt_eval.dev_escape_disabled",
|
|
589
|
+
next_action=next_action,
|
|
590
|
+
artifact_path=manifest_fields.path,
|
|
591
|
+
reason=reason,
|
|
592
|
+
),
|
|
593
|
+
}
|
|
594
|
+
next_action = (
|
|
595
|
+
"Use eval-curator-batch --report e aplique com --prompt-eval. "
|
|
596
|
+
"--skip-prompt-eval não aplica outputs de curadoria; o output será ignorado até passar pela rota oficial."
|
|
597
|
+
)
|
|
598
|
+
return {
|
|
599
|
+
"status": "dev_escape_ignored",
|
|
600
|
+
"blocked_reason": "curator_prompt_eval.dev_escape_ignored",
|
|
601
|
+
"reason": reason,
|
|
602
|
+
"next_action": next_action,
|
|
603
|
+
"agent_event": curator_agent_event(
|
|
604
|
+
code="agent.curator_prompt_eval_skip_attempt",
|
|
605
|
+
root_cause_code="curator_prompt_eval.dev_escape_ignored",
|
|
606
|
+
next_action=wiki_cli_command(
|
|
607
|
+
"eval-curator-batch",
|
|
608
|
+
"--plan",
|
|
609
|
+
"<plan>",
|
|
610
|
+
"--outputs",
|
|
611
|
+
"<manifest>",
|
|
612
|
+
"--report",
|
|
613
|
+
"<curator-prompt-eval.json>",
|
|
614
|
+
"--json",
|
|
615
|
+
),
|
|
616
|
+
artifact_path=manifest_fields.path,
|
|
617
|
+
reason=reason,
|
|
618
|
+
),
|
|
619
|
+
}
|
|
620
|
+
if prompt_eval_path is None:
|
|
621
|
+
raise ValidationError(
|
|
622
|
+
"curator_prompt_eval_required: curator prompt eval required before apply-curator-batch; run eval-curator-batch "
|
|
623
|
+
"--plan <plan> --outputs <manifest> --report <report> --json and pass --prompt-eval <report>"
|
|
624
|
+
)
|
|
625
|
+
prompt_eval_report = _validate_curator_prompt_eval_report(
|
|
626
|
+
_read_json_object(prompt_eval_path, label="curator prompt eval")
|
|
627
|
+
)
|
|
628
|
+
fingerprints = _CuratorPromptEvalFingerprintsFields.model_validate(prompt_eval_report.input_fingerprints)
|
|
629
|
+
if not prompt_eval_report.input_fingerprints:
|
|
630
|
+
raise ValidationError("curator prompt eval requires input_fingerprints")
|
|
631
|
+
expected_plan_hash = curator_plan_hash(plan)
|
|
632
|
+
expected_manifest_hash = f"sha256:{_validate_curator_manifest(manifest).fingerprint()}"
|
|
633
|
+
plan_prompt = _CuratorPromptIdentityFields.model_validate(_json_object_field(plan, "prompt_identity"))
|
|
634
|
+
report_prompt = _CuratorPromptIdentityFields.model_validate(prompt_eval_report.prompt_identity)
|
|
635
|
+
if fingerprints.plan_hash != expected_plan_hash:
|
|
636
|
+
raise ValidationError("curator prompt eval plan_hash mismatch")
|
|
637
|
+
if fingerprints.manifest_hash != expected_manifest_hash:
|
|
638
|
+
next_action = "Recriar manifest e avaliacao pela rota oficial antes de aplicar."
|
|
639
|
+
return {
|
|
640
|
+
"status": "blocked",
|
|
641
|
+
"blocked_reason": "curator_prompt_eval.inconsistent_report",
|
|
642
|
+
"root_cause": "curator_batch.prompt_eval_manifest_mismatch",
|
|
643
|
+
"path": str(prompt_eval_path),
|
|
644
|
+
"next_action": next_action,
|
|
645
|
+
"prompt_identity": prompt_eval_report.prompt_identity,
|
|
646
|
+
"input_fingerprints": {
|
|
647
|
+
"plan_hash": expected_plan_hash,
|
|
648
|
+
"manifest_hash": expected_manifest_hash,
|
|
649
|
+
"prompt_identity_hash": plan_prompt.aggregate_hash,
|
|
650
|
+
},
|
|
651
|
+
}
|
|
652
|
+
if plan_prompt.aggregate_hash != report_prompt.aggregate_hash:
|
|
653
|
+
raise ValidationError("curator prompt eval prompt_identity mismatch")
|
|
654
|
+
aggregate = _CuratorPromptEvalAggregateFields.model_validate(prompt_eval_report.aggregate)
|
|
655
|
+
report_status = prompt_eval_report.status
|
|
656
|
+
next_action = prompt_eval_report.next_action
|
|
657
|
+
item_statuses = [_CuratorPromptEvalItemFields.model_validate(item).status for item in prompt_eval_report.items]
|
|
658
|
+
pass_status_inconsistent = (
|
|
659
|
+
report_status == "pass"
|
|
660
|
+
and (
|
|
661
|
+
aggregate.issue_count != 0
|
|
662
|
+
or bool(prompt_eval_report.aggregate_issues)
|
|
663
|
+
or any(status != "pass" for status in item_statuses)
|
|
664
|
+
)
|
|
665
|
+
)
|
|
666
|
+
if pass_status_inconsistent:
|
|
667
|
+
next_action = (
|
|
668
|
+
"Regenerar curator-prompt-eval com eval-curator-batch a partir do plan/manifest oficiais; "
|
|
669
|
+
"não edite o relatório de avaliação manualmente."
|
|
670
|
+
)
|
|
671
|
+
return {
|
|
672
|
+
"status": "invalid",
|
|
673
|
+
"blocked_reason": "curator_prompt_eval.inconsistent_report",
|
|
674
|
+
"path": str(prompt_eval_path),
|
|
675
|
+
"score": aggregate.score,
|
|
676
|
+
"issue_count": aggregate.issue_count,
|
|
677
|
+
"next_action": next_action,
|
|
678
|
+
"agent_event": curator_agent_event(
|
|
679
|
+
code="agent.curator_prompt_eval_manual_status_edit",
|
|
680
|
+
root_cause_code="curator_prompt_eval.inconsistent_report",
|
|
681
|
+
next_action=next_action,
|
|
682
|
+
artifact_path=str(prompt_eval_path),
|
|
683
|
+
reason="pass_status_inconsistent_with_report_issues",
|
|
684
|
+
),
|
|
685
|
+
"prompt_identity": prompt_eval_report.prompt_identity,
|
|
686
|
+
"input_fingerprints": {
|
|
687
|
+
"plan_hash": expected_plan_hash,
|
|
688
|
+
"manifest_hash": expected_manifest_hash,
|
|
689
|
+
"prompt_identity_hash": plan_prompt.aggregate_hash,
|
|
690
|
+
},
|
|
691
|
+
}
|
|
692
|
+
if report_status not in {"pass", "needs_review"}:
|
|
693
|
+
return {
|
|
694
|
+
"status": "invalid",
|
|
695
|
+
"blocked_reason": "curator_prompt_eval.invalid_status",
|
|
696
|
+
"path": str(prompt_eval_path),
|
|
697
|
+
"score": aggregate.score,
|
|
698
|
+
"issue_count": aggregate.issue_count,
|
|
699
|
+
"next_action": (
|
|
700
|
+
next_action
|
|
701
|
+
or "Regenerar curator-prompt-eval com eval-curator-batch; status permitido é pass ou needs_review."
|
|
702
|
+
),
|
|
703
|
+
"agent_event": curator_agent_event(
|
|
704
|
+
code=(
|
|
705
|
+
"agent.curator_prompt_eval_manual_status_edit"
|
|
706
|
+
if report_status == "approved"
|
|
707
|
+
else "agent.curator_prompt_eval_invalid_status"
|
|
708
|
+
),
|
|
709
|
+
root_cause_code="curator_prompt_eval.invalid_status",
|
|
710
|
+
next_action=(
|
|
711
|
+
next_action
|
|
712
|
+
or "Regenerar curator-prompt-eval com eval-curator-batch; status permitido é pass ou needs_review."
|
|
713
|
+
),
|
|
714
|
+
artifact_path=str(prompt_eval_path),
|
|
715
|
+
reason=f"invalid_status:{report_status}",
|
|
716
|
+
),
|
|
717
|
+
"prompt_identity": prompt_eval_report.prompt_identity,
|
|
718
|
+
"input_fingerprints": {
|
|
719
|
+
"plan_hash": expected_plan_hash,
|
|
720
|
+
"manifest_hash": expected_manifest_hash,
|
|
721
|
+
"prompt_identity_hash": plan_prompt.aggregate_hash,
|
|
722
|
+
},
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
"status": report_status,
|
|
726
|
+
"path": str(prompt_eval_path),
|
|
727
|
+
"score": aggregate.score,
|
|
728
|
+
"issue_count": aggregate.issue_count,
|
|
729
|
+
"next_action": next_action,
|
|
730
|
+
"prompt_identity": prompt_eval_report.prompt_identity,
|
|
731
|
+
"input_fingerprints": {
|
|
732
|
+
"plan_hash": expected_plan_hash,
|
|
733
|
+
"manifest_hash": expected_manifest_hash,
|
|
734
|
+
"prompt_identity_hash": plan_prompt.aggregate_hash,
|
|
735
|
+
},
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _blocked_by_prompt_eval_receipt(
|
|
740
|
+
*,
|
|
741
|
+
plan: JsonObject,
|
|
742
|
+
db_path: Path,
|
|
743
|
+
by_work_id: dict[str, JsonObject],
|
|
744
|
+
manifest_items: list[dict[str, str]],
|
|
745
|
+
prompt_eval: JsonObject,
|
|
746
|
+
) -> JsonObject:
|
|
747
|
+
prompt_eval_fields = _CuratorPromptEvalBlockFields.model_validate(prompt_eval)
|
|
748
|
+
plan_fields = CuratorBatchPlan.model_validate(plan)
|
|
749
|
+
prompt_eval_status = prompt_eval_fields.status
|
|
750
|
+
blocked_reason = (
|
|
751
|
+
prompt_eval_fields.blocked_reason
|
|
752
|
+
or (
|
|
753
|
+
"curator_prompt_eval.needs_review"
|
|
754
|
+
if prompt_eval_status == "needs_review"
|
|
755
|
+
else "curator_prompt_eval.blocked"
|
|
756
|
+
)
|
|
757
|
+
)
|
|
758
|
+
next_action = (
|
|
759
|
+
prompt_eval_fields.next_action
|
|
760
|
+
or "Revisar outputs e prompt/rubrica, regenerar curator-prompt-eval e repetir apply-curator-batch com --prompt-eval."
|
|
761
|
+
)
|
|
762
|
+
agent_events = [prompt_eval_fields.agent_event] if prompt_eval_fields.agent_event is not None else []
|
|
763
|
+
prompt_eval_payload = {key: value for key, value in prompt_eval.items() if key != "agent_event"} if agent_events else prompt_eval
|
|
764
|
+
return _finalize_curator_apply_receipt({
|
|
765
|
+
"schema": VOCABULARY_CURATOR_BATCH_RECEIPT_SCHEMA,
|
|
766
|
+
"phase": "vocabulary_curation",
|
|
767
|
+
"status": "blocked",
|
|
768
|
+
"blocked_reason": blocked_reason,
|
|
769
|
+
"next_action": next_action,
|
|
770
|
+
"agent_notice": agent_output_ignored_notice(next_action),
|
|
771
|
+
"required_inputs": ["prompt_eval"],
|
|
772
|
+
"human_decision_required": False,
|
|
773
|
+
"batch_id": plan_fields.batch_id,
|
|
774
|
+
"db_path": str(db_path),
|
|
775
|
+
"prompt_eval": prompt_eval_payload,
|
|
776
|
+
"agent_events": agent_events,
|
|
777
|
+
"plan_item_count": len(by_work_id),
|
|
778
|
+
"manifest_item_count": len(manifest_items),
|
|
779
|
+
"applied_count": 0,
|
|
780
|
+
"blocked_count": len(manifest_items),
|
|
781
|
+
"items": [
|
|
782
|
+
{
|
|
783
|
+
"work_id": item["work_id"],
|
|
784
|
+
"output_path": item["output_path"],
|
|
785
|
+
"status": "blocked",
|
|
786
|
+
"blocked_reason": blocked_reason,
|
|
787
|
+
"next_action": next_action,
|
|
788
|
+
"agent_notice": agent_output_ignored_notice(next_action),
|
|
789
|
+
"note_path": str(by_work_id.get(item["work_id"], {}).get("note_path", "")),
|
|
790
|
+
"content_hash": str(by_work_id.get(item["work_id"], {}).get("content_hash", "")),
|
|
791
|
+
}
|
|
792
|
+
for item in manifest_items
|
|
793
|
+
],
|
|
794
|
+
"error_context": {
|
|
795
|
+
"phase": "vocabulary_curation",
|
|
796
|
+
"blocked_reason": blocked_reason,
|
|
797
|
+
"root_cause": str(prompt_eval.get("root_cause") or blocked_reason),
|
|
798
|
+
"affected_artifact": str(prompt_eval.get("path") or "curator_prompt_eval"),
|
|
799
|
+
"error_summary": "curator prompt evaluation did not pass or was inconsistent",
|
|
800
|
+
"suggested_fix": next_action,
|
|
801
|
+
"next_action": next_action,
|
|
802
|
+
"retry_scope": "curator_prompt_eval_then_apply",
|
|
803
|
+
},
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def _work_id_from_semantic_output_path(path: Path) -> str:
|
|
808
|
+
return path.name.removesuffix(".semantic-ingestion.json")
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _ignored_output_notices(
|
|
812
|
+
*,
|
|
813
|
+
work_items: list[Any],
|
|
814
|
+
manifest_items: list[dict[str, str]],
|
|
815
|
+
) -> list[dict[str, str]]:
|
|
816
|
+
manifest_paths = {
|
|
817
|
+
Path(item["output_path"]).expanduser().resolve(strict=False)
|
|
818
|
+
for item in manifest_items
|
|
819
|
+
if str(item.get("output_path") or "")
|
|
820
|
+
}
|
|
821
|
+
output_dirs = {
|
|
822
|
+
Path(str(item.output_path)).expanduser().parent
|
|
823
|
+
for item in work_items
|
|
824
|
+
if str(getattr(item, "output_path", "") or "")
|
|
825
|
+
}
|
|
826
|
+
notices: list[dict[str, str]] = []
|
|
827
|
+
for output_dir in sorted(output_dirs):
|
|
828
|
+
if not output_dir.is_dir():
|
|
829
|
+
continue
|
|
830
|
+
for output_path in sorted(output_dir.glob("*.semantic-ingestion.json")):
|
|
831
|
+
normalized_output = output_path.expanduser().resolve(strict=False)
|
|
832
|
+
if normalized_output in manifest_paths:
|
|
833
|
+
continue
|
|
834
|
+
notice = CuratorIgnoredOutputNotice(
|
|
835
|
+
work_id=_work_id_from_semantic_output_path(output_path),
|
|
836
|
+
output_path=str(output_path),
|
|
837
|
+
reason="not_in_manifest",
|
|
838
|
+
next_action="Recriar o manifest pela rota oficial se este output deve ser aplicado.",
|
|
839
|
+
)
|
|
840
|
+
notices.append(notice.model_dump(mode="json"))
|
|
841
|
+
return notices
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def collect_curator_outputs(
|
|
845
|
+
*,
|
|
846
|
+
plan: JsonObject,
|
|
847
|
+
manifest_path: Path,
|
|
848
|
+
include_missing: bool = False,
|
|
849
|
+
) -> JsonObject:
|
|
850
|
+
typed_plan = _validate_curator_batch_plan(plan)
|
|
851
|
+
items: list[CuratorManifestItem] = []
|
|
852
|
+
missing_outputs: list[dict[str, str]] = []
|
|
853
|
+
seen: set[str] = set()
|
|
854
|
+
for raw_item in typed_plan.work_items:
|
|
855
|
+
work_id = raw_item.work_id
|
|
856
|
+
output_path = raw_item.output_path
|
|
857
|
+
if work_id in seen:
|
|
858
|
+
raise ValidationError(f"duplicate work_id in curator batch plan: {work_id}")
|
|
859
|
+
seen.add(work_id)
|
|
860
|
+
path = Path(output_path)
|
|
861
|
+
if path.is_file():
|
|
862
|
+
items.append(CuratorManifestItem(work_id=work_id, output_path=str(path), sha256=_sha256_bytes(path.read_bytes())))
|
|
863
|
+
elif include_missing:
|
|
864
|
+
missing_outputs.append({"work_id": work_id, "output_path": str(path)})
|
|
865
|
+
else:
|
|
866
|
+
missing_outputs.append({"work_id": work_id, "output_path": str(path)})
|
|
867
|
+
|
|
868
|
+
manifest = CuratorManifest(
|
|
869
|
+
schema=VOCABULARY_CURATOR_BATCH_OUTPUT_MANIFEST_SCHEMA,
|
|
870
|
+
batch_id=typed_plan.batch_id,
|
|
871
|
+
items=items,
|
|
872
|
+
)
|
|
873
|
+
manifest_payload = manifest.model_dump(mode="json", by_alias=True)
|
|
874
|
+
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
875
|
+
tmp_path = manifest_path.with_name(manifest_path.name + ".tmp")
|
|
876
|
+
tmp_path.write_text(json.dumps(manifest_payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
877
|
+
os.replace(tmp_path, manifest_path)
|
|
878
|
+
return {
|
|
879
|
+
"schema": "medical-notes-workbench.vocabulary-curator-output-collection.v1",
|
|
880
|
+
"phase": "vocabulary_curation",
|
|
881
|
+
"status": "completed_with_missing" if missing_outputs else "completed",
|
|
882
|
+
"manifest_path": str(manifest_path),
|
|
883
|
+
"batch_id": typed_plan.batch_id,
|
|
884
|
+
"planned_count": typed_plan.item_count,
|
|
885
|
+
"included_count": len(items),
|
|
886
|
+
"missing_count": len(missing_outputs),
|
|
887
|
+
"missing_outputs": missing_outputs,
|
|
888
|
+
"include_missing": bool(include_missing),
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def apply_curator_batch_outputs(
|
|
893
|
+
*,
|
|
894
|
+
plan: JsonObject,
|
|
895
|
+
manifest_path: Path,
|
|
896
|
+
prompt_eval_path: Path | None = None,
|
|
897
|
+
skip_prompt_eval: bool = False,
|
|
898
|
+
skip_prompt_eval_reason: str = "",
|
|
899
|
+
) -> JsonObject:
|
|
900
|
+
typed_plan = _validate_curator_batch_plan(plan)
|
|
901
|
+
db_path = Path(typed_plan.db_path)
|
|
902
|
+
|
|
903
|
+
by_work_id: dict[str, JsonObject] = {}
|
|
904
|
+
for item in typed_plan.work_items:
|
|
905
|
+
work_id = item.work_id
|
|
906
|
+
if work_id in by_work_id:
|
|
907
|
+
raise ValidationError(f"duplicate work_id in curator batch plan: {work_id}")
|
|
908
|
+
by_work_id[work_id] = item.model_dump(mode="json", by_alias=True)
|
|
909
|
+
|
|
910
|
+
manifest = _read_json_object(manifest_path, label="curator batch output manifest")
|
|
911
|
+
manifest_items = _manifest_items(manifest)
|
|
912
|
+
ignored_notices = _ignored_output_notices(work_items=typed_plan.work_items, manifest_items=manifest_items)
|
|
913
|
+
prompt_eval = _prompt_eval_summary(
|
|
914
|
+
plan=plan,
|
|
915
|
+
manifest=manifest,
|
|
916
|
+
prompt_eval_path=prompt_eval_path,
|
|
917
|
+
skip_prompt_eval=skip_prompt_eval,
|
|
918
|
+
skip_prompt_eval_reason=skip_prompt_eval_reason,
|
|
919
|
+
)
|
|
920
|
+
if prompt_eval.get("status") != "pass" and prompt_eval.get("status") != "skipped":
|
|
921
|
+
return _blocked_by_prompt_eval_receipt(
|
|
922
|
+
plan=plan,
|
|
923
|
+
db_path=db_path,
|
|
924
|
+
by_work_id=by_work_id,
|
|
925
|
+
manifest_items=manifest_items,
|
|
926
|
+
prompt_eval=prompt_eval,
|
|
927
|
+
)
|
|
928
|
+
agent_events = [prompt_eval["agent_event"]] if isinstance(prompt_eval.get("agent_event"), dict) else []
|
|
929
|
+
if agent_events:
|
|
930
|
+
prompt_eval = {key: value for key, value in prompt_eval.items() if key != "agent_event"}
|
|
931
|
+
seen: set[str] = set()
|
|
932
|
+
receipts: list[JsonObject] = []
|
|
933
|
+
applied_count = 0
|
|
934
|
+
blocked_count = 0
|
|
935
|
+
for manifest_item in manifest_items:
|
|
936
|
+
work_id = manifest_item["work_id"]
|
|
937
|
+
if work_id in seen:
|
|
938
|
+
raise ValidationError(f"duplicate work_id in curator batch manifest: {work_id}")
|
|
939
|
+
seen.add(work_id)
|
|
940
|
+
if work_id not in by_work_id:
|
|
941
|
+
raise ValidationError(f"unknown work_id in curator batch manifest: {work_id}")
|
|
942
|
+
output_path = Path(manifest_item["output_path"])
|
|
943
|
+
try:
|
|
944
|
+
expected_output_hash = str(manifest_item.get("sha256") or "")
|
|
945
|
+
if not expected_output_hash:
|
|
946
|
+
raise ValidationError(
|
|
947
|
+
"curator_output_manifest.missing_sha256: manifest item must be produced by collect-curator-outputs "
|
|
948
|
+
"before eval/apply so output changes can be detected."
|
|
949
|
+
)
|
|
950
|
+
try:
|
|
951
|
+
output_bytes = output_path.read_bytes()
|
|
952
|
+
except OSError as exc:
|
|
953
|
+
raise ValidationError(f"curator_output_unreadable: {output_path}: {exc}") from exc
|
|
954
|
+
actual_output_hash = _sha256_bytes(output_bytes)
|
|
955
|
+
if actual_output_hash != expected_output_hash:
|
|
956
|
+
raise ValidationError(
|
|
957
|
+
"curator_output_hash_mismatch: output changed after collect-curator-outputs; "
|
|
958
|
+
"regenerate manifest and curator prompt eval before applying"
|
|
959
|
+
)
|
|
960
|
+
if output_bytes.startswith((b"\xff\xfe", b"\xfe\xff")):
|
|
961
|
+
raise ValidationError(
|
|
962
|
+
f"artifact_encoding.unsupported_utf16: curator batch output {output_path} is UTF-16; "
|
|
963
|
+
"regenerate it with med-link-graph-curator or write UTF-8 without BOM."
|
|
964
|
+
)
|
|
965
|
+
try:
|
|
966
|
+
output_text = output_bytes.decode("utf-8-sig")
|
|
967
|
+
except UnicodeDecodeError as exc:
|
|
968
|
+
raise ValidationError(
|
|
969
|
+
f"artifact_encoding.invalid_utf8: curator batch output {output_path} must be UTF-8: {exc}"
|
|
970
|
+
) from exc
|
|
971
|
+
try:
|
|
972
|
+
output_payload = json.loads(output_text)
|
|
973
|
+
except json.JSONDecodeError as exc:
|
|
974
|
+
raise ValidationError(f"curator batch output is invalid JSON: {output_path}: {exc}") from exc
|
|
975
|
+
if not isinstance(output_payload, dict):
|
|
976
|
+
raise ValidationError(f"curator batch output must be a JSON object: {output_path}")
|
|
977
|
+
output_payload = JsonObjectAdapter.validate_python(output_payload)
|
|
978
|
+
typed_output = _validate_note_semantic_ingestion_output(output_payload)
|
|
979
|
+
receipt = JsonObjectAdapter.validate_python(
|
|
980
|
+
apply_semantic_ingestion(
|
|
981
|
+
db_path=db_path,
|
|
982
|
+
item=typed_output.model_dump(mode="json", by_alias=True, exclude_none=True),
|
|
983
|
+
require_contract=True,
|
|
984
|
+
)
|
|
985
|
+
)
|
|
986
|
+
except ValidationError as exc:
|
|
987
|
+
error_text = str(exc)
|
|
988
|
+
blocked_reason = (
|
|
989
|
+
"subagent_output_contract.invalid"
|
|
990
|
+
if error_text.startswith("subagent_output_contract.invalid")
|
|
991
|
+
else "curator_output_hash_mismatch"
|
|
992
|
+
if error_text.startswith("curator_output_hash_mismatch")
|
|
993
|
+
else "curator_output_manifest.missing_sha256"
|
|
994
|
+
if error_text.startswith("curator_output_manifest.missing_sha256")
|
|
995
|
+
else "semantic_ingestion.validation_error"
|
|
996
|
+
)
|
|
997
|
+
next_action = (
|
|
998
|
+
"Regenerar o output com med-link-graph-curator direto a partir do work_item oficial; não use @generalist nem output sem workflow/phase/source_workflow."
|
|
999
|
+
if blocked_reason == "subagent_output_contract.invalid"
|
|
1000
|
+
else "Regenerar o manifest com collect-curator-outputs, rodar eval-curator-batch novamente e repetir apply-curator-batch."
|
|
1001
|
+
if blocked_reason in {"curator_output_hash_mismatch", "curator_output_manifest.missing_sha256"}
|
|
1002
|
+
else "Corrigir o output note-semantic-ingestion.v1 e repetir apply-curator-batch após eval-curator-batch passar."
|
|
1003
|
+
)
|
|
1004
|
+
agent_event = None
|
|
1005
|
+
if blocked_reason == "curator_output_hash_mismatch":
|
|
1006
|
+
agent_event = curator_agent_event(
|
|
1007
|
+
code="agent.curator_output_changed_after_collection",
|
|
1008
|
+
root_cause_code=blocked_reason,
|
|
1009
|
+
next_action=next_action,
|
|
1010
|
+
artifact_path=str(output_path),
|
|
1011
|
+
reason="output_hash_changed_after_collect",
|
|
1012
|
+
)
|
|
1013
|
+
elif blocked_reason == "semantic_ingestion.validation_error" and "aliases[" in error_text and "surface" in error_text:
|
|
1014
|
+
agent_event = curator_agent_event(
|
|
1015
|
+
code="agent.curator_alias_surface_without_text",
|
|
1016
|
+
root_cause_code=blocked_reason,
|
|
1017
|
+
next_action=next_action,
|
|
1018
|
+
artifact_path=str(output_path),
|
|
1019
|
+
reason="aliases_surface_without_text",
|
|
1020
|
+
)
|
|
1021
|
+
receipt: JsonObject = {
|
|
1022
|
+
"schema": "medical-notes-workbench.note-semantic-ingestion-apply-receipt.v1",
|
|
1023
|
+
"status": "blocked",
|
|
1024
|
+
"blocked_reason": blocked_reason,
|
|
1025
|
+
"error": error_text,
|
|
1026
|
+
"next_action": next_action,
|
|
1027
|
+
"agent_notice": agent_output_ignored_notice(next_action),
|
|
1028
|
+
"note_path": str(by_work_id[work_id].get("note_path", "")),
|
|
1029
|
+
"content_hash": str(by_work_id[work_id].get("content_hash", "")),
|
|
1030
|
+
"error_context": {
|
|
1031
|
+
"phase": "vocabulary_curation",
|
|
1032
|
+
"blocked_reason": blocked_reason,
|
|
1033
|
+
"root_cause": blocked_reason,
|
|
1034
|
+
"affected_artifact": str(output_path),
|
|
1035
|
+
"error_summary": error_text,
|
|
1036
|
+
"suggested_fix": next_action,
|
|
1037
|
+
"next_action": next_action,
|
|
1038
|
+
"retry_scope": "single_curator_work_item",
|
|
1039
|
+
},
|
|
1040
|
+
}
|
|
1041
|
+
if agent_event is not None:
|
|
1042
|
+
receipt["agent_event"] = agent_event
|
|
1043
|
+
status = _json_text_field(receipt, "status", "blocked")
|
|
1044
|
+
if status == "applied":
|
|
1045
|
+
applied_count += 1
|
|
1046
|
+
else:
|
|
1047
|
+
blocked_count += 1
|
|
1048
|
+
receipt = JsonObjectAdapter.validate_python(dict(receipt))
|
|
1049
|
+
receipt_next_action = _json_text_field(
|
|
1050
|
+
receipt,
|
|
1051
|
+
"next_action",
|
|
1052
|
+
"Resolver o item bloqueado e repetir apply-curator-batch.",
|
|
1053
|
+
)
|
|
1054
|
+
receipt["agent_notice"] = agent_output_ignored_notice(receipt_next_action)
|
|
1055
|
+
receipt_item: JsonObject = {
|
|
1056
|
+
"work_id": work_id,
|
|
1057
|
+
"output_path": str(output_path),
|
|
1058
|
+
"status": status,
|
|
1059
|
+
"blocked_reason": _json_text_field(receipt, "blocked_reason"),
|
|
1060
|
+
"note_path": _json_text_field(
|
|
1061
|
+
receipt,
|
|
1062
|
+
"note_path",
|
|
1063
|
+
str(by_work_id[work_id].get("note_path", "")),
|
|
1064
|
+
),
|
|
1065
|
+
"content_hash": (
|
|
1066
|
+
_json_text_field(receipt, "content_hash")
|
|
1067
|
+
or _json_text_field(receipt, "expected_hash")
|
|
1068
|
+
or str(by_work_id[work_id].get("content_hash", ""))
|
|
1069
|
+
),
|
|
1070
|
+
"receipt": receipt,
|
|
1071
|
+
}
|
|
1072
|
+
if status != "applied":
|
|
1073
|
+
receipt_item["agent_notice"] = _json_text_field(receipt, "agent_notice", agent_output_ignored_notice())
|
|
1074
|
+
agent_event_payload = _json_object_field(receipt, "agent_event")
|
|
1075
|
+
if agent_event_payload:
|
|
1076
|
+
agent_events.append(agent_event_payload)
|
|
1077
|
+
receipts.append(
|
|
1078
|
+
receipt_item
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
result: JsonObject = {
|
|
1082
|
+
"schema": VOCABULARY_CURATOR_BATCH_RECEIPT_SCHEMA,
|
|
1083
|
+
"phase": "vocabulary_curation",
|
|
1084
|
+
"status": "completed" if blocked_count == 0 else "completed_with_blockers",
|
|
1085
|
+
"batch_id": typed_plan.batch_id,
|
|
1086
|
+
"db_path": str(db_path),
|
|
1087
|
+
"prompt_eval": prompt_eval,
|
|
1088
|
+
"plan_item_count": len(by_work_id),
|
|
1089
|
+
"manifest_item_count": len(manifest_items),
|
|
1090
|
+
"applied_count": applied_count,
|
|
1091
|
+
"blocked_count": blocked_count,
|
|
1092
|
+
"agent_events": agent_events,
|
|
1093
|
+
"agent_output_ignored_notices": ignored_notices,
|
|
1094
|
+
"items": receipts,
|
|
1095
|
+
}
|
|
1096
|
+
first_blocked = next((item for item in receipts if item.get("status") != "applied"), None)
|
|
1097
|
+
if first_blocked is not None:
|
|
1098
|
+
first_receipt = first_blocked.get("receipt") if isinstance(first_blocked.get("receipt"), dict) else {}
|
|
1099
|
+
next_action = str(first_receipt.get("next_action") or "Resolver os itens bloqueados e repetir apply-curator-batch.")
|
|
1100
|
+
blocked_reason = str(first_blocked.get("blocked_reason") or "")
|
|
1101
|
+
result["next_action"] = next_action
|
|
1102
|
+
result["agent_notice"] = str(first_blocked.get("agent_notice") or agent_output_ignored_notice(next_action))
|
|
1103
|
+
result["required_inputs"] = ["blocked_curator_work_items"]
|
|
1104
|
+
result["human_decision_required"] = False
|
|
1105
|
+
result["error_context"] = {
|
|
1106
|
+
"phase": "vocabulary_curation",
|
|
1107
|
+
"blocked_reason": blocked_reason,
|
|
1108
|
+
"root_cause": blocked_reason,
|
|
1109
|
+
"affected_artifact": str(first_blocked.get("output_path") or ""),
|
|
1110
|
+
"error_summary": str(first_receipt.get("error") or first_receipt.get("blocked_reason") or ""),
|
|
1111
|
+
"suggested_fix": next_action,
|
|
1112
|
+
"next_action": next_action,
|
|
1113
|
+
"retry_scope": "blocked_curator_work_items",
|
|
1114
|
+
}
|
|
1115
|
+
return _finalize_curator_apply_receipt(result)
|