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,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
from pydantic import AfterValidator, BaseModel, ConfigDict, TypeAdapter
|
|
6
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
7
|
+
from typing_extensions import TypeAliasType
|
|
8
|
+
|
|
9
|
+
from mednotes.kernel.errors import ValidationError
|
|
10
|
+
|
|
11
|
+
JsonValue = TypeAliasType(
|
|
12
|
+
"JsonValue",
|
|
13
|
+
str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"],
|
|
14
|
+
)
|
|
15
|
+
_StrictJsonObject = dict[str, JsonValue]
|
|
16
|
+
_StrictJsonArray = list[JsonValue]
|
|
17
|
+
|
|
18
|
+
JsonObjectAdapter = TypeAdapter(_StrictJsonObject)
|
|
19
|
+
JsonArrayAdapter = TypeAdapter(_StrictJsonArray)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _validated_json_object(value: Any) -> dict[str, Any]:
|
|
23
|
+
return JsonObjectAdapter.validate_python(value)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _validated_json_array(value: Any) -> list[Any]:
|
|
27
|
+
return JsonArrayAdapter.validate_python(value)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
JsonObject = Annotated[dict[str, Any], AfterValidator(_validated_json_object)]
|
|
31
|
+
JsonArray = Annotated[list[Any], AfterValidator(_validated_json_array)]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ContractModel(BaseModel):
|
|
35
|
+
model_config = ConfigDict(
|
|
36
|
+
extra="forbid",
|
|
37
|
+
populate_by_name=True,
|
|
38
|
+
validate_assignment=True,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def to_payload(self) -> dict[str, Any]:
|
|
42
|
+
payload = self.model_dump(mode="json", by_alias=True)
|
|
43
|
+
JsonObjectAdapter.validate_python(payload)
|
|
44
|
+
return payload
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def contract_error(exc: PydanticValidationError, *, prefix: str) -> ValidationError:
|
|
48
|
+
first = exc.errors()[0] if exc.errors() else {}
|
|
49
|
+
loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
|
|
50
|
+
msg = str(first.get("msg") or str(exc))
|
|
51
|
+
return ValidationError(f"{prefix}: {loc}: {msg}")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import field_validator, model_validator
|
|
4
|
+
from pydantic_core.core_schema import ValidationInfo
|
|
5
|
+
|
|
6
|
+
from mednotes.kernel.base import ContractModel
|
|
7
|
+
from mednotes.kernel.workflow import WorkflowDecisionKind
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _non_empty(value: str, field_name: str) -> str:
|
|
11
|
+
cleaned = str(value or "").strip()
|
|
12
|
+
if not cleaned:
|
|
13
|
+
raise ValueError(f"{field_name} must be non-empty")
|
|
14
|
+
return cleaned
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BlockerEntryModel(ContractModel):
|
|
18
|
+
code: str
|
|
19
|
+
owner_phase: str
|
|
20
|
+
default_decision: WorkflowDecisionKind
|
|
21
|
+
safe_to_continue_batch: bool
|
|
22
|
+
requires_human_packet: bool
|
|
23
|
+
public_label: str
|
|
24
|
+
public_explanation: str
|
|
25
|
+
developer_explanation: str
|
|
26
|
+
test_fixture: str
|
|
27
|
+
|
|
28
|
+
@field_validator("code", "owner_phase", "public_label", "public_explanation", "developer_explanation", "test_fixture")
|
|
29
|
+
@classmethod
|
|
30
|
+
def _required_text(cls, value: str, info: ValidationInfo) -> str:
|
|
31
|
+
return _non_empty(value, str(info.field_name))
|
|
32
|
+
|
|
33
|
+
@model_validator(mode="after")
|
|
34
|
+
def _validate_human_packet_policy(self) -> BlockerEntryModel:
|
|
35
|
+
if self.default_decision == WorkflowDecisionKind.ASK_HUMAN and not self.requires_human_packet:
|
|
36
|
+
raise ValueError("requires_human_packet must be true when default_decision is ask_human")
|
|
37
|
+
if self.default_decision != WorkflowDecisionKind.ASK_HUMAN and self.requires_human_packet:
|
|
38
|
+
raise ValueError("requires_human_packet is only valid for ask_human blockers")
|
|
39
|
+
return self
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from mednotes.kernel.effects import WorkflowEffect, WorkflowEffectKind, WorkflowEffectResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MissingWorkflowEffectAdapter(ValueError):
|
|
10
|
+
"""Raised when an FSM emitted an effect that has no official adapter."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class WorkflowEffectExecutionContext:
|
|
15
|
+
dry_run: bool = False
|
|
16
|
+
environment: dict[str, str] = field(default_factory=dict)
|
|
17
|
+
artifacts_dir: str = ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WorkflowEffectAdapter(Protocol):
|
|
21
|
+
def run(self, effect: WorkflowEffect, context: WorkflowEffectExecutionContext) -> WorkflowEffectResult:
|
|
22
|
+
"""Materialize one already-authorized workflow effect."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class WorkflowEffectExecutor:
|
|
27
|
+
adapters: dict[WorkflowEffectKind, WorkflowEffectAdapter]
|
|
28
|
+
context: WorkflowEffectExecutionContext = field(default_factory=WorkflowEffectExecutionContext)
|
|
29
|
+
|
|
30
|
+
def execute(
|
|
31
|
+
self,
|
|
32
|
+
effect: WorkflowEffect,
|
|
33
|
+
*,
|
|
34
|
+
context: WorkflowEffectExecutionContext | None = None,
|
|
35
|
+
) -> WorkflowEffectResult:
|
|
36
|
+
if not isinstance(effect, WorkflowEffect):
|
|
37
|
+
raise TypeError("WorkflowEffectExecutor.execute requires WorkflowEffect")
|
|
38
|
+
adapter = self._adapter_for(effect.kind)
|
|
39
|
+
return adapter.run(effect, context or self.context)
|
|
40
|
+
|
|
41
|
+
def _adapter_for(self, kind: WorkflowEffectKind) -> WorkflowEffectAdapter:
|
|
42
|
+
match kind:
|
|
43
|
+
case (
|
|
44
|
+
WorkflowEffectKind.RUN_SUBWORKFLOW
|
|
45
|
+
| WorkflowEffectKind.CALL_SPECIALIST_MODEL
|
|
46
|
+
| WorkflowEffectKind.ASK_HUMAN
|
|
47
|
+
| WorkflowEffectKind.WAIT_EXTERNAL
|
|
48
|
+
):
|
|
49
|
+
return self._required(kind)
|
|
50
|
+
|
|
51
|
+
def _required(self, kind: WorkflowEffectKind) -> WorkflowEffectAdapter:
|
|
52
|
+
adapter = self.adapters.get(kind)
|
|
53
|
+
if adapter is None:
|
|
54
|
+
raise MissingWorkflowEffectAdapter(f"no adapter registered for workflow effect kind: {kind.value}")
|
|
55
|
+
return adapter
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Executable workflow effect intents without workflow-result dependencies.
|
|
2
|
+
|
|
3
|
+
`WorkflowEffect` is the FSM-owned intent that adapters may execute later. It
|
|
4
|
+
must stay independent from `workflow.py` so `state_machine.py` can type
|
|
5
|
+
transition effects without creating a kernel import cycle.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
|
|
11
|
+
from pydantic import Field, ValidationInfo, field_validator, model_validator
|
|
12
|
+
|
|
13
|
+
from mednotes.kernel.base import ContractModel, JsonObject
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WorkflowEffectKind(StrEnum):
|
|
17
|
+
RUN_SUBWORKFLOW = "run_subworkflow"
|
|
18
|
+
CALL_SPECIALIST_MODEL = "call_specialist_model"
|
|
19
|
+
ASK_HUMAN = "ask_human"
|
|
20
|
+
WAIT_EXTERNAL = "wait_external"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WorkflowEffect(ContractModel):
|
|
24
|
+
"""Executable work intent emitted from one stable workflow state."""
|
|
25
|
+
|
|
26
|
+
schema_id: str = Field(default="workflow-effect.v1", alias="schema")
|
|
27
|
+
workflow: str = Field(min_length=1)
|
|
28
|
+
run_id: str = Field(min_length=1)
|
|
29
|
+
effect_id: str = Field(min_length=1)
|
|
30
|
+
origin_state: str = Field(min_length=1)
|
|
31
|
+
kind: WorkflowEffectKind
|
|
32
|
+
target: str = ""
|
|
33
|
+
payload: JsonObject = Field(default_factory=dict)
|
|
34
|
+
mutates_resources: bool = Field(default=False, strict=True)
|
|
35
|
+
no_resource_mutation: bool = Field(default=False, strict=True)
|
|
36
|
+
rollback_declared: bool = Field(default=False, strict=True)
|
|
37
|
+
requires_receipt: bool = Field(default=True, strict=True)
|
|
38
|
+
requires_attestation: bool = Field(default=False, strict=True)
|
|
39
|
+
model_policy: JsonObject = Field(default_factory=dict)
|
|
40
|
+
resume_action: str = ""
|
|
41
|
+
metadata: JsonObject = Field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
@field_validator("workflow", "run_id", "effect_id", "origin_state")
|
|
44
|
+
@classmethod
|
|
45
|
+
def _required_text(cls, value: str, info: ValidationInfo) -> str:
|
|
46
|
+
cleaned = value.strip()
|
|
47
|
+
if not cleaned:
|
|
48
|
+
raise ValueError(f"{info.field_name} must be non-empty")
|
|
49
|
+
return cleaned
|
|
50
|
+
|
|
51
|
+
@model_validator(mode="after")
|
|
52
|
+
def _validate_effect_contract(self) -> WorkflowEffect:
|
|
53
|
+
if (
|
|
54
|
+
self.kind
|
|
55
|
+
in {
|
|
56
|
+
WorkflowEffectKind.RUN_SUBWORKFLOW,
|
|
57
|
+
WorkflowEffectKind.CALL_SPECIALIST_MODEL,
|
|
58
|
+
WorkflowEffectKind.WAIT_EXTERNAL,
|
|
59
|
+
}
|
|
60
|
+
and not self.target.strip()
|
|
61
|
+
):
|
|
62
|
+
raise ValueError(f"{self.kind} requires target")
|
|
63
|
+
if self.mutates_resources and self.no_resource_mutation:
|
|
64
|
+
raise ValueError("mutates_resources cannot be combined with no_resource_mutation")
|
|
65
|
+
if self.mutates_resources and not self.rollback_declared:
|
|
66
|
+
raise ValueError("mutates_resources requires rollback_declared")
|
|
67
|
+
if self.kind == WorkflowEffectKind.CALL_SPECIALIST_MODEL and not self.model_policy:
|
|
68
|
+
raise ValueError("call_specialist_model requires model_policy")
|
|
69
|
+
return self
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Framework workflow-effect contracts (pure FSM kernel, domain-agnostic).
|
|
2
|
+
|
|
3
|
+
WorkflowEffect / WorkflowEffectResult are the generic effect intent + result of
|
|
4
|
+
the FSM kernel. Concrete product payloads live in domain modules; adapters are
|
|
5
|
+
the only layer allowed to materialize those effects in the outside world.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
|
|
11
|
+
from pydantic import Field, SerializeAsAny, model_validator
|
|
12
|
+
|
|
13
|
+
from mednotes.kernel.base import ContractModel, JsonObject
|
|
14
|
+
from mednotes.kernel.effect_intent import WorkflowEffect, WorkflowEffectKind
|
|
15
|
+
from mednotes.kernel.progress import WorkflowProgressEvent
|
|
16
|
+
from mednotes.kernel.workflow import HumanDecisionPacket
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"WorkflowEffect",
|
|
20
|
+
"WorkflowEffectKind",
|
|
21
|
+
"WorkflowEffectOutcome",
|
|
22
|
+
"WorkflowEffectResult",
|
|
23
|
+
"WorkflowEffectStatus",
|
|
24
|
+
"workflow_effect_blocked_outcome",
|
|
25
|
+
"workflow_effect_completed_outcome",
|
|
26
|
+
"workflow_effect_failed_outcome",
|
|
27
|
+
"workflow_effect_skipped_outcome",
|
|
28
|
+
"workflow_effect_waiting_agent_outcome",
|
|
29
|
+
"workflow_effect_waiting_external_outcome",
|
|
30
|
+
"workflow_effect_waiting_human_outcome",
|
|
31
|
+
"workflow_effect_warning_outcome",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class WorkflowEffectStatus(StrEnum):
|
|
36
|
+
COMPLETED = "completed"
|
|
37
|
+
COMPLETED_WITH_WARNINGS = "completed_with_warnings"
|
|
38
|
+
WAITING_AGENT = "waiting_agent"
|
|
39
|
+
WAITING_EXTERNAL = "waiting_external"
|
|
40
|
+
WAITING_HUMAN = "waiting_human"
|
|
41
|
+
BLOCKED = "blocked"
|
|
42
|
+
FAILED = "failed"
|
|
43
|
+
SKIPPED = "skipped"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class WorkflowEffectOutcome(ContractModel):
|
|
47
|
+
"""Generic outcome discriminator for framework-level roundtrips.
|
|
48
|
+
|
|
49
|
+
Domain adapters should return stricter outcome models at their own
|
|
50
|
+
boundary. The kernel only requires a stable `code` field so result envelopes
|
|
51
|
+
can be serialized without knowing product-specific outcome matrices.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
code: str = Field(min_length=1)
|
|
55
|
+
reason_code: str = ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def workflow_effect_completed_outcome() -> WorkflowEffectOutcome:
|
|
59
|
+
"""Factory for framework-only tests and adapters without domain policy."""
|
|
60
|
+
|
|
61
|
+
return WorkflowEffectOutcome(code="workflow_effect.completed")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def workflow_effect_warning_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
|
|
65
|
+
"""Factory for framework-level warning outcomes."""
|
|
66
|
+
|
|
67
|
+
return WorkflowEffectOutcome(code="workflow_effect.completed_with_warnings", reason_code=reason_code)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def workflow_effect_waiting_external_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
|
|
71
|
+
"""Factory for framework-level resumable external waits."""
|
|
72
|
+
|
|
73
|
+
return WorkflowEffectOutcome(code="workflow_effect.waiting_external", reason_code=reason_code)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def workflow_effect_waiting_agent_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
|
|
77
|
+
"""Factory for framework-level executable agent work."""
|
|
78
|
+
|
|
79
|
+
return WorkflowEffectOutcome(code="workflow_effect.waiting_agent", reason_code=reason_code)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def workflow_effect_waiting_human_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
|
|
83
|
+
"""Factory for framework-level human-decision waits."""
|
|
84
|
+
|
|
85
|
+
return WorkflowEffectOutcome(code="workflow_effect.waiting_human", reason_code=reason_code)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def workflow_effect_blocked_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
|
|
89
|
+
"""Factory for framework-level blocked outcomes."""
|
|
90
|
+
|
|
91
|
+
return WorkflowEffectOutcome(code="workflow_effect.blocked", reason_code=reason_code)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def workflow_effect_failed_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
|
|
95
|
+
"""Factory for framework-level failed outcomes."""
|
|
96
|
+
|
|
97
|
+
return WorkflowEffectOutcome(code="workflow_effect.failed", reason_code=reason_code)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def workflow_effect_skipped_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
|
|
101
|
+
"""Factory for intentionally skipped effects."""
|
|
102
|
+
|
|
103
|
+
return WorkflowEffectOutcome(code="workflow_effect.skipped", reason_code=reason_code)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class WorkflowEffectResult(ContractModel):
|
|
107
|
+
"""Typed result returned by the adapter that materialized one effect."""
|
|
108
|
+
|
|
109
|
+
schema_id: str = Field(default="workflow-effect-result.v1", alias="schema")
|
|
110
|
+
effect: WorkflowEffect
|
|
111
|
+
status: WorkflowEffectStatus
|
|
112
|
+
# Effect outcomes are polymorphic by design: adapters may return a stricter
|
|
113
|
+
# domain model while the kernel only requires the generic code contract.
|
|
114
|
+
outcome: SerializeAsAny[WorkflowEffectOutcome | ContractModel]
|
|
115
|
+
public_summary: str = Field(min_length=1)
|
|
116
|
+
developer_summary: str = Field(min_length=1)
|
|
117
|
+
payload: JsonObject = Field(default_factory=dict)
|
|
118
|
+
receipt: JsonObject | None = None
|
|
119
|
+
attestation: JsonObject | None = None
|
|
120
|
+
human_decision_packet: HumanDecisionPacket | None = None
|
|
121
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
122
|
+
progress_events: list[WorkflowProgressEvent] = Field(default_factory=list)
|
|
123
|
+
next_action: str = ""
|
|
124
|
+
resume_action: str = ""
|
|
125
|
+
|
|
126
|
+
@model_validator(mode="after")
|
|
127
|
+
def _validate_result_contract(self) -> WorkflowEffectResult:
|
|
128
|
+
if (
|
|
129
|
+
self.status
|
|
130
|
+
in {
|
|
131
|
+
WorkflowEffectStatus.BLOCKED,
|
|
132
|
+
WorkflowEffectStatus.FAILED,
|
|
133
|
+
WorkflowEffectStatus.COMPLETED_WITH_WARNINGS,
|
|
134
|
+
}
|
|
135
|
+
and not self.next_action.strip()
|
|
136
|
+
):
|
|
137
|
+
raise ValueError(f"{self.status} requires next_action")
|
|
138
|
+
if self.status == WorkflowEffectStatus.WAITING_EXTERNAL:
|
|
139
|
+
if not self.resume_action.strip():
|
|
140
|
+
raise ValueError("waiting_external effect result requires resume_action")
|
|
141
|
+
if not self.next_action.strip():
|
|
142
|
+
object.__setattr__(self, "next_action", self.resume_action)
|
|
143
|
+
if self.status == WorkflowEffectStatus.WAITING_HUMAN and self.human_decision_packet is None:
|
|
144
|
+
raise ValueError("waiting_human effect result requires human_decision_packet")
|
|
145
|
+
outcome_code = getattr(self.outcome, "code", "")
|
|
146
|
+
if not isinstance(outcome_code, str) or not outcome_code.strip():
|
|
147
|
+
raise ValueError("effect result outcome requires code")
|
|
148
|
+
if (
|
|
149
|
+
self.status in {WorkflowEffectStatus.COMPLETED, WorkflowEffectStatus.COMPLETED_WITH_WARNINGS}
|
|
150
|
+
and self.effect.requires_receipt
|
|
151
|
+
and self.receipt is None
|
|
152
|
+
):
|
|
153
|
+
raise ValueError("completed effect result requires receipt")
|
|
154
|
+
if (
|
|
155
|
+
self.status in {WorkflowEffectStatus.COMPLETED, WorkflowEffectStatus.COMPLETED_WITH_WARNINGS}
|
|
156
|
+
and self.effect.requires_attestation
|
|
157
|
+
and self.attestation is None
|
|
158
|
+
):
|
|
159
|
+
raise ValueError("effect result requires attestation")
|
|
160
|
+
return self
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Framework process-exit codes and the typed-error hierarchy.
|
|
2
|
+
|
|
3
|
+
Domain-agnostic: plain exit codes plus a base exception that carries one. Lives
|
|
4
|
+
in the framework so the FSM kernel can raise/type its errors without importing
|
|
5
|
+
domain facades. Layering rule: framework <- domain <- adapters
|
|
6
|
+
(tools/audit/import_layering.py).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
EXIT_OK = 0
|
|
11
|
+
EXIT_USAGE = 2
|
|
12
|
+
EXIT_VALIDATION = 3
|
|
13
|
+
EXIT_MISSING = 4
|
|
14
|
+
EXIT_IO = 5
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MedOpsError(Exception):
|
|
18
|
+
"""Base exception carrying a process exit code."""
|
|
19
|
+
|
|
20
|
+
exit_code = EXIT_IO
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ValidationError(MedOpsError):
|
|
24
|
+
exit_code = EXIT_VALIDATION
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MissingPathError(MedOpsError):
|
|
28
|
+
exit_code = EXIT_MISSING
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CollisionError(MedOpsError):
|
|
32
|
+
exit_code = EXIT_VALIDATION
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FileWriteError(MedOpsError):
|
|
36
|
+
"""Filesystem write failed after local retry/recovery attempts."""
|
|
37
|
+
|
|
38
|
+
exit_code = EXIT_IO
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from mednotes.kernel.base import ContractModel, JsonObject
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WorkflowEvent(ContractModel):
|
|
11
|
+
"""Base fact consumed by workflow statecharts; domains add typed fields."""
|
|
12
|
+
|
|
13
|
+
workflow: str = Field(min_length=1)
|
|
14
|
+
run_id: str = Field(min_length=1)
|
|
15
|
+
name: str = Field(min_length=1)
|
|
16
|
+
current_state: str = Field(min_length=1)
|
|
17
|
+
# Redacted replay/debug material only; FSM decisions must use typed fields.
|
|
18
|
+
audit_evidence: JsonObject = Field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WorkflowEventLike(Protocol):
|
|
22
|
+
"""Structural event surface required by the StateChart kernel wrapper."""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def workflow(self) -> str: ...
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def run_id(self) -> str: ...
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def name(self) -> str:
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def current_state(self) -> str: ...
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from mednotes.kernel.base import ContractModel, JsonObject
|
|
8
|
+
from mednotes.kernel.effects import WorkflowEffect
|
|
9
|
+
from mednotes.kernel.fsm_event import WorkflowEventLike
|
|
10
|
+
from mednotes.kernel.fsm_transition_result import WorkflowTransitionResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WorkflowModel(ContractModel):
|
|
14
|
+
"""Persisted workflow state used directly as python-statemachine carrier."""
|
|
15
|
+
|
|
16
|
+
STATECHART_STATE_FIELD: ClassVar[str] = "state"
|
|
17
|
+
|
|
18
|
+
workflow: str = Field(min_length=1)
|
|
19
|
+
run_id: str = Field(min_length=1)
|
|
20
|
+
# Public persisted state is the single mutable StateChart carrier.
|
|
21
|
+
state: str = Field(min_length=1)
|
|
22
|
+
# Events are typed before sending; the persisted log stores JSON evidence
|
|
23
|
+
# so WorkflowModel can rehydrate without importing every domain union.
|
|
24
|
+
event_log: list[JsonObject] = Field(default_factory=list)
|
|
25
|
+
transition_log: list[WorkflowTransitionResult] = Field(default_factory=list)
|
|
26
|
+
last_transition: WorkflowTransitionResult | None = None
|
|
27
|
+
pending_effects: list[WorkflowEffect] = Field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def start(cls, *, workflow: str, run_id: str, initial_state: str) -> WorkflowModel:
|
|
31
|
+
return cls(workflow=workflow, run_id=run_id, state=initial_state)
|
|
32
|
+
|
|
33
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
34
|
+
"""Allow python-statemachine's transient empty carrier during microsteps."""
|
|
35
|
+
|
|
36
|
+
if name == self.STATECHART_STATE_FIELD and value is None:
|
|
37
|
+
object.__setattr__(self, name, value)
|
|
38
|
+
return
|
|
39
|
+
super().__setattr__(name, value)
|
|
40
|
+
|
|
41
|
+
def record_event(self, event: WorkflowEventLike) -> None:
|
|
42
|
+
if not isinstance(event, ContractModel):
|
|
43
|
+
raise TypeError("workflow events must be Pydantic contract models")
|
|
44
|
+
if event.workflow != self.workflow or event.run_id != self.run_id:
|
|
45
|
+
raise ValueError("event belongs to a different workflow run")
|
|
46
|
+
self.event_log.append(event.to_payload())
|
|
47
|
+
|
|
48
|
+
def record_transition(self, transition: WorkflowTransitionResult) -> None:
|
|
49
|
+
if transition.workflow != self.workflow or transition.run_id != self.run_id:
|
|
50
|
+
raise ValueError("transition belongs to a different workflow run")
|
|
51
|
+
if self.state != transition.to_state:
|
|
52
|
+
raise ValueError("machine state does not match transition target")
|
|
53
|
+
object.__setattr__(self, "last_transition", transition)
|
|
54
|
+
object.__setattr__(self, "pending_effects", list(transition.effects))
|
|
55
|
+
self.transition_log.append(transition)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, StrictStr, ValidationInfo, field_validator
|
|
6
|
+
|
|
7
|
+
from mednotes.kernel.base import ContractModel, JsonObject
|
|
8
|
+
from mednotes.kernel.effects import WorkflowEffect, WorkflowEffectKind
|
|
9
|
+
from mednotes.kernel.workflow import HumanDecisionPacket, WorkflowDecision
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WorkflowTransitionResult(ContractModel):
|
|
13
|
+
"""Typed result returned by a real StateChart transition callback."""
|
|
14
|
+
|
|
15
|
+
workflow: StrictStr = Field(min_length=1)
|
|
16
|
+
run_id: StrictStr = Field(min_length=1)
|
|
17
|
+
from_state: StrictStr = Field(min_length=1)
|
|
18
|
+
to_state: StrictStr = Field(min_length=1)
|
|
19
|
+
trigger: StrictStr = Field(min_length=1)
|
|
20
|
+
reason_code: StrictStr = Field(min_length=1)
|
|
21
|
+
effects: list[WorkflowEffect] = Field(default_factory=list)
|
|
22
|
+
decision: WorkflowDecision | None = None
|
|
23
|
+
human_decision_packet: HumanDecisionPacket | None = None
|
|
24
|
+
resume_action: str = ""
|
|
25
|
+
# Redacted/debug-only evidence; categories and effects are validated from typed fields.
|
|
26
|
+
audit_evidence: JsonObject = Field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
@field_validator("workflow", "run_id", "from_state", "to_state", "trigger", "reason_code")
|
|
29
|
+
@classmethod
|
|
30
|
+
def _required_text(cls, value: str, info: ValidationInfo) -> str:
|
|
31
|
+
cleaned = value.strip()
|
|
32
|
+
if not cleaned:
|
|
33
|
+
raise ValueError(f"{info.field_name} must be non-empty")
|
|
34
|
+
return cleaned
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def validate_transition_result(
|
|
38
|
+
transition: WorkflowTransitionResult,
|
|
39
|
+
*,
|
|
40
|
+
category_for_state: Callable[[str], object],
|
|
41
|
+
) -> WorkflowTransitionResult:
|
|
42
|
+
"""Validate category-dependent contracts without importing domain state maps."""
|
|
43
|
+
|
|
44
|
+
for effect in transition.effects:
|
|
45
|
+
if effect.origin_state != transition.to_state:
|
|
46
|
+
raise ValueError("effect origin_state must match transition target")
|
|
47
|
+
|
|
48
|
+
category = _category_value(category_for_state(transition.to_state))
|
|
49
|
+
match category:
|
|
50
|
+
case "waiting_agent":
|
|
51
|
+
if not transition.effects:
|
|
52
|
+
raise ValueError("waiting_agent transition requires at least one workflow effect")
|
|
53
|
+
case "waiting_human":
|
|
54
|
+
if transition.human_decision_packet is None:
|
|
55
|
+
raise ValueError("waiting_human transition requires human_decision_packet")
|
|
56
|
+
if not _has_effect_kind(transition, WorkflowEffectKind.ASK_HUMAN):
|
|
57
|
+
raise ValueError("waiting_human transition requires ask_human effect")
|
|
58
|
+
case "waiting_external":
|
|
59
|
+
if not transition.resume_action.strip():
|
|
60
|
+
raise ValueError("waiting_external transition requires resume_action")
|
|
61
|
+
if not _has_effect_kind(transition, WorkflowEffectKind.WAIT_EXTERNAL):
|
|
62
|
+
raise ValueError("waiting_external transition requires wait_external effect")
|
|
63
|
+
case "blocked" | "failed":
|
|
64
|
+
if transition.decision is None:
|
|
65
|
+
raise ValueError(f"{category} transition requires decision")
|
|
66
|
+
return transition
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _category_value(category: object) -> str:
|
|
70
|
+
value = getattr(category, "value", category)
|
|
71
|
+
return str(value)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _has_effect_kind(transition: WorkflowTransitionResult, kind: WorkflowEffectKind) -> bool:
|
|
75
|
+
return any(effect.kind == kind for effect in transition.effects)
|