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,949 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
9
|
+
|
|
10
|
+
from mednotes.domains.wiki.capabilities.related_notes.related_notes import (
|
|
11
|
+
recover_related_notes_export_operation_result,
|
|
12
|
+
sync_related_notes_operation_result,
|
|
13
|
+
)
|
|
14
|
+
from mednotes.domains.wiki.capabilities.specialist.specialist_receipts import (
|
|
15
|
+
validate_specialist_task_run_receipt_attestation,
|
|
16
|
+
)
|
|
17
|
+
from mednotes.domains.wiki.common import ValidationError
|
|
18
|
+
from mednotes.domains.wiki.contracts.effect_payloads import (
|
|
19
|
+
LinkEffectBlockedOutcome,
|
|
20
|
+
LinkEffectCompletedOutcome,
|
|
21
|
+
LinkEffectFailedOutcome,
|
|
22
|
+
LinkEffectGraphBlockedOutcome,
|
|
23
|
+
LinkEffectLinkerBlockedOutcome,
|
|
24
|
+
LinkSubworkflowEffectPayload,
|
|
25
|
+
LinkWorkflowRunEffectPayload,
|
|
26
|
+
RelatedNotesBlockedOutcome,
|
|
27
|
+
RelatedNotesExportCompletedOutcome,
|
|
28
|
+
RelatedNotesExportEffectPayload,
|
|
29
|
+
RelatedNotesQuotaWaitOutcome,
|
|
30
|
+
RelatedNotesRecoveryEffectPayload,
|
|
31
|
+
RelatedNotesSyncCompletedOutcome,
|
|
32
|
+
RelatedNotesSyncEffectPayload,
|
|
33
|
+
RelatedNotesSyncSectionEffectPayload,
|
|
34
|
+
RelatedNotesSyncWarningOutcome,
|
|
35
|
+
SpecialistModelBlockedOutcome,
|
|
36
|
+
SpecialistModelCapacityWaitOutcome,
|
|
37
|
+
SpecialistModelCompletedOutcome,
|
|
38
|
+
SpecialistModelEffectPayload,
|
|
39
|
+
WaitExternalEffectOutcome,
|
|
40
|
+
WaitExternalEffectPayload,
|
|
41
|
+
)
|
|
42
|
+
from mednotes.domains.wiki.flows.link.linking import run_linker
|
|
43
|
+
from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter, JsonValue
|
|
44
|
+
from mednotes.kernel.effect_executor import WorkflowEffectExecutionContext
|
|
45
|
+
from mednotes.kernel.effects import (
|
|
46
|
+
WorkflowEffect,
|
|
47
|
+
WorkflowEffectKind,
|
|
48
|
+
WorkflowEffectResult,
|
|
49
|
+
WorkflowEffectStatus,
|
|
50
|
+
)
|
|
51
|
+
from mednotes.kernel.workflow import VersionControlSafety
|
|
52
|
+
|
|
53
|
+
RELATED_NOTES_EXTERNAL_RETRY_REASONS = frozenset(
|
|
54
|
+
{
|
|
55
|
+
"related_notes_headless_quota_exhausted",
|
|
56
|
+
"related_notes_headless_time_budget_exhausted",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class WaitExternalEffectAdapter:
|
|
63
|
+
def run(self, effect: WorkflowEffect, context: WorkflowEffectExecutionContext) -> WorkflowEffectResult:
|
|
64
|
+
del context
|
|
65
|
+
if effect.kind != WorkflowEffectKind.WAIT_EXTERNAL:
|
|
66
|
+
raise ValueError(f"WaitExternalEffectAdapter cannot run effect kind: {effect.kind.value}")
|
|
67
|
+
try:
|
|
68
|
+
typed = WaitExternalEffectPayload.from_effect_payload(effect.payload)
|
|
69
|
+
except PydanticValidationError as exc:
|
|
70
|
+
return _effect_payload_contract_block(
|
|
71
|
+
effect,
|
|
72
|
+
payload_schema="wait_external_effect_payload",
|
|
73
|
+
exc=exc,
|
|
74
|
+
operation_payload=_safe_operation_payload(effect.payload),
|
|
75
|
+
)
|
|
76
|
+
recovery_state = typed.related_notes_recovery_state
|
|
77
|
+
if recovery_state is not None:
|
|
78
|
+
reason = recovery_state.blocked_reason or typed.blocked_reason or effect.target
|
|
79
|
+
next_action = recovery_state.next_action or typed.next_action
|
|
80
|
+
else:
|
|
81
|
+
reason = typed.blocked_reason or typed.wait_target or effect.target or "workflow_external_wait"
|
|
82
|
+
next_action = typed.next_action
|
|
83
|
+
resume_action = effect.resume_action or next_action or "Aguardar a condição externa e retomar pela rota oficial."
|
|
84
|
+
return WorkflowEffectResult(
|
|
85
|
+
effect=effect,
|
|
86
|
+
status=WorkflowEffectStatus.WAITING_EXTERNAL,
|
|
87
|
+
outcome=WaitExternalEffectOutcome(reason_code=reason),
|
|
88
|
+
public_summary="Workflow aguardando condição externa para continuar.",
|
|
89
|
+
developer_summary=f"Workflow effect is waiting for external condition: {reason}.",
|
|
90
|
+
payload=typed.to_payload(),
|
|
91
|
+
next_action=resume_action,
|
|
92
|
+
resume_action=resume_action,
|
|
93
|
+
error_context={
|
|
94
|
+
"phase": effect.origin_state,
|
|
95
|
+
"blocked_reason": reason,
|
|
96
|
+
"root_cause": reason,
|
|
97
|
+
"affected_artifact": effect.target,
|
|
98
|
+
"next_action": resume_action,
|
|
99
|
+
"retry_scope": "wait_external_resume",
|
|
100
|
+
"human_decision_required": False,
|
|
101
|
+
"wait_target": typed.wait_target,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class RelatedNotesEffectAdapter:
|
|
108
|
+
config: object
|
|
109
|
+
recover_related_notes_export_fn: Callable[..., object] = recover_related_notes_export_operation_result
|
|
110
|
+
sync_related_notes_fn: Callable[..., object] = sync_related_notes_operation_result
|
|
111
|
+
|
|
112
|
+
def run(self, effect: WorkflowEffect, context: WorkflowEffectExecutionContext) -> WorkflowEffectResult:
|
|
113
|
+
del context
|
|
114
|
+
if effect.kind != WorkflowEffectKind.RUN_SUBWORKFLOW:
|
|
115
|
+
raise ValueError(f"RelatedNotesEffectAdapter cannot run effect kind: {effect.kind.value}")
|
|
116
|
+
match effect.target:
|
|
117
|
+
case "related_notes.export" | "related_notes_export":
|
|
118
|
+
return self._recover_export(effect)
|
|
119
|
+
case "related_notes.section":
|
|
120
|
+
return self._sync_section(effect)
|
|
121
|
+
case _:
|
|
122
|
+
raise ValueError(f"RelatedNotesEffectAdapter cannot run effect target: {effect.target}")
|
|
123
|
+
|
|
124
|
+
def _recover_export(self, effect: WorkflowEffect) -> WorkflowEffectResult:
|
|
125
|
+
try:
|
|
126
|
+
intent = RelatedNotesExportEffectPayload.from_effect_payload(effect.payload)
|
|
127
|
+
except PydanticValidationError as exc:
|
|
128
|
+
return _effect_payload_contract_block(
|
|
129
|
+
effect,
|
|
130
|
+
payload_schema="related_notes_export_effect_payload",
|
|
131
|
+
exc=exc,
|
|
132
|
+
operation_payload=_safe_operation_payload(effect.payload),
|
|
133
|
+
)
|
|
134
|
+
result = self.recover_related_notes_export_fn(
|
|
135
|
+
self.config,
|
|
136
|
+
export_path=_optional_path(intent.export_path),
|
|
137
|
+
mode=intent.mode or "auto",
|
|
138
|
+
workflow=effect.workflow,
|
|
139
|
+
run_id=effect.run_id,
|
|
140
|
+
)
|
|
141
|
+
try:
|
|
142
|
+
operation_payload = _json_object(result)
|
|
143
|
+
typed = RelatedNotesRecoveryEffectPayload.from_operation_payload(operation_payload)
|
|
144
|
+
except PydanticValidationError as exc:
|
|
145
|
+
return _effect_payload_contract_block(
|
|
146
|
+
effect,
|
|
147
|
+
payload_schema="related_notes_recovery_effect_payload",
|
|
148
|
+
exc=exc,
|
|
149
|
+
operation_payload=_safe_operation_payload(result),
|
|
150
|
+
)
|
|
151
|
+
recovery_state = typed.related_notes_recovery_state
|
|
152
|
+
reason = str(
|
|
153
|
+
(recovery_state.blocked_reason if recovery_state is not None else "") or typed.blocked_reason or ""
|
|
154
|
+
)
|
|
155
|
+
next_action = str(typed.next_action or (recovery_state.next_action if recovery_state is not None else "") or "")
|
|
156
|
+
if (
|
|
157
|
+
recovery_state is not None
|
|
158
|
+
and recovery_state.status == "waiting_for_retry"
|
|
159
|
+
and reason in RELATED_NOTES_EXTERNAL_RETRY_REASONS
|
|
160
|
+
):
|
|
161
|
+
return WorkflowEffectResult(
|
|
162
|
+
effect=effect,
|
|
163
|
+
status=WorkflowEffectStatus.WAITING_EXTERNAL,
|
|
164
|
+
outcome=RelatedNotesQuotaWaitOutcome(reason_code=reason),
|
|
165
|
+
public_summary="Notas Relacionadas aguardando cota externa.",
|
|
166
|
+
developer_summary="Related Notes recovery paused with reusable progress.",
|
|
167
|
+
payload=typed.to_payload(),
|
|
168
|
+
next_action=next_action,
|
|
169
|
+
resume_action=next_action or effect.resume_action,
|
|
170
|
+
)
|
|
171
|
+
if typed.status in {"recovered", "completed"}:
|
|
172
|
+
if effect.requires_receipt and typed.receipt is None:
|
|
173
|
+
return _effect_payload_contract_block_from_message(
|
|
174
|
+
effect,
|
|
175
|
+
payload_schema="related_notes_recovery_effect_payload",
|
|
176
|
+
loc="receipt",
|
|
177
|
+
message="completed related notes recovery effect payload requires receipt",
|
|
178
|
+
operation_payload=typed.to_payload(),
|
|
179
|
+
)
|
|
180
|
+
return WorkflowEffectResult(
|
|
181
|
+
effect=effect,
|
|
182
|
+
status=WorkflowEffectStatus.COMPLETED,
|
|
183
|
+
outcome=RelatedNotesExportCompletedOutcome(),
|
|
184
|
+
public_summary="Export de Notas Relacionadas atualizado.",
|
|
185
|
+
developer_summary="Related Notes export recovery completed.",
|
|
186
|
+
payload=typed.to_payload(),
|
|
187
|
+
receipt=typed.receipt if effect.requires_receipt else None,
|
|
188
|
+
)
|
|
189
|
+
return WorkflowEffectResult(
|
|
190
|
+
effect=effect,
|
|
191
|
+
status=WorkflowEffectStatus.BLOCKED,
|
|
192
|
+
outcome=RelatedNotesBlockedOutcome(reason_code=reason or "related_notes_blocked"),
|
|
193
|
+
public_summary="Notas Relacionadas bloqueadas antes de alterar a Wiki.",
|
|
194
|
+
developer_summary="Related Notes recovery returned a blocking status.",
|
|
195
|
+
payload=typed.to_payload(),
|
|
196
|
+
next_action=next_action or "Corrigir o bloqueio de Related Notes pela rota oficial.",
|
|
197
|
+
error_context={"blocked_reason": reason or "related_notes_blocked"},
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _sync_section(self, effect: WorkflowEffect) -> WorkflowEffectResult:
|
|
201
|
+
try:
|
|
202
|
+
intent = RelatedNotesSyncSectionEffectPayload.from_effect_payload(effect.payload)
|
|
203
|
+
except PydanticValidationError as exc:
|
|
204
|
+
return _effect_payload_contract_block(
|
|
205
|
+
effect,
|
|
206
|
+
payload_schema="related_notes_sync_section_effect_payload",
|
|
207
|
+
exc=exc,
|
|
208
|
+
operation_payload=_safe_operation_payload(effect.payload),
|
|
209
|
+
)
|
|
210
|
+
result = self.sync_related_notes_fn(
|
|
211
|
+
self.config,
|
|
212
|
+
export_path=_optional_path(intent.export_path),
|
|
213
|
+
apply=intent.apply,
|
|
214
|
+
# Markdown .bak files are retired; vault guard/version control is
|
|
215
|
+
# the recovery mechanism for workflow-driven mutations.
|
|
216
|
+
backup=False,
|
|
217
|
+
receipt_path=_optional_path(intent.receipt_path),
|
|
218
|
+
min_score=float(intent.min_score),
|
|
219
|
+
max_links=intent.max_links,
|
|
220
|
+
max_age_hours=float(intent.max_age_hours),
|
|
221
|
+
)
|
|
222
|
+
try:
|
|
223
|
+
operation_payload = _json_object(result)
|
|
224
|
+
typed = RelatedNotesSyncEffectPayload.from_operation_payload(operation_payload)
|
|
225
|
+
except PydanticValidationError as exc:
|
|
226
|
+
return _effect_payload_contract_block(
|
|
227
|
+
effect,
|
|
228
|
+
payload_schema="related_notes_sync_effect_payload",
|
|
229
|
+
exc=exc,
|
|
230
|
+
operation_payload=_safe_operation_payload(result),
|
|
231
|
+
)
|
|
232
|
+
if typed.status == "completed":
|
|
233
|
+
safety = _version_control_safety_or_block(
|
|
234
|
+
effect,
|
|
235
|
+
typed.to_payload(),
|
|
236
|
+
changed_file_count=typed.applied_note_count,
|
|
237
|
+
outcome=RelatedNotesBlockedOutcome(reason_code="version_control_safety_evidence_missing"),
|
|
238
|
+
)
|
|
239
|
+
if isinstance(safety, WorkflowEffectResult):
|
|
240
|
+
return safety
|
|
241
|
+
return WorkflowEffectResult(
|
|
242
|
+
effect=effect,
|
|
243
|
+
status=WorkflowEffectStatus.COMPLETED,
|
|
244
|
+
outcome=RelatedNotesSyncCompletedOutcome(),
|
|
245
|
+
public_summary="Notas Relacionadas sincronizadas.",
|
|
246
|
+
developer_summary="Related Notes sync completed.",
|
|
247
|
+
payload=typed.to_payload(),
|
|
248
|
+
receipt=typed.receipt,
|
|
249
|
+
)
|
|
250
|
+
if typed.status in {"preview_ready", "completed_with_warnings"}:
|
|
251
|
+
return WorkflowEffectResult(
|
|
252
|
+
effect=effect,
|
|
253
|
+
status=WorkflowEffectStatus.COMPLETED_WITH_WARNINGS,
|
|
254
|
+
outcome=RelatedNotesSyncWarningOutcome(reason_code=str(typed.blocked_reason or typed.status)),
|
|
255
|
+
public_summary="Notas Relacionadas conferidas com aviso.",
|
|
256
|
+
developer_summary="Related Notes sync returned a non-mutating or warning result.",
|
|
257
|
+
payload=typed.to_payload(),
|
|
258
|
+
receipt=typed.receipt,
|
|
259
|
+
next_action=str(typed.next_action or "Revisar a previa de Notas Relacionadas."),
|
|
260
|
+
)
|
|
261
|
+
return WorkflowEffectResult(
|
|
262
|
+
effect=effect,
|
|
263
|
+
status=WorkflowEffectStatus.BLOCKED,
|
|
264
|
+
outcome=RelatedNotesBlockedOutcome(reason_code=str(typed.blocked_reason or "related_notes_blocked")),
|
|
265
|
+
public_summary="Notas Relacionadas nao foram sincronizadas.",
|
|
266
|
+
developer_summary="Related Notes sync returned blocked status.",
|
|
267
|
+
payload=typed.to_payload(),
|
|
268
|
+
next_action=str(typed.next_action or "Atualizar o export do Related Notes e repetir a rota oficial."),
|
|
269
|
+
error_context={"blocked_reason": str(typed.blocked_reason or "related_notes_blocked")},
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _optional_path(value: object) -> Path | None:
|
|
274
|
+
text = _text_or_empty(value).strip()
|
|
275
|
+
return Path(text) if text else None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _version_control_safety_or_block(
|
|
279
|
+
effect: WorkflowEffect,
|
|
280
|
+
operation_payload: JsonObject,
|
|
281
|
+
*,
|
|
282
|
+
changed_file_count: int,
|
|
283
|
+
outcome: ContractModel,
|
|
284
|
+
) -> VersionControlSafety | WorkflowEffectResult:
|
|
285
|
+
"""Accept only real guard evidence or explicit non-mutation contracts.
|
|
286
|
+
|
|
287
|
+
Adapters must not fabricate safety from `mutates_resources`, apply mode or
|
|
288
|
+
counters. A mutation with changed files needs safety copied from an
|
|
289
|
+
operation payload or receipt produced by the guarded runtime.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
safety = _version_control_safety_from_payload(operation_payload)
|
|
293
|
+
if safety is None:
|
|
294
|
+
if changed_file_count > 0 or effect.mutates_resources:
|
|
295
|
+
return _version_control_safety_evidence_missing(
|
|
296
|
+
effect,
|
|
297
|
+
operation_payload=operation_payload,
|
|
298
|
+
outcome=outcome,
|
|
299
|
+
)
|
|
300
|
+
return _no_resource_mutation_safety()
|
|
301
|
+
if safety.no_resource_mutation and changed_file_count > 0:
|
|
302
|
+
return _version_control_safety_evidence_missing(
|
|
303
|
+
effect,
|
|
304
|
+
operation_payload=operation_payload,
|
|
305
|
+
outcome=outcome,
|
|
306
|
+
)
|
|
307
|
+
if safety.changed_file_count != changed_file_count:
|
|
308
|
+
return _version_control_safety_evidence_mismatch(
|
|
309
|
+
effect,
|
|
310
|
+
operation_payload=operation_payload,
|
|
311
|
+
outcome=outcome,
|
|
312
|
+
expected_changed_file_count=changed_file_count,
|
|
313
|
+
evidence_changed_file_count=safety.changed_file_count,
|
|
314
|
+
)
|
|
315
|
+
return safety
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _version_control_safety_from_payload(payload: JsonObject) -> VersionControlSafety | None:
|
|
319
|
+
for candidate in (
|
|
320
|
+
_json_field(payload, "version_control_safety"),
|
|
321
|
+
_json_field(_json_object_or_empty(_json_field(payload, "receipt")), "version_control_safety"),
|
|
322
|
+
_json_field(_json_object_or_empty(_json_field(payload, "guard_receipt")), "version_control_safety"),
|
|
323
|
+
):
|
|
324
|
+
if isinstance(candidate, dict):
|
|
325
|
+
return VersionControlSafety.model_validate(candidate)
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _version_control_safety_evidence_missing(
|
|
330
|
+
effect: WorkflowEffect,
|
|
331
|
+
*,
|
|
332
|
+
operation_payload: JsonObject,
|
|
333
|
+
outcome: ContractModel,
|
|
334
|
+
) -> WorkflowEffectResult:
|
|
335
|
+
return WorkflowEffectResult(
|
|
336
|
+
effect=effect,
|
|
337
|
+
status=WorkflowEffectStatus.BLOCKED,
|
|
338
|
+
outcome=outcome,
|
|
339
|
+
public_summary="A alteração foi bloqueada porque faltou comprovante de proteção do recurso.",
|
|
340
|
+
developer_summary="Mutating effect result did not carry typed version_control_safety evidence.",
|
|
341
|
+
payload={
|
|
342
|
+
"schema": "medical-notes-workbench.version-control-safety-error.v1",
|
|
343
|
+
"operation_payload": operation_payload,
|
|
344
|
+
},
|
|
345
|
+
next_action="Reexecutar pela rota oficial com guard/receipt de versionamento ativo.",
|
|
346
|
+
error_context={
|
|
347
|
+
"root_cause": "version_control_safety_evidence_missing",
|
|
348
|
+
"affected_artifact": effect.target,
|
|
349
|
+
"retry_scope": "effect_adapter",
|
|
350
|
+
},
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _version_control_safety_evidence_mismatch(
|
|
355
|
+
effect: WorkflowEffect,
|
|
356
|
+
*,
|
|
357
|
+
operation_payload: JsonObject,
|
|
358
|
+
outcome: ContractModel,
|
|
359
|
+
expected_changed_file_count: int,
|
|
360
|
+
evidence_changed_file_count: int,
|
|
361
|
+
) -> WorkflowEffectResult:
|
|
362
|
+
return WorkflowEffectResult(
|
|
363
|
+
effect=effect,
|
|
364
|
+
status=WorkflowEffectStatus.BLOCKED,
|
|
365
|
+
outcome=outcome,
|
|
366
|
+
public_summary="A alteração foi bloqueada porque o comprovante de proteção não bate com o resultado.",
|
|
367
|
+
developer_summary="version_control_safety changed_file_count did not match the mutating effect result.",
|
|
368
|
+
payload={
|
|
369
|
+
"schema": "medical-notes-workbench.version-control-safety-error.v1",
|
|
370
|
+
"operation_payload": operation_payload,
|
|
371
|
+
"expected_changed_file_count": expected_changed_file_count,
|
|
372
|
+
"evidence_changed_file_count": evidence_changed_file_count,
|
|
373
|
+
},
|
|
374
|
+
next_action="Reexecutar pela rota oficial; se o mismatch repetir, tratar como bug de contrato do adapter.",
|
|
375
|
+
error_context={
|
|
376
|
+
"root_cause": "version_control_safety_evidence_mismatch",
|
|
377
|
+
"affected_artifact": effect.target,
|
|
378
|
+
"retry_scope": "effect_adapter",
|
|
379
|
+
"expected_changed_file_count": expected_changed_file_count,
|
|
380
|
+
"evidence_changed_file_count": evidence_changed_file_count,
|
|
381
|
+
},
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _no_resource_mutation_safety() -> VersionControlSafety:
|
|
386
|
+
return VersionControlSafety(
|
|
387
|
+
resource_guard_active=False,
|
|
388
|
+
run_start_seen=False,
|
|
389
|
+
run_finish_seen=False,
|
|
390
|
+
restore_point_before="",
|
|
391
|
+
restore_point_after="",
|
|
392
|
+
sync_status="not_checked",
|
|
393
|
+
backup_online="not_checked",
|
|
394
|
+
direct_mutation_forbidden=True,
|
|
395
|
+
mutation_without_guard=False,
|
|
396
|
+
rollback_declared=False,
|
|
397
|
+
no_resource_mutation=True,
|
|
398
|
+
changed_file_count=0,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _link_changed_file_count(payload: JsonObject) -> int:
|
|
403
|
+
files_changed = _int_field(payload, "files_changed", 0)
|
|
404
|
+
changed_files = _json_field(payload, "changed_files", [])
|
|
405
|
+
if isinstance(changed_files, list):
|
|
406
|
+
return max(files_changed, len(changed_files))
|
|
407
|
+
return files_changed
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _child_effect_version_control_safety(
|
|
411
|
+
parent_safety: VersionControlSafety,
|
|
412
|
+
*,
|
|
413
|
+
changed_file_count: int,
|
|
414
|
+
mutates: bool,
|
|
415
|
+
) -> VersionControlSafety:
|
|
416
|
+
"""Project an already-active parent guard onto one child effect result.
|
|
417
|
+
|
|
418
|
+
The child runtime owns the mutation count; the parent safety owns evidence
|
|
419
|
+
that a rollback route/guard exists. This avoids accepting legacy receipts
|
|
420
|
+
while also avoiding a false mismatch when the pre-link guard was captured
|
|
421
|
+
before the link effect mutated files.
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
mutated = mutates or changed_file_count > 0
|
|
425
|
+
payload = parent_safety.to_payload()
|
|
426
|
+
payload["changed_file_count"] = changed_file_count
|
|
427
|
+
payload["no_resource_mutation"] = not mutated
|
|
428
|
+
payload["mutation_without_guard"] = bool(mutated and not parent_safety.rollback_declared)
|
|
429
|
+
return VersionControlSafety.model_validate(payload)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _json_object_or_empty(value: JsonValue) -> JsonObject:
|
|
433
|
+
return _json_object(value) if isinstance(value, dict) else {}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@dataclass
|
|
437
|
+
class LinkWorkflowEffectAdapter:
|
|
438
|
+
config: object
|
|
439
|
+
run_linker_fn: Callable[..., object] = run_linker
|
|
440
|
+
|
|
441
|
+
def run(self, effect: WorkflowEffect, context: WorkflowEffectExecutionContext) -> WorkflowEffectResult:
|
|
442
|
+
del context
|
|
443
|
+
if effect.kind != WorkflowEffectKind.RUN_SUBWORKFLOW or effect.target not in {
|
|
444
|
+
"/mednotes:link",
|
|
445
|
+
"/mednotes:link-body",
|
|
446
|
+
}:
|
|
447
|
+
raise ValueError(f"LinkWorkflowEffectAdapter cannot run effect: {effect.kind.value}:{effect.target}")
|
|
448
|
+
try:
|
|
449
|
+
intent = LinkWorkflowRunEffectPayload.from_effect_payload(effect.payload)
|
|
450
|
+
except PydanticValidationError as exc:
|
|
451
|
+
return _effect_payload_contract_block(
|
|
452
|
+
effect,
|
|
453
|
+
payload_schema="link_workflow_run_effect_payload",
|
|
454
|
+
exc=exc,
|
|
455
|
+
operation_payload=_safe_operation_payload(effect.payload),
|
|
456
|
+
)
|
|
457
|
+
if intent.apply and not intent.diagnosis_path.strip():
|
|
458
|
+
return _effect_payload_contract_block_from_message(
|
|
459
|
+
effect,
|
|
460
|
+
payload_schema="link_workflow_run_effect_payload",
|
|
461
|
+
loc="diagnosis_path",
|
|
462
|
+
message="link workflow apply effects require diagnosis_path",
|
|
463
|
+
operation_payload=_safe_operation_payload(effect.payload),
|
|
464
|
+
)
|
|
465
|
+
mode: Literal["apply", "diagnose"] = "apply" if intent.apply else "diagnose"
|
|
466
|
+
include_related_notes = effect.target != "/mednotes:link-body" and not intent.no_related_notes
|
|
467
|
+
parent_safety = intent.version_control_safety
|
|
468
|
+
raw = self.run_linker_fn(
|
|
469
|
+
self.config,
|
|
470
|
+
diagnose=intent.diagnose,
|
|
471
|
+
apply=intent.apply,
|
|
472
|
+
diagnosis_path=_optional_path(intent.diagnosis_path),
|
|
473
|
+
receipt_path=_optional_path(intent.receipt_path),
|
|
474
|
+
trigger_context_path=_optional_path(intent.trigger_context_path),
|
|
475
|
+
include_related_notes=include_related_notes,
|
|
476
|
+
# The FSM effect contract no longer carries adjacent-backup policy.
|
|
477
|
+
backup=False,
|
|
478
|
+
force_diagnose=intent.force_diagnose,
|
|
479
|
+
llm_disambiguation=intent.llm_disambiguation or "auto",
|
|
480
|
+
llm_model=_optional_text(intent.llm_model),
|
|
481
|
+
llm_timeout=intent.llm_timeout,
|
|
482
|
+
version_control_guard_active=parent_safety is not None,
|
|
483
|
+
)
|
|
484
|
+
try:
|
|
485
|
+
operation_payload = _json_object(raw)
|
|
486
|
+
changed_file_count = _link_changed_file_count(operation_payload)
|
|
487
|
+
if parent_safety is not None and _json_field(operation_payload, "version_control_safety") == {}:
|
|
488
|
+
enriched = dict(operation_payload)
|
|
489
|
+
enriched["version_control_safety"] = _child_effect_version_control_safety(
|
|
490
|
+
parent_safety,
|
|
491
|
+
changed_file_count=changed_file_count,
|
|
492
|
+
mutates=effect.mutates_resources,
|
|
493
|
+
).to_payload()
|
|
494
|
+
operation_payload = JsonObjectAdapter.validate_python(enriched)
|
|
495
|
+
if parent_safety is not None and _version_control_safety_from_payload(operation_payload) is None:
|
|
496
|
+
enriched = dict(operation_payload)
|
|
497
|
+
enriched["version_control_safety"] = _child_effect_version_control_safety(
|
|
498
|
+
parent_safety,
|
|
499
|
+
changed_file_count=changed_file_count,
|
|
500
|
+
mutates=effect.mutates_resources,
|
|
501
|
+
).to_payload()
|
|
502
|
+
operation_payload = JsonObjectAdapter.validate_python(enriched)
|
|
503
|
+
safety = _version_control_safety_or_block(
|
|
504
|
+
effect,
|
|
505
|
+
operation_payload,
|
|
506
|
+
changed_file_count=changed_file_count,
|
|
507
|
+
outcome=LinkEffectBlockedOutcome(reason_code="version_control_safety_evidence_missing"),
|
|
508
|
+
)
|
|
509
|
+
if isinstance(safety, WorkflowEffectResult):
|
|
510
|
+
return safety
|
|
511
|
+
fsm_payload = _link_fsm_payload_from_raw(
|
|
512
|
+
effect=effect,
|
|
513
|
+
raw=operation_payload,
|
|
514
|
+
mode=mode,
|
|
515
|
+
include_related_notes=include_related_notes,
|
|
516
|
+
version_control_safety=safety,
|
|
517
|
+
)
|
|
518
|
+
fsm_payload = _attach_link_operation_report_details(fsm_payload, operation_payload)
|
|
519
|
+
typed = LinkSubworkflowEffectPayload.model_validate(
|
|
520
|
+
{
|
|
521
|
+
"schema": _json_field(fsm_payload, "schema"),
|
|
522
|
+
"progress_view_model": _json_field(fsm_payload, "progress_view_model"),
|
|
523
|
+
"receipt": _json_field(fsm_payload, "receipt"),
|
|
524
|
+
"reports": _json_field(fsm_payload, "reports"),
|
|
525
|
+
"error_context": _json_field(fsm_payload, "error_context", {}),
|
|
526
|
+
"fsm_payload": fsm_payload,
|
|
527
|
+
}
|
|
528
|
+
)
|
|
529
|
+
except PydanticValidationError as exc:
|
|
530
|
+
return _effect_payload_contract_block(
|
|
531
|
+
effect,
|
|
532
|
+
payload_schema="link_subworkflow_effect_payload",
|
|
533
|
+
exc=exc,
|
|
534
|
+
operation_payload=_safe_operation_payload(raw),
|
|
535
|
+
)
|
|
536
|
+
except ValueError as exc:
|
|
537
|
+
return _effect_payload_contract_block_from_message(
|
|
538
|
+
effect,
|
|
539
|
+
payload_schema="link_subworkflow_effect_payload",
|
|
540
|
+
loc="$",
|
|
541
|
+
message=str(exc),
|
|
542
|
+
operation_payload=_safe_operation_payload(raw),
|
|
543
|
+
)
|
|
544
|
+
status = typed.progress_view_model.status.value
|
|
545
|
+
effect_status = _effect_status_from_progress_status(status)
|
|
546
|
+
reason_code = _link_effect_reason_code(typed.error_context, status=status)
|
|
547
|
+
error_context = dict(typed.error_context)
|
|
548
|
+
if effect_status in {
|
|
549
|
+
WorkflowEffectStatus.BLOCKED,
|
|
550
|
+
WorkflowEffectStatus.FAILED,
|
|
551
|
+
WorkflowEffectStatus.WAITING_EXTERNAL,
|
|
552
|
+
WorkflowEffectStatus.WAITING_HUMAN,
|
|
553
|
+
} and reason_code:
|
|
554
|
+
error_context.setdefault("blocked_reason", reason_code)
|
|
555
|
+
error_context.setdefault("root_cause", reason_code)
|
|
556
|
+
report_summary = _json_field(typed.reports, "summary")
|
|
557
|
+
return WorkflowEffectResult(
|
|
558
|
+
effect=effect,
|
|
559
|
+
status=effect_status,
|
|
560
|
+
outcome=_link_outcome_for_effect_status(effect_status, reason_code=reason_code or status),
|
|
561
|
+
public_summary=_text_or_empty(report_summary),
|
|
562
|
+
developer_summary="Link adapter returned an FSM-first payload.",
|
|
563
|
+
payload=typed.fsm_payload,
|
|
564
|
+
receipt=typed.receipt.to_payload() if effect.requires_receipt else None,
|
|
565
|
+
next_action=_text_or_empty(typed.receipt.next_action),
|
|
566
|
+
resume_action=_text_or_empty(typed.progress_view_model.resume_action),
|
|
567
|
+
error_context=error_context,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
@dataclass
|
|
572
|
+
class WikiSubworkflowEffectAdapter:
|
|
573
|
+
"""Route generic subworkflow effects to the correct Wiki-domain adapter.
|
|
574
|
+
|
|
575
|
+
`WorkflowEffectKind` stays domain-agnostic in the kernel. Wiki-specific
|
|
576
|
+
work such as Related Notes is selected here from `target` plus the typed
|
|
577
|
+
payload, so the FSM keeps one clean executable effect vocabulary.
|
|
578
|
+
"""
|
|
579
|
+
|
|
580
|
+
link_adapter: LinkWorkflowEffectAdapter
|
|
581
|
+
related_notes_adapter: RelatedNotesEffectAdapter
|
|
582
|
+
|
|
583
|
+
def run(self, effect: WorkflowEffect, context: WorkflowEffectExecutionContext) -> WorkflowEffectResult:
|
|
584
|
+
if effect.kind != WorkflowEffectKind.RUN_SUBWORKFLOW:
|
|
585
|
+
raise ValueError(f"WikiSubworkflowEffectAdapter cannot run effect kind: {effect.kind.value}")
|
|
586
|
+
if effect.target in {"/mednotes:link", "/mednotes:link-body"}:
|
|
587
|
+
return self.link_adapter.run(effect, context)
|
|
588
|
+
if effect.target in {"related_notes.export", "related_notes_export", "related_notes.section"}:
|
|
589
|
+
return self.related_notes_adapter.run(effect, context)
|
|
590
|
+
raise ValueError(f"WikiSubworkflowEffectAdapter cannot route effect target: {effect.target}")
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _link_fsm_payload_from_raw(
|
|
594
|
+
*,
|
|
595
|
+
effect: WorkflowEffect,
|
|
596
|
+
raw: JsonObject,
|
|
597
|
+
mode: Literal["apply", "diagnose"],
|
|
598
|
+
include_related_notes: bool,
|
|
599
|
+
version_control_safety: VersionControlSafety,
|
|
600
|
+
) -> JsonObject:
|
|
601
|
+
from mednotes.domains.wiki.flows.link.link_fsm import build_link_fsm_result
|
|
602
|
+
from mednotes.domains.wiki.flows.link.link_runtime_result import link_fsm_facts_from_linker_result
|
|
603
|
+
|
|
604
|
+
result = build_link_fsm_result(
|
|
605
|
+
link_fsm_facts_from_linker_result(
|
|
606
|
+
raw,
|
|
607
|
+
run_id=f"link-{effect.run_id}",
|
|
608
|
+
mode=mode,
|
|
609
|
+
include_related_notes=include_related_notes,
|
|
610
|
+
version_control_safety=version_control_safety,
|
|
611
|
+
)
|
|
612
|
+
).to_payload()
|
|
613
|
+
return _json_object(result)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _attach_link_operation_report_details(fsm_payload: JsonObject, operation_payload: JsonObject) -> JsonObject:
|
|
617
|
+
"""Expose compact child-operation facts inside the child FSM report.
|
|
618
|
+
|
|
619
|
+
Parent workflows still consume `link-fsm-result.v1` as the only operational
|
|
620
|
+
artifact. These details are a typed adapter projection from the raw linker
|
|
621
|
+
result, kept under `reports.details` so callers do not need to parse the
|
|
622
|
+
legacy `link-run.v1` receipt as workflow truth.
|
|
623
|
+
"""
|
|
624
|
+
|
|
625
|
+
details: dict[str, object] = {}
|
|
626
|
+
for key in (
|
|
627
|
+
"related_notes_sync",
|
|
628
|
+
"body_term_linker",
|
|
629
|
+
"reference_repair",
|
|
630
|
+
"reference_repair_apply",
|
|
631
|
+
"graph_audit_before",
|
|
632
|
+
"graph_audit_after",
|
|
633
|
+
"blockers",
|
|
634
|
+
):
|
|
635
|
+
value = _json_field(operation_payload, key)
|
|
636
|
+
if isinstance(value, dict):
|
|
637
|
+
details[key] = _json_object(value)
|
|
638
|
+
elif isinstance(value, list):
|
|
639
|
+
details[key] = list(value)
|
|
640
|
+
for key in (
|
|
641
|
+
"status",
|
|
642
|
+
"phase",
|
|
643
|
+
"blocked_reason",
|
|
644
|
+
"next_action",
|
|
645
|
+
"returncode",
|
|
646
|
+
"diagnosis_path",
|
|
647
|
+
"receipt_path",
|
|
648
|
+
"files_changed",
|
|
649
|
+
"changed_file_count",
|
|
650
|
+
"blocker_count",
|
|
651
|
+
"links_planned",
|
|
652
|
+
"links_rewritten",
|
|
653
|
+
):
|
|
654
|
+
value = _json_field(operation_payload, key)
|
|
655
|
+
if value is not None:
|
|
656
|
+
details[key] = value
|
|
657
|
+
changed_files = _json_field(operation_payload, "changed_files")
|
|
658
|
+
if isinstance(changed_files, list):
|
|
659
|
+
details["changed_files"] = list(changed_files)
|
|
660
|
+
|
|
661
|
+
reports = _json_object_or_empty(_json_field(fsm_payload, "reports"))
|
|
662
|
+
existing_details = _json_object_or_empty(_json_field(reports, "details"))
|
|
663
|
+
reports["details"] = JsonObjectAdapter.validate_python({**existing_details, **details})
|
|
664
|
+
return JsonObjectAdapter.validate_python({**fsm_payload, "reports": reports})
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _effect_status_from_progress_status(status: str) -> WorkflowEffectStatus:
|
|
668
|
+
match status:
|
|
669
|
+
case "completed":
|
|
670
|
+
return WorkflowEffectStatus.COMPLETED
|
|
671
|
+
case "completed_with_warnings":
|
|
672
|
+
return WorkflowEffectStatus.COMPLETED_WITH_WARNINGS
|
|
673
|
+
case "completed_with_link_blockers":
|
|
674
|
+
return WorkflowEffectStatus.BLOCKED
|
|
675
|
+
case "waiting_external":
|
|
676
|
+
return WorkflowEffectStatus.WAITING_EXTERNAL
|
|
677
|
+
case "waiting_human":
|
|
678
|
+
return WorkflowEffectStatus.WAITING_HUMAN
|
|
679
|
+
case "blocked":
|
|
680
|
+
return WorkflowEffectStatus.BLOCKED
|
|
681
|
+
case "failed":
|
|
682
|
+
return WorkflowEffectStatus.FAILED
|
|
683
|
+
case _:
|
|
684
|
+
return WorkflowEffectStatus.FAILED
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _link_effect_reason_code(error_context: JsonObject, *, status: str) -> str:
|
|
688
|
+
"""Preserve linker blocker semantics when converting FSM payloads to effects."""
|
|
689
|
+
|
|
690
|
+
for value in (
|
|
691
|
+
_json_field(error_context, "blocked_reason"),
|
|
692
|
+
_json_field(error_context, "root_cause"),
|
|
693
|
+
):
|
|
694
|
+
text = _text_or_empty(value)
|
|
695
|
+
if text:
|
|
696
|
+
return text
|
|
697
|
+
if status == "completed_with_link_blockers":
|
|
698
|
+
return "link_plan_blocked"
|
|
699
|
+
return ""
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _link_outcome_for_effect_status(status: WorkflowEffectStatus, *, reason_code: str = "") -> ContractModel:
|
|
703
|
+
"""Map link FSM status to link-domain outcomes, not generic status buckets."""
|
|
704
|
+
|
|
705
|
+
match status:
|
|
706
|
+
case WorkflowEffectStatus.COMPLETED:
|
|
707
|
+
return LinkEffectCompletedOutcome()
|
|
708
|
+
case WorkflowEffectStatus.COMPLETED_WITH_WARNINGS:
|
|
709
|
+
return LinkEffectLinkerBlockedOutcome(reason_code=reason_code)
|
|
710
|
+
case WorkflowEffectStatus.WAITING_EXTERNAL:
|
|
711
|
+
return LinkEffectBlockedOutcome(reason_code=reason_code)
|
|
712
|
+
case WorkflowEffectStatus.BLOCKED:
|
|
713
|
+
if reason_code in {"graph_blocked", "graph_blockers", "link_plan_blocked"}:
|
|
714
|
+
return LinkEffectGraphBlockedOutcome()
|
|
715
|
+
if reason_code in {"linker_blocked", "body_term_linker", "related_notes_blocked"}:
|
|
716
|
+
return LinkEffectLinkerBlockedOutcome(reason_code=reason_code)
|
|
717
|
+
return LinkEffectBlockedOutcome(reason_code=reason_code)
|
|
718
|
+
case WorkflowEffectStatus.FAILED:
|
|
719
|
+
return LinkEffectFailedOutcome(reason_code=reason_code)
|
|
720
|
+
case WorkflowEffectStatus.SKIPPED:
|
|
721
|
+
return LinkEffectBlockedOutcome(reason_code=reason_code or "skipped")
|
|
722
|
+
case _:
|
|
723
|
+
return LinkEffectFailedOutcome(reason_code=reason_code or status.value)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
@dataclass
|
|
727
|
+
class SpecialistModelEffectAdapter:
|
|
728
|
+
runner: Callable[[WorkflowEffect], object] | None = None
|
|
729
|
+
|
|
730
|
+
def run(self, effect: WorkflowEffect, context: WorkflowEffectExecutionContext) -> WorkflowEffectResult:
|
|
731
|
+
del context
|
|
732
|
+
if effect.kind != WorkflowEffectKind.CALL_SPECIALIST_MODEL:
|
|
733
|
+
raise ValueError(f"SpecialistModelEffectAdapter cannot run effect kind: {effect.kind.value}")
|
|
734
|
+
if self.runner is None:
|
|
735
|
+
return WorkflowEffectResult(
|
|
736
|
+
effect=effect,
|
|
737
|
+
status=WorkflowEffectStatus.WAITING_AGENT,
|
|
738
|
+
outcome=SpecialistModelBlockedOutcome(reason_code="specialist_agent_required"),
|
|
739
|
+
public_summary="Aguardando modelo especializado antes de continuar.",
|
|
740
|
+
developer_summary=(
|
|
741
|
+
"No Python runner is available; the agent must execute the specialist effect through "
|
|
742
|
+
"agent_directive.control instead of treating this as external quota."
|
|
743
|
+
),
|
|
744
|
+
payload={"model_policy": dict(effect.model_policy)},
|
|
745
|
+
next_action="Executar a chamada ao especialista pela rota oficial do agente.",
|
|
746
|
+
)
|
|
747
|
+
raw_result = self.runner(effect)
|
|
748
|
+
if not isinstance(raw_result, dict):
|
|
749
|
+
return _effect_payload_contract_block_from_message(
|
|
750
|
+
effect,
|
|
751
|
+
payload_schema="specialist_model_effect_payload",
|
|
752
|
+
loc="$",
|
|
753
|
+
message=f"specialist runner returned {type(raw_result).__name__}, expected object",
|
|
754
|
+
operation_payload={"raw_result_type": type(raw_result).__name__},
|
|
755
|
+
)
|
|
756
|
+
try:
|
|
757
|
+
operation_payload = _json_object(raw_result)
|
|
758
|
+
result = SpecialistModelEffectPayload.from_operation_payload(operation_payload)
|
|
759
|
+
except PydanticValidationError as exc:
|
|
760
|
+
return _effect_payload_contract_block(
|
|
761
|
+
effect,
|
|
762
|
+
payload_schema="specialist_model_effect_payload",
|
|
763
|
+
exc=exc,
|
|
764
|
+
operation_payload=_safe_operation_payload(raw_result),
|
|
765
|
+
)
|
|
766
|
+
if result.status == "waiting_external":
|
|
767
|
+
reason = str(result.blocked_reason or result.status)
|
|
768
|
+
next_action = str(result.next_action or "Aguardar capacidade do modelo especialista e retomar pela rota oficial.")
|
|
769
|
+
return WorkflowEffectResult(
|
|
770
|
+
effect=effect,
|
|
771
|
+
status=WorkflowEffectStatus.WAITING_EXTERNAL,
|
|
772
|
+
outcome=SpecialistModelCapacityWaitOutcome(reason_code=reason),
|
|
773
|
+
public_summary="Modelo especialista aguardando capacidade externa.",
|
|
774
|
+
developer_summary="Specialist runner returned a resumable external wait.",
|
|
775
|
+
payload=result.to_payload(),
|
|
776
|
+
next_action=next_action,
|
|
777
|
+
resume_action=next_action,
|
|
778
|
+
error_context={
|
|
779
|
+
"blocked_reason": reason,
|
|
780
|
+
"root_cause": reason,
|
|
781
|
+
"required_inputs": list(result.required_inputs),
|
|
782
|
+
},
|
|
783
|
+
)
|
|
784
|
+
if result.status != "completed":
|
|
785
|
+
reason = str(result.blocked_reason or result.status or "blocked")
|
|
786
|
+
return WorkflowEffectResult(
|
|
787
|
+
effect=effect,
|
|
788
|
+
status=WorkflowEffectStatus.BLOCKED,
|
|
789
|
+
outcome=SpecialistModelBlockedOutcome(reason_code=reason),
|
|
790
|
+
public_summary="A chamada ao especialista nao produziu saida aplicavel.",
|
|
791
|
+
developer_summary="Specialist runner returned non-completed status.",
|
|
792
|
+
payload=result.to_payload(),
|
|
793
|
+
next_action=str(result.next_action or "Repetir a chamada ao especialista pela rota oficial."),
|
|
794
|
+
error_context={"status": str(result.status or "blocked"), "blocked_reason": reason},
|
|
795
|
+
)
|
|
796
|
+
receipt = result.receipt
|
|
797
|
+
if receipt is None:
|
|
798
|
+
return _effect_payload_contract_block_from_message(
|
|
799
|
+
effect,
|
|
800
|
+
payload_schema="specialist_model_effect_payload",
|
|
801
|
+
loc="receipt",
|
|
802
|
+
message="completed specialist model effect payload requires typed receipt",
|
|
803
|
+
operation_payload=result.to_payload(),
|
|
804
|
+
)
|
|
805
|
+
try:
|
|
806
|
+
raw_receipt_payload = _json_object(_json_field(result.operation_payload, "receipt"))
|
|
807
|
+
validate_specialist_task_run_receipt_attestation(raw_receipt_payload)
|
|
808
|
+
except PydanticValidationError as exc:
|
|
809
|
+
return _effect_payload_contract_block(
|
|
810
|
+
effect,
|
|
811
|
+
payload_schema="specialist_model_effect_payload",
|
|
812
|
+
exc=exc,
|
|
813
|
+
operation_payload=result.to_payload(),
|
|
814
|
+
)
|
|
815
|
+
except ValidationError as exc:
|
|
816
|
+
return _effect_payload_contract_block_from_message(
|
|
817
|
+
effect,
|
|
818
|
+
payload_schema="specialist_model_effect_payload",
|
|
819
|
+
loc="receipt.receipt_attestation",
|
|
820
|
+
message=str(exc),
|
|
821
|
+
operation_payload=result.to_payload(),
|
|
822
|
+
)
|
|
823
|
+
if receipt.specialist_output_attestation is None:
|
|
824
|
+
return _effect_payload_contract_block_from_message(
|
|
825
|
+
effect,
|
|
826
|
+
payload_schema="specialist_model_effect_payload",
|
|
827
|
+
loc="receipt.specialist_output_attestation",
|
|
828
|
+
message="completed specialist model effect payload requires receipt attestation",
|
|
829
|
+
operation_payload=result.to_payload(),
|
|
830
|
+
)
|
|
831
|
+
receipt_payload = receipt.to_payload()
|
|
832
|
+
attestation_payload = receipt.specialist_output_attestation.to_payload()
|
|
833
|
+
return WorkflowEffectResult(
|
|
834
|
+
effect=effect,
|
|
835
|
+
status=WorkflowEffectStatus.COMPLETED,
|
|
836
|
+
outcome=SpecialistModelCompletedOutcome(),
|
|
837
|
+
public_summary="Saida especializada recebida e validada.",
|
|
838
|
+
developer_summary="Specialist runner returned a typed specialist task receipt and attestation.",
|
|
839
|
+
payload={**result.payload, "specialist_task_run_receipt": receipt_payload},
|
|
840
|
+
receipt=receipt_payload,
|
|
841
|
+
attestation=attestation_payload,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _effect_payload_contract_block(
|
|
846
|
+
effect: WorkflowEffect,
|
|
847
|
+
*,
|
|
848
|
+
payload_schema: str,
|
|
849
|
+
exc: PydanticValidationError,
|
|
850
|
+
operation_payload: JsonObject,
|
|
851
|
+
outcome: ContractModel | None = None,
|
|
852
|
+
) -> WorkflowEffectResult:
|
|
853
|
+
first = exc.errors()[0] if exc.errors() else {}
|
|
854
|
+
loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
|
|
855
|
+
msg = str(first.get("msg") or str(exc))
|
|
856
|
+
return WorkflowEffectResult(
|
|
857
|
+
effect=effect,
|
|
858
|
+
status=WorkflowEffectStatus.BLOCKED,
|
|
859
|
+
outcome=outcome or LinkEffectBlockedOutcome(reason_code="effect_payload_contract_invalid"),
|
|
860
|
+
public_summary="O resultado interno do workflow falhou na validação antes de continuar.",
|
|
861
|
+
developer_summary=f"{payload_schema} invalid at {loc}: {msg}",
|
|
862
|
+
payload={
|
|
863
|
+
"schema": "medical-notes-workbench.workflow-effect-payload-contract-error.v1",
|
|
864
|
+
"payload_schema": payload_schema,
|
|
865
|
+
"operation_payload": operation_payload,
|
|
866
|
+
},
|
|
867
|
+
next_action="Corrigir o contrato tipado do efeito e repetir pela rota oficial.",
|
|
868
|
+
error_context={
|
|
869
|
+
"root_cause": "effect_payload_contract_invalid",
|
|
870
|
+
"payload_schema": payload_schema,
|
|
871
|
+
"contract_error": {"loc": loc, "message": msg},
|
|
872
|
+
},
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _effect_payload_contract_block_from_message(
|
|
877
|
+
effect: WorkflowEffect,
|
|
878
|
+
*,
|
|
879
|
+
payload_schema: str,
|
|
880
|
+
loc: str,
|
|
881
|
+
message: str,
|
|
882
|
+
operation_payload: JsonObject,
|
|
883
|
+
outcome: ContractModel | None = None,
|
|
884
|
+
) -> WorkflowEffectResult:
|
|
885
|
+
return WorkflowEffectResult(
|
|
886
|
+
effect=effect,
|
|
887
|
+
status=WorkflowEffectStatus.BLOCKED,
|
|
888
|
+
outcome=outcome or LinkEffectBlockedOutcome(reason_code="effect_payload_contract_invalid"),
|
|
889
|
+
public_summary="O resultado interno do workflow falhou na validação antes de continuar.",
|
|
890
|
+
developer_summary=f"{payload_schema} invalid at {loc}: {message}",
|
|
891
|
+
payload={
|
|
892
|
+
"schema": "medical-notes-workbench.workflow-effect-payload-contract-error.v1",
|
|
893
|
+
"payload_schema": payload_schema,
|
|
894
|
+
"operation_payload": operation_payload,
|
|
895
|
+
},
|
|
896
|
+
next_action="Corrigir o contrato tipado do efeito e repetir pela rota oficial.",
|
|
897
|
+
error_context={
|
|
898
|
+
"root_cause": "effect_payload_contract_invalid",
|
|
899
|
+
"payload_schema": payload_schema,
|
|
900
|
+
"contract_error": {"loc": loc, "message": message},
|
|
901
|
+
},
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _json_object(payload: object) -> JsonObject:
|
|
906
|
+
return JsonObjectAdapter.validate_python(payload)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _safe_operation_payload(payload: object) -> JsonObject:
|
|
910
|
+
try:
|
|
911
|
+
return _json_object(payload)
|
|
912
|
+
except PydanticValidationError:
|
|
913
|
+
return {"raw_result_type": type(payload).__name__}
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def _json_field(source: JsonObject, key: str, default: JsonValue = None) -> JsonValue:
|
|
917
|
+
return source[key] if key in source else default
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def _optional_text(value: JsonValue) -> str | None:
|
|
921
|
+
text = _text_or_empty(value).strip()
|
|
922
|
+
return text or None
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def _text_or_empty(value: object) -> str:
|
|
926
|
+
if value is None:
|
|
927
|
+
return ""
|
|
928
|
+
if isinstance(value, str):
|
|
929
|
+
return value
|
|
930
|
+
return f"{value}"
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def _float_field(source: JsonObject, key: str, default: float) -> float:
|
|
934
|
+
value = _json_field(source, key, default)
|
|
935
|
+
if isinstance(value, int | float) and not isinstance(value, bool):
|
|
936
|
+
return float(value)
|
|
937
|
+
if isinstance(value, str):
|
|
938
|
+
try:
|
|
939
|
+
return float(value)
|
|
940
|
+
except ValueError:
|
|
941
|
+
return default
|
|
942
|
+
return default
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _int_field(source: JsonObject, key: str, default: int) -> int:
|
|
946
|
+
value = _json_field(source, key, default)
|
|
947
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
948
|
+
return value
|
|
949
|
+
return default
|