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,1119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal, cast
|
|
4
|
+
|
|
5
|
+
from pydantic import ConfigDict, Field, StrictStr, field_validator, model_validator
|
|
6
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
7
|
+
from pydantic.json_schema import SkipJsonSchema
|
|
8
|
+
|
|
9
|
+
from mednotes.domains.wiki.contracts.workflow_outcomes import WorkflowDecision
|
|
10
|
+
from mednotes.domains.wiki.flows.link.link_machine import (
|
|
11
|
+
LINK_BODY_WORKFLOW,
|
|
12
|
+
LINK_PUBLIC_WORKFLOWS,
|
|
13
|
+
LinkBoundaryEvent,
|
|
14
|
+
LinkMachine,
|
|
15
|
+
LinkMode,
|
|
16
|
+
category_for_link_state,
|
|
17
|
+
)
|
|
18
|
+
from mednotes.domains.wiki.flows.link.link_machine import (
|
|
19
|
+
LinkState as MachineLinkState,
|
|
20
|
+
)
|
|
21
|
+
from mednotes.kernel.agent_directive import (
|
|
22
|
+
AgentDirective,
|
|
23
|
+
agent_directive_from_progress_view_model,
|
|
24
|
+
assert_agent_directive_matches_progress,
|
|
25
|
+
)
|
|
26
|
+
from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
|
|
27
|
+
from mednotes.kernel.effects import WorkflowEffectKind
|
|
28
|
+
from mednotes.kernel.errors import EXIT_IO, EXIT_MISSING, EXIT_OK, EXIT_USAGE, EXIT_VALIDATION
|
|
29
|
+
from mednotes.kernel.fsm_event import WorkflowEventLike
|
|
30
|
+
from mednotes.kernel.fsm_model import WorkflowModel
|
|
31
|
+
from mednotes.kernel.fsm_transition_result import WorkflowTransitionResult
|
|
32
|
+
from mednotes.kernel.progress import (
|
|
33
|
+
WorkflowProgressCounts,
|
|
34
|
+
WorkflowProgressEventType,
|
|
35
|
+
WorkflowProgressState,
|
|
36
|
+
WorkflowProgressStatus,
|
|
37
|
+
WorkflowProgressViewModel,
|
|
38
|
+
build_progress_view_model,
|
|
39
|
+
progress_state_from_view_model,
|
|
40
|
+
)
|
|
41
|
+
from mednotes.kernel.public_report import (
|
|
42
|
+
WorkflowPrimaryObjectiveSummary,
|
|
43
|
+
WorkflowPublicReport,
|
|
44
|
+
WorkflowReports,
|
|
45
|
+
assert_public_report_matches_progress,
|
|
46
|
+
public_progress_followup_line,
|
|
47
|
+
)
|
|
48
|
+
from mednotes.kernel.state_machine import (
|
|
49
|
+
WorkflowStateCategory,
|
|
50
|
+
WorkflowStateMachineSnapshot,
|
|
51
|
+
WorkflowTransition,
|
|
52
|
+
send_workflow_event,
|
|
53
|
+
)
|
|
54
|
+
from mednotes.kernel.workflow import (
|
|
55
|
+
HumanDecisionPacket,
|
|
56
|
+
ReceiptStatus,
|
|
57
|
+
VersionControlSafety,
|
|
58
|
+
WorkflowReceiptPayload,
|
|
59
|
+
assert_diagnostic_context_evidence_only,
|
|
60
|
+
diagnostic_context_evidence_only,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
LINK_WORKFLOW = "/mednotes:link"
|
|
64
|
+
LINK_BODY_PUBLIC_WORKFLOW = LINK_BODY_WORKFLOW
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _MachineAuditEvidence(ContractModel):
|
|
68
|
+
"""Typed event evidence consumed by parent workflow FSMs."""
|
|
69
|
+
|
|
70
|
+
model_config = ConfigDict(extra="ignore")
|
|
71
|
+
|
|
72
|
+
adapter_schema: str = ""
|
|
73
|
+
adapter_phase: str = ""
|
|
74
|
+
adapter_status: str = ""
|
|
75
|
+
adapter_reason: str = ""
|
|
76
|
+
operation: JsonObject = Field(default_factory=dict)
|
|
77
|
+
mode: str = ""
|
|
78
|
+
include_related_notes: bool = False
|
|
79
|
+
counts: JsonObject = Field(default_factory=dict)
|
|
80
|
+
required_inputs: list[str] = Field(default_factory=list)
|
|
81
|
+
related_notes_recovery_state: JsonObject = Field(default_factory=dict)
|
|
82
|
+
stale_reason: str = ""
|
|
83
|
+
expected_git_status_hash: str = ""
|
|
84
|
+
actual_git_status_hash: str = ""
|
|
85
|
+
expected_git_head: str = ""
|
|
86
|
+
actual_git_head: str = ""
|
|
87
|
+
|
|
88
|
+
@field_validator("operation", mode="before")
|
|
89
|
+
@classmethod
|
|
90
|
+
def _coerce_operation(cls, value: object) -> JsonObject:
|
|
91
|
+
if not isinstance(value, dict):
|
|
92
|
+
return {}
|
|
93
|
+
return JsonObjectAdapter.validate_python(value)
|
|
94
|
+
|
|
95
|
+
@field_validator("counts", "related_notes_recovery_state", mode="before")
|
|
96
|
+
@classmethod
|
|
97
|
+
def _coerce_json_object(cls, value: object) -> JsonObject:
|
|
98
|
+
if not isinstance(value, dict):
|
|
99
|
+
return {}
|
|
100
|
+
return JsonObjectAdapter.validate_python(value)
|
|
101
|
+
|
|
102
|
+
def to_payload(self) -> JsonObject:
|
|
103
|
+
return JsonObjectAdapter.validate_python(self.model_dump(mode="json"))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class _MachineEventEvidence(ContractModel):
|
|
107
|
+
"""Typed lens over persisted machine event evidence."""
|
|
108
|
+
|
|
109
|
+
model_config = ConfigDict(extra="ignore")
|
|
110
|
+
|
|
111
|
+
audit_evidence: _MachineAuditEvidence = Field(default_factory=_MachineAuditEvidence)
|
|
112
|
+
LINK_SCHEMA = "medical-notes-workbench.link-fsm-result.v1"
|
|
113
|
+
LINK_RECEIPT_SCHEMA = "medical-notes-workbench.link-receipt.v1"
|
|
114
|
+
LINK_AGENT_DIRECTIVE_FIELD = "agent_directive"
|
|
115
|
+
|
|
116
|
+
LINK_ALLOWED_ROOT_KEYS = frozenset(
|
|
117
|
+
{
|
|
118
|
+
"schema",
|
|
119
|
+
"workflow",
|
|
120
|
+
"run_id",
|
|
121
|
+
"state_machine_snapshot",
|
|
122
|
+
"progress_view_model",
|
|
123
|
+
"decision",
|
|
124
|
+
"human_decision_packet",
|
|
125
|
+
"receipt",
|
|
126
|
+
"reports",
|
|
127
|
+
"agent_directive",
|
|
128
|
+
"artifacts",
|
|
129
|
+
"version_control_safety",
|
|
130
|
+
"diagnostic_context",
|
|
131
|
+
"error_context",
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
LINK_FORBIDDEN_ROOT_KEYS = frozenset(
|
|
135
|
+
{
|
|
136
|
+
"status",
|
|
137
|
+
"phase",
|
|
138
|
+
"blocked_reason",
|
|
139
|
+
"next_action",
|
|
140
|
+
"required_inputs",
|
|
141
|
+
"human_decision_required",
|
|
142
|
+
"returncode",
|
|
143
|
+
"workflow_exit_code",
|
|
144
|
+
"body_term_linker",
|
|
145
|
+
"related_notes_sync",
|
|
146
|
+
"reference_repair",
|
|
147
|
+
"graph_audit_after",
|
|
148
|
+
"vocabulary_bootstrap",
|
|
149
|
+
"vocabulary_curator_batch_plan",
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def category_for_state(state: str) -> WorkflowStateCategory:
|
|
154
|
+
"""Map link leaf states through the canonical LinkMachine enum only."""
|
|
155
|
+
|
|
156
|
+
return category_for_link_state(MachineLinkState(state))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class LinkFsmFacts(ContractModel):
|
|
160
|
+
workflow: Literal["/mednotes:link", "/mednotes:link-body"] = LINK_WORKFLOW
|
|
161
|
+
run_id: str = Field(min_length=1)
|
|
162
|
+
mode: LinkMode = LinkMode.FULL
|
|
163
|
+
initial_state: MachineLinkState
|
|
164
|
+
event: LinkBoundaryEvent
|
|
165
|
+
changed_files: list[str] = Field(default_factory=list)
|
|
166
|
+
mutated: bool = False
|
|
167
|
+
artifacts: JsonObject = Field(default_factory=dict)
|
|
168
|
+
version_control_safety: VersionControlSafety
|
|
169
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
170
|
+
|
|
171
|
+
@model_validator(mode="after")
|
|
172
|
+
def _event_must_match_fsm_entry(self) -> LinkFsmFacts:
|
|
173
|
+
if self.event.workflow != self.workflow:
|
|
174
|
+
raise ValueError("link event workflow must match LinkFsmFacts.workflow")
|
|
175
|
+
if self.event.run_id != self.run_id:
|
|
176
|
+
raise ValueError("link event run_id must match LinkFsmFacts.run_id")
|
|
177
|
+
if self.event.current_state != self.initial_state.value:
|
|
178
|
+
raise ValueError("link event current_state must match initial_state")
|
|
179
|
+
if self.workflow == LINK_BODY_PUBLIC_WORKFLOW and self.mode != LinkMode.BODY_ONLY:
|
|
180
|
+
raise ValueError("link-body facts require body_only mode")
|
|
181
|
+
if self.workflow == LINK_WORKFLOW and self.mode != LinkMode.FULL:
|
|
182
|
+
raise ValueError("full link facts require full mode")
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class _LinkPayloadProgressViewFields(ContractModel):
|
|
187
|
+
status: StrictStr
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class _LinkPayloadSnapshotFields(ContractModel):
|
|
191
|
+
current_category: StrictStr
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class _LinkPayloadReceiptFields(ContractModel):
|
|
195
|
+
status: StrictStr
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class _LinkPayloadFields(ContractModel):
|
|
199
|
+
workflow: StrictStr
|
|
200
|
+
progress_view_model: _LinkPayloadProgressViewFields
|
|
201
|
+
state_machine_snapshot: _LinkPayloadSnapshotFields
|
|
202
|
+
receipt: _LinkPayloadReceiptFields
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class _LinkCliExitCodeFields(ContractModel):
|
|
206
|
+
progress_view_model: _LinkPayloadProgressViewFields
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class _LinkErrorContextFields(ContractModel):
|
|
210
|
+
missing_inputs: list[StrictStr] = Field(default_factory=list)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class LinkFsmResult(ContractModel):
|
|
214
|
+
schema_id: Literal["medical-notes-workbench.link-fsm-result.v1"] = Field(default=LINK_SCHEMA, alias="schema")
|
|
215
|
+
workflow: Literal["/mednotes:link", "/mednotes:link-body"] = LINK_WORKFLOW
|
|
216
|
+
run_id: str = Field(min_length=1)
|
|
217
|
+
progress_state: SkipJsonSchema[WorkflowProgressState]
|
|
218
|
+
progress_view_model: WorkflowProgressViewModel
|
|
219
|
+
state_machine_snapshot: WorkflowStateMachineSnapshot
|
|
220
|
+
decision: WorkflowDecision | None = None
|
|
221
|
+
human_decision_packet: HumanDecisionPacket | None = None
|
|
222
|
+
receipt: WorkflowReceiptPayload
|
|
223
|
+
reports: WorkflowReports
|
|
224
|
+
agent_directive: JsonObject
|
|
225
|
+
artifacts: JsonObject = Field(default_factory=dict)
|
|
226
|
+
version_control_safety: VersionControlSafety
|
|
227
|
+
diagnostic_context: JsonObject = Field(default_factory=dict)
|
|
228
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
229
|
+
|
|
230
|
+
@model_validator(mode="before")
|
|
231
|
+
@classmethod
|
|
232
|
+
def _hydrate_progress_state_from_public_payload(cls, value: object) -> object:
|
|
233
|
+
"""Accept public payloads where progress_state is intentionally hidden."""
|
|
234
|
+
|
|
235
|
+
if not isinstance(value, dict) or "progress_state" in value or "progress_view_model" not in value:
|
|
236
|
+
return value
|
|
237
|
+
hydrated = dict(value)
|
|
238
|
+
progress_view = WorkflowProgressViewModel.model_validate(value["progress_view_model"])
|
|
239
|
+
hydrated["progress_state"] = progress_state_from_view_model(progress_view).to_payload()
|
|
240
|
+
return hydrated
|
|
241
|
+
|
|
242
|
+
@model_validator(mode="after")
|
|
243
|
+
def _progress_view_model_matches_state(self) -> LinkFsmResult:
|
|
244
|
+
expected = build_progress_view_model(self.progress_state).to_payload()
|
|
245
|
+
if self.progress_view_model.to_payload() != expected:
|
|
246
|
+
raise ValueError("progress_view_model must match progress_state")
|
|
247
|
+
return self
|
|
248
|
+
|
|
249
|
+
def to_payload(self) -> JsonObject:
|
|
250
|
+
payload: JsonObject = {
|
|
251
|
+
"schema": self.schema_id,
|
|
252
|
+
"workflow": self.workflow,
|
|
253
|
+
"run_id": self.run_id,
|
|
254
|
+
"state_machine_snapshot": self.state_machine_snapshot.to_payload(),
|
|
255
|
+
"progress_view_model": self.progress_view_model.to_payload(),
|
|
256
|
+
"decision": self.decision.to_payload() if self.decision is not None else None,
|
|
257
|
+
"human_decision_packet": self.human_decision_packet.to_payload()
|
|
258
|
+
if self.human_decision_packet is not None
|
|
259
|
+
else None,
|
|
260
|
+
"receipt": self.receipt.to_payload(),
|
|
261
|
+
"reports": self.reports.to_payload(),
|
|
262
|
+
"agent_directive": dict(self.agent_directive),
|
|
263
|
+
"artifacts": dict(self.artifacts),
|
|
264
|
+
"version_control_safety": self.version_control_safety.to_payload(),
|
|
265
|
+
"error_context": dict(self.error_context),
|
|
266
|
+
}
|
|
267
|
+
if self.diagnostic_context:
|
|
268
|
+
payload["diagnostic_context"] = dict(self.diagnostic_context)
|
|
269
|
+
payload = JsonObjectAdapter.validate_python(payload)
|
|
270
|
+
assert_link_fsm_payload(payload)
|
|
271
|
+
return payload
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def build_link_fsm_result(facts: LinkFsmFacts) -> LinkFsmResult:
|
|
275
|
+
"""Project one typed LinkMachine event into the public link FSM payload."""
|
|
276
|
+
|
|
277
|
+
return build_link_fsm_result_from_model(
|
|
278
|
+
_link_model_after_event(facts.initial_state, facts.event),
|
|
279
|
+
version_control_safety=facts.version_control_safety,
|
|
280
|
+
error_context=facts.error_context,
|
|
281
|
+
artifacts=facts.artifacts,
|
|
282
|
+
changed_files=facts.changed_files,
|
|
283
|
+
mutated=facts.mutated,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _link_model_after_event(initial_state: MachineLinkState, event: WorkflowEventLike) -> WorkflowModel:
|
|
288
|
+
model = WorkflowModel.start(
|
|
289
|
+
workflow=event.workflow,
|
|
290
|
+
run_id=event.run_id,
|
|
291
|
+
initial_state=initial_state.value,
|
|
292
|
+
)
|
|
293
|
+
send_workflow_event(
|
|
294
|
+
LinkMachine(model=model, state_field=WorkflowModel.STATECHART_STATE_FIELD),
|
|
295
|
+
event,
|
|
296
|
+
)
|
|
297
|
+
return model
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def build_link_fsm_result_from_model(
|
|
301
|
+
model: WorkflowModel,
|
|
302
|
+
*,
|
|
303
|
+
version_control_safety: VersionControlSafety | dict[str, object],
|
|
304
|
+
error_context: JsonObject | None = None,
|
|
305
|
+
artifacts: JsonObject | None = None,
|
|
306
|
+
changed_files: list[str] | None = None,
|
|
307
|
+
mutated: bool | None = None,
|
|
308
|
+
) -> LinkFsmResult:
|
|
309
|
+
"""Project a real LinkMachine model without reading adapter reports."""
|
|
310
|
+
|
|
311
|
+
_validate_link_machine_model(model)
|
|
312
|
+
state = MachineLinkState(model.state)
|
|
313
|
+
category = category_for_link_state(state)
|
|
314
|
+
progress_state = _progress_state_from_model(model, state, category)
|
|
315
|
+
progress_view_model = build_progress_view_model(progress_state)
|
|
316
|
+
snapshot = _snapshot_from_model(model, state, category)
|
|
317
|
+
safety = _version_control_safety(version_control_safety)
|
|
318
|
+
receipt = _receipt_from_model(
|
|
319
|
+
model,
|
|
320
|
+
progress_state=progress_state,
|
|
321
|
+
progress_view_model=progress_view_model,
|
|
322
|
+
snapshot=snapshot,
|
|
323
|
+
version_control_safety=safety,
|
|
324
|
+
changed_files=changed_files or [],
|
|
325
|
+
mutated=mutated,
|
|
326
|
+
)
|
|
327
|
+
reports_model = _reports_from_model(model, state, progress_state)
|
|
328
|
+
public_report = reports_model.public_report
|
|
329
|
+
diagnostic_context = _diagnostic_context_from_model(model, state, category)
|
|
330
|
+
agent_directive = agent_directive_from_progress_view_model(
|
|
331
|
+
progress_view_model,
|
|
332
|
+
schema="medical-notes-workbench.agent-directive.v1",
|
|
333
|
+
reason=_machine_reason_code(model, state),
|
|
334
|
+
effects=model.pending_effects,
|
|
335
|
+
blockers=_machine_blockers(category, model, state),
|
|
336
|
+
resume=progress_state.resume_action,
|
|
337
|
+
report_requires=["graph", "body_links", "related_notes"],
|
|
338
|
+
summary=public_report.summary_text(),
|
|
339
|
+
instructions=_machine_agent_instructions(category),
|
|
340
|
+
).to_payload()
|
|
341
|
+
machine_error_context = error_context or _error_context_from_model(model, state, category)
|
|
342
|
+
return LinkFsmResult(
|
|
343
|
+
workflow=cast(Literal["/mednotes:link", "/mednotes:link-body"], model.workflow),
|
|
344
|
+
run_id=model.run_id,
|
|
345
|
+
progress_state=progress_state,
|
|
346
|
+
progress_view_model=progress_view_model,
|
|
347
|
+
state_machine_snapshot=snapshot,
|
|
348
|
+
decision=model.last_transition.decision if model.last_transition is not None else None,
|
|
349
|
+
human_decision_packet=model.last_transition.human_decision_packet if model.last_transition is not None else None,
|
|
350
|
+
receipt=receipt,
|
|
351
|
+
reports=reports_model,
|
|
352
|
+
agent_directive=JsonObjectAdapter.validate_python(agent_directive),
|
|
353
|
+
artifacts=artifacts or {},
|
|
354
|
+
version_control_safety=safety,
|
|
355
|
+
diagnostic_context=diagnostic_context,
|
|
356
|
+
error_context=machine_error_context,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def link_fsm_payload_from_model(
|
|
361
|
+
model: WorkflowModel,
|
|
362
|
+
*,
|
|
363
|
+
version_control_safety: VersionControlSafety | dict[str, object],
|
|
364
|
+
) -> JsonObject:
|
|
365
|
+
"""JSON boundary for the machine-driven link FSM projection."""
|
|
366
|
+
|
|
367
|
+
return build_link_fsm_result_from_model(model, version_control_safety=version_control_safety).to_payload()
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _validate_link_machine_model(model: WorkflowModel) -> None:
|
|
371
|
+
if model.workflow not in LINK_PUBLIC_WORKFLOWS:
|
|
372
|
+
raise ValueError(f"link FSM projector requires workflow in {sorted(LINK_PUBLIC_WORKFLOWS)}")
|
|
373
|
+
MachineLinkState(model.state)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _progress_state_from_model(
|
|
377
|
+
model: WorkflowModel,
|
|
378
|
+
state: MachineLinkState,
|
|
379
|
+
category: WorkflowStateCategory,
|
|
380
|
+
) -> WorkflowProgressState:
|
|
381
|
+
status = _machine_progress_status(category)
|
|
382
|
+
current, total, counts = _machine_progress_numbers(model, state, status)
|
|
383
|
+
return WorkflowProgressState(
|
|
384
|
+
workflow=model.workflow,
|
|
385
|
+
run_id=model.run_id,
|
|
386
|
+
state=state.value,
|
|
387
|
+
phase=_machine_phase_for_state(state),
|
|
388
|
+
event_type=_machine_event_type(status),
|
|
389
|
+
message=_machine_message_for_state(state),
|
|
390
|
+
status=status,
|
|
391
|
+
current=current,
|
|
392
|
+
total=total,
|
|
393
|
+
counts=counts,
|
|
394
|
+
resume_action=_machine_resume_action(model, state),
|
|
395
|
+
resume_supported=status
|
|
396
|
+
in {
|
|
397
|
+
WorkflowProgressStatus.WAITING_AGENT,
|
|
398
|
+
WorkflowProgressStatus.WAITING_EXTERNAL,
|
|
399
|
+
WorkflowProgressStatus.WAITING_HUMAN,
|
|
400
|
+
WorkflowProgressStatus.BLOCKED,
|
|
401
|
+
},
|
|
402
|
+
can_continue_now=status
|
|
403
|
+
in {
|
|
404
|
+
WorkflowProgressStatus.RUNNING,
|
|
405
|
+
WorkflowProgressStatus.WAITING_AGENT,
|
|
406
|
+
},
|
|
407
|
+
decision=model.last_transition.decision.decision_summary()
|
|
408
|
+
if model.last_transition is not None and model.last_transition.decision is not None
|
|
409
|
+
else None,
|
|
410
|
+
technical_context={
|
|
411
|
+
"reason": _machine_reason_code(model, state),
|
|
412
|
+
"category": category.value,
|
|
413
|
+
"source": "LinkMachine",
|
|
414
|
+
},
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _machine_progress_numbers(
|
|
419
|
+
model: WorkflowModel,
|
|
420
|
+
state: MachineLinkState,
|
|
421
|
+
status: WorkflowProgressStatus,
|
|
422
|
+
) -> tuple[int, int, WorkflowProgressCounts]:
|
|
423
|
+
changed = _machine_event_int(model, "changed_file_count") or _machine_audit_count(model, "files_changed")
|
|
424
|
+
planned = max(_machine_event_int(model, "planned_link_count"), _machine_audit_count(model, "links_planned"))
|
|
425
|
+
rewritten = _machine_audit_count(model, "links_rewritten")
|
|
426
|
+
blocker_count = max(_machine_event_int(model, "blocker_count"), _machine_audit_count(model, "blocker_count"))
|
|
427
|
+
fresh = _machine_audit_count(model, "fresh_record_count")
|
|
428
|
+
remaining = _machine_audit_count(model, "remaining_count")
|
|
429
|
+
total_notes = _machine_audit_count(model, "total_note_count")
|
|
430
|
+
cache_hits = _machine_audit_count(model, "reused_count")
|
|
431
|
+
api_calls = _machine_audit_count(model, "embedded_count")
|
|
432
|
+
|
|
433
|
+
if state == MachineLinkState.WAITING_EXTERNAL_RELATED_NOTES_QUOTA:
|
|
434
|
+
total = total_notes or fresh + remaining
|
|
435
|
+
return (
|
|
436
|
+
fresh,
|
|
437
|
+
total,
|
|
438
|
+
WorkflowProgressCounts(
|
|
439
|
+
planned_items=total,
|
|
440
|
+
processed_items=fresh,
|
|
441
|
+
cache_hits=cache_hits,
|
|
442
|
+
api_calls=api_calls,
|
|
443
|
+
remaining_items=remaining,
|
|
444
|
+
blocked_items=remaining,
|
|
445
|
+
deferred_items=remaining,
|
|
446
|
+
mutated_files=changed,
|
|
447
|
+
written_files=changed,
|
|
448
|
+
),
|
|
449
|
+
)
|
|
450
|
+
if state == MachineLinkState.COMPLETED:
|
|
451
|
+
total = max(changed, rewritten, planned)
|
|
452
|
+
return (
|
|
453
|
+
total,
|
|
454
|
+
total,
|
|
455
|
+
WorkflowProgressCounts(
|
|
456
|
+
planned_items=total,
|
|
457
|
+
processed_items=total,
|
|
458
|
+
mutated_files=changed,
|
|
459
|
+
written_files=changed,
|
|
460
|
+
),
|
|
461
|
+
)
|
|
462
|
+
if state == MachineLinkState.COMPLETED_WITH_LINK_BLOCKERS:
|
|
463
|
+
blocked = max(blocker_count, 1)
|
|
464
|
+
return (
|
|
465
|
+
0,
|
|
466
|
+
blocked,
|
|
467
|
+
WorkflowProgressCounts(
|
|
468
|
+
planned_items=max(planned, blocked),
|
|
469
|
+
warnings=blocked,
|
|
470
|
+
remaining_items=blocked,
|
|
471
|
+
blocked_items=blocked,
|
|
472
|
+
mutated_files=changed,
|
|
473
|
+
written_files=changed,
|
|
474
|
+
),
|
|
475
|
+
)
|
|
476
|
+
if state == MachineLinkState.WAITING_HUMAN_CONFIRMATION:
|
|
477
|
+
total = max(planned, rewritten)
|
|
478
|
+
return (
|
|
479
|
+
0,
|
|
480
|
+
total,
|
|
481
|
+
WorkflowProgressCounts(
|
|
482
|
+
planned_items=total,
|
|
483
|
+
remaining_items=total,
|
|
484
|
+
blocked_items=total,
|
|
485
|
+
),
|
|
486
|
+
)
|
|
487
|
+
if status in {WorkflowProgressStatus.WAITING_AGENT, WorkflowProgressStatus.BLOCKED, WorkflowProgressStatus.FAILED}:
|
|
488
|
+
blocked = max(blocker_count, remaining, 1)
|
|
489
|
+
return (
|
|
490
|
+
0,
|
|
491
|
+
blocked,
|
|
492
|
+
WorkflowProgressCounts(
|
|
493
|
+
planned_items=max(planned, blocked),
|
|
494
|
+
remaining_items=blocked,
|
|
495
|
+
blocked_items=blocked,
|
|
496
|
+
mutated_files=changed,
|
|
497
|
+
written_files=changed,
|
|
498
|
+
),
|
|
499
|
+
)
|
|
500
|
+
total = max(planned, rewritten)
|
|
501
|
+
return 0, total, WorkflowProgressCounts(planned_items=total)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _machine_event_int(model: WorkflowModel, field_name: str) -> int:
|
|
505
|
+
if not model.event_log:
|
|
506
|
+
return 0
|
|
507
|
+
event = model.event_log[-1]
|
|
508
|
+
value = event[field_name] if field_name in event else 0
|
|
509
|
+
if isinstance(value, bool):
|
|
510
|
+
return 0
|
|
511
|
+
if isinstance(value, int) and value > 0:
|
|
512
|
+
return value
|
|
513
|
+
return 0
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _machine_audit_count(model: WorkflowModel, field_name: str) -> int:
|
|
517
|
+
evidence = _machine_audit_evidence(model)
|
|
518
|
+
try:
|
|
519
|
+
raw_counts = evidence["counts"]
|
|
520
|
+
except KeyError:
|
|
521
|
+
return 0
|
|
522
|
+
if not isinstance(raw_counts, dict):
|
|
523
|
+
return 0
|
|
524
|
+
try:
|
|
525
|
+
value = raw_counts[field_name]
|
|
526
|
+
except KeyError:
|
|
527
|
+
return 0
|
|
528
|
+
if isinstance(value, bool):
|
|
529
|
+
return 0
|
|
530
|
+
if isinstance(value, int) and value > 0:
|
|
531
|
+
return value
|
|
532
|
+
return 0
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _snapshot_from_model(
|
|
536
|
+
model: WorkflowModel,
|
|
537
|
+
state: MachineLinkState,
|
|
538
|
+
category: WorkflowStateCategory,
|
|
539
|
+
) -> WorkflowStateMachineSnapshot:
|
|
540
|
+
return WorkflowStateMachineSnapshot(
|
|
541
|
+
workflow=model.workflow,
|
|
542
|
+
run_id=model.run_id,
|
|
543
|
+
current_state=state.value,
|
|
544
|
+
current_category=category,
|
|
545
|
+
transitions=[_machine_snapshot_transition(transition) for transition in model.transition_log],
|
|
546
|
+
metadata={
|
|
547
|
+
"reason": _machine_reason_code(model, state),
|
|
548
|
+
"source": "LinkMachine",
|
|
549
|
+
"link_mode": _link_mode_for_model(model).value,
|
|
550
|
+
},
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _link_mode_for_model(model: WorkflowModel) -> LinkMode:
|
|
555
|
+
"""Recover the invariant execution mode from the observed event or workflow."""
|
|
556
|
+
|
|
557
|
+
if model.workflow == LINK_BODY_PUBLIC_WORKFLOW:
|
|
558
|
+
return LinkMode.BODY_ONLY
|
|
559
|
+
for raw_event in reversed(model.event_log):
|
|
560
|
+
try:
|
|
561
|
+
observation = raw_event["observation"]
|
|
562
|
+
except KeyError:
|
|
563
|
+
continue
|
|
564
|
+
if not isinstance(observation, dict):
|
|
565
|
+
continue
|
|
566
|
+
try:
|
|
567
|
+
raw_mode = observation["mode"]
|
|
568
|
+
except KeyError:
|
|
569
|
+
continue
|
|
570
|
+
if isinstance(raw_mode, str) and raw_mode:
|
|
571
|
+
return LinkMode(raw_mode)
|
|
572
|
+
return LinkMode.FULL
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _machine_snapshot_transition(transition: WorkflowTransitionResult) -> WorkflowTransition:
|
|
576
|
+
return WorkflowTransition(
|
|
577
|
+
workflow=transition.workflow,
|
|
578
|
+
from_state=transition.from_state,
|
|
579
|
+
to_state=transition.to_state,
|
|
580
|
+
to_category=category_for_link_state(MachineLinkState(transition.to_state)),
|
|
581
|
+
trigger=transition.trigger,
|
|
582
|
+
effects=list(transition.effects),
|
|
583
|
+
decision=transition.decision,
|
|
584
|
+
resume_action=transition.resume_action,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _receipt_from_model(
|
|
589
|
+
model: WorkflowModel,
|
|
590
|
+
*,
|
|
591
|
+
progress_state: WorkflowProgressState,
|
|
592
|
+
progress_view_model: WorkflowProgressViewModel,
|
|
593
|
+
snapshot: WorkflowStateMachineSnapshot,
|
|
594
|
+
version_control_safety: VersionControlSafety,
|
|
595
|
+
changed_files: list[str],
|
|
596
|
+
mutated: bool | None,
|
|
597
|
+
) -> WorkflowReceiptPayload:
|
|
598
|
+
return WorkflowReceiptPayload(
|
|
599
|
+
schema=LINK_RECEIPT_SCHEMA,
|
|
600
|
+
workflow=model.workflow,
|
|
601
|
+
run_id=model.run_id,
|
|
602
|
+
status=_machine_receipt_status(progress_state.status),
|
|
603
|
+
mutated=mutated if mutated is not None else version_control_safety.changed_file_count > 0,
|
|
604
|
+
next_action="" if progress_state.status == WorkflowProgressStatus.COMPLETED else progress_state.resume_action,
|
|
605
|
+
human_decision_required=progress_state.status == WorkflowProgressStatus.WAITING_HUMAN,
|
|
606
|
+
human_decision_packet=model.last_transition.human_decision_packet if model.last_transition is not None else None,
|
|
607
|
+
changed_files=changed_files,
|
|
608
|
+
version_control_safety=version_control_safety,
|
|
609
|
+
progress_state=progress_state,
|
|
610
|
+
progress_view_model=progress_view_model,
|
|
611
|
+
state_machine_snapshot=snapshot,
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _reports_from_model(
|
|
616
|
+
model: WorkflowModel,
|
|
617
|
+
state: MachineLinkState,
|
|
618
|
+
progress_state: WorkflowProgressState,
|
|
619
|
+
) -> WorkflowReports:
|
|
620
|
+
summary = _machine_message_for_state(state)
|
|
621
|
+
public_lines = [summary]
|
|
622
|
+
followup_line = public_progress_followup_line(progress_state)
|
|
623
|
+
if followup_line:
|
|
624
|
+
public_lines.append(followup_line)
|
|
625
|
+
public_report = WorkflowPublicReport(
|
|
626
|
+
workflow=progress_state.workflow,
|
|
627
|
+
run_id=model.run_id,
|
|
628
|
+
headline=summary,
|
|
629
|
+
lines=public_lines,
|
|
630
|
+
)
|
|
631
|
+
details: JsonObject = {
|
|
632
|
+
"primary_objective_summary": _primary_objective_summary(
|
|
633
|
+
run_id=model.run_id,
|
|
634
|
+
workflow=progress_state.workflow,
|
|
635
|
+
state=state,
|
|
636
|
+
progress_state=progress_state,
|
|
637
|
+
).to_payload()
|
|
638
|
+
}
|
|
639
|
+
operation_details = _operation_details_from_model(model)
|
|
640
|
+
if operation_details:
|
|
641
|
+
details.update(operation_details)
|
|
642
|
+
return WorkflowReports(
|
|
643
|
+
summary=summary,
|
|
644
|
+
public_report=public_report,
|
|
645
|
+
details=details,
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _operation_details_from_model(model: WorkflowModel) -> JsonObject:
|
|
650
|
+
"""Expose typed child-operation evidence for parent FSMs without root state."""
|
|
651
|
+
|
|
652
|
+
for event in reversed(model.event_log):
|
|
653
|
+
event_evidence = _MachineEventEvidence.model_validate(event)
|
|
654
|
+
if event_evidence.audit_evidence.operation:
|
|
655
|
+
return event_evidence.audit_evidence.operation
|
|
656
|
+
return {}
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _primary_objective_summary(
|
|
660
|
+
*,
|
|
661
|
+
run_id: str,
|
|
662
|
+
workflow: str,
|
|
663
|
+
state: MachineLinkState,
|
|
664
|
+
progress_state: WorkflowProgressState,
|
|
665
|
+
) -> WorkflowPrimaryObjectiveSummary:
|
|
666
|
+
"""State-owned answer to whether `/mednotes:link` completed its job."""
|
|
667
|
+
|
|
668
|
+
completed = state == MachineLinkState.COMPLETED
|
|
669
|
+
changed_count = max(progress_state.counts.mutated_files, progress_state.counts.written_files)
|
|
670
|
+
return WorkflowPrimaryObjectiveSummary(
|
|
671
|
+
workflow=workflow,
|
|
672
|
+
run_id=run_id,
|
|
673
|
+
objective=_link_objective_for_workflow(workflow),
|
|
674
|
+
completed=completed,
|
|
675
|
+
status=state.value,
|
|
676
|
+
mutation_state="changed" if changed_count > 0 else "unchanged",
|
|
677
|
+
mutation_summary=_link_mutation_summary(changed_count),
|
|
678
|
+
remaining_work_summary=_link_remaining_work_summary(state, completed),
|
|
679
|
+
next_step_summary=_link_next_step_summary(progress_state, completed),
|
|
680
|
+
blocked_reason="" if completed else state.value,
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _link_mutation_summary(changed_count: int) -> str:
|
|
685
|
+
if changed_count > 0:
|
|
686
|
+
return f"{changed_count} arquivo(s) de links foram alterados."
|
|
687
|
+
return "Nenhum arquivo de links foi alterado nesta etapa."
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _link_objective_for_workflow(workflow: str) -> str:
|
|
691
|
+
if workflow == LINK_BODY_PUBLIC_WORKFLOW:
|
|
692
|
+
return "Atualizar somente WikiLinks no corpo das notas, sem Notas Relacionadas."
|
|
693
|
+
return "Atualizar grafo, links de corpo e Notas Relacionadas quando aplicável."
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _link_remaining_work_summary(state: MachineLinkState, completed: bool) -> str:
|
|
697
|
+
if completed:
|
|
698
|
+
return "Grafo, links de corpo e Notas Relacionadas ficaram concluídos."
|
|
699
|
+
if state == MachineLinkState.COMPLETED_WITH_LINK_BLOCKERS:
|
|
700
|
+
return "O link terminou com pendências explícitas de grafo ou Notas Relacionadas."
|
|
701
|
+
return _machine_message_for_state(state)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def _link_next_step_summary(progress_state: WorkflowProgressState, completed: bool) -> str:
|
|
705
|
+
if completed:
|
|
706
|
+
return "Nenhuma ação pendente para o pacote de links."
|
|
707
|
+
return progress_state.resume_action or "Retomar /mednotes:link pela rota oficial."
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _diagnostic_context_from_model(
|
|
711
|
+
model: WorkflowModel,
|
|
712
|
+
state: MachineLinkState,
|
|
713
|
+
category: WorkflowStateCategory,
|
|
714
|
+
) -> JsonObject:
|
|
715
|
+
if category in {WorkflowStateCategory.COMPLETED, WorkflowStateCategory.COMPLETED_WITH_WARNINGS}:
|
|
716
|
+
return {}
|
|
717
|
+
context: JsonObject = {
|
|
718
|
+
"schema": "medical-notes-workbench.link-fsm-diagnostic-context.v2",
|
|
719
|
+
"state": state.value,
|
|
720
|
+
"category": category.value,
|
|
721
|
+
"reason": _machine_reason_code(model, state),
|
|
722
|
+
"source": "LinkMachine",
|
|
723
|
+
}
|
|
724
|
+
evidence = _machine_audit_evidence(model)
|
|
725
|
+
for key, value in evidence.items():
|
|
726
|
+
if key not in context:
|
|
727
|
+
context[key] = value
|
|
728
|
+
return diagnostic_context_evidence_only(context)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _machine_audit_evidence(model: WorkflowModel) -> JsonObject:
|
|
732
|
+
if not model.event_log:
|
|
733
|
+
return {}
|
|
734
|
+
event = _MachineEventEvidence.model_validate(model.event_log[-1])
|
|
735
|
+
return event.audit_evidence.to_payload()
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _machine_progress_status(category: WorkflowStateCategory) -> WorkflowProgressStatus:
|
|
739
|
+
match category:
|
|
740
|
+
case WorkflowStateCategory.PREPARING | WorkflowStateCategory.RUNNING:
|
|
741
|
+
return WorkflowProgressStatus.RUNNING
|
|
742
|
+
case WorkflowStateCategory.WAITING_AGENT:
|
|
743
|
+
return WorkflowProgressStatus.WAITING_AGENT
|
|
744
|
+
case WorkflowStateCategory.WAITING_EXTERNAL:
|
|
745
|
+
return WorkflowProgressStatus.WAITING_EXTERNAL
|
|
746
|
+
case WorkflowStateCategory.WAITING_HUMAN:
|
|
747
|
+
return WorkflowProgressStatus.WAITING_HUMAN
|
|
748
|
+
case WorkflowStateCategory.BLOCKED:
|
|
749
|
+
return WorkflowProgressStatus.BLOCKED
|
|
750
|
+
case WorkflowStateCategory.FAILED:
|
|
751
|
+
return WorkflowProgressStatus.FAILED
|
|
752
|
+
case WorkflowStateCategory.COMPLETED:
|
|
753
|
+
return WorkflowProgressStatus.COMPLETED
|
|
754
|
+
case WorkflowStateCategory.COMPLETED_WITH_WARNINGS:
|
|
755
|
+
return WorkflowProgressStatus.COMPLETED_WITH_WARNINGS
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _machine_receipt_status(status: WorkflowProgressStatus) -> ReceiptStatus:
|
|
759
|
+
match status:
|
|
760
|
+
case WorkflowProgressStatus.RUNNING:
|
|
761
|
+
return "running"
|
|
762
|
+
case WorkflowProgressStatus.COMPLETED:
|
|
763
|
+
return "completed"
|
|
764
|
+
case WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
|
|
765
|
+
return "completed_with_warnings"
|
|
766
|
+
case WorkflowProgressStatus.WAITING_AGENT:
|
|
767
|
+
return "waiting_agent"
|
|
768
|
+
case WorkflowProgressStatus.WAITING_EXTERNAL:
|
|
769
|
+
return "waiting_external"
|
|
770
|
+
case WorkflowProgressStatus.WAITING_HUMAN:
|
|
771
|
+
return "waiting_human"
|
|
772
|
+
case WorkflowProgressStatus.FAILED:
|
|
773
|
+
return "failed"
|
|
774
|
+
case WorkflowProgressStatus.BLOCKED:
|
|
775
|
+
return "blocked"
|
|
776
|
+
case _:
|
|
777
|
+
return "blocked"
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _machine_event_type(status: WorkflowProgressStatus) -> WorkflowProgressEventType:
|
|
781
|
+
match status:
|
|
782
|
+
case WorkflowProgressStatus.COMPLETED | WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
|
|
783
|
+
return WorkflowProgressEventType.WORKFLOW_COMPLETED
|
|
784
|
+
case WorkflowProgressStatus.FAILED:
|
|
785
|
+
return WorkflowProgressEventType.WORKFLOW_FAILED
|
|
786
|
+
case WorkflowProgressStatus.WAITING_EXTERNAL:
|
|
787
|
+
return WorkflowProgressEventType.EXTERNAL_WAIT_STARTED
|
|
788
|
+
case WorkflowProgressStatus.WAITING_HUMAN:
|
|
789
|
+
return WorkflowProgressEventType.DECISION_EMITTED
|
|
790
|
+
case _:
|
|
791
|
+
return WorkflowProgressEventType.STATE_ENTERED
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _machine_phase_for_state(state: MachineLinkState) -> str:
|
|
795
|
+
match state:
|
|
796
|
+
case MachineLinkState.CHECKING_TRIGGER_CONTEXT:
|
|
797
|
+
return "trigger_context"
|
|
798
|
+
case MachineLinkState.DIAGNOSING_GRAPH | MachineLinkState.STALE_DIAGNOSIS:
|
|
799
|
+
return "diagnosis"
|
|
800
|
+
case MachineLinkState.VOCABULARY_BOOTSTRAP_REQUIRED:
|
|
801
|
+
return "vocabulary_bootstrap"
|
|
802
|
+
case MachineLinkState.PLANNING_BODY_LINKS | MachineLinkState.APPLYING_BODY_LINKS:
|
|
803
|
+
return "body_links"
|
|
804
|
+
case MachineLinkState.PLANNING_RELATED_NOTES | MachineLinkState.APPLYING_RELATED_NOTES:
|
|
805
|
+
return "related_notes"
|
|
806
|
+
case (
|
|
807
|
+
MachineLinkState.PLANNING_VOCABULARY_SEMANTIC_REPAIR
|
|
808
|
+
| MachineLinkState.APPLYING_VOCABULARY_SEMANTIC_REPAIR
|
|
809
|
+
):
|
|
810
|
+
return "vocabulary_semantic_repair"
|
|
811
|
+
case MachineLinkState.WAITING_AGENT_DISAMBIGUATION:
|
|
812
|
+
return "agent_disambiguation"
|
|
813
|
+
case MachineLinkState.WAITING_AGENT_RELATED_NOTES_EXPORT_RECOVERY:
|
|
814
|
+
return "related_notes_export_recovery"
|
|
815
|
+
case MachineLinkState.WAITING_AGENT_VOCABULARY_CURATOR:
|
|
816
|
+
return "vocabulary_curator"
|
|
817
|
+
case MachineLinkState.WAITING_EXTERNAL_RELATED_NOTES_QUOTA:
|
|
818
|
+
return "related_notes_recovery"
|
|
819
|
+
case MachineLinkState.WAITING_HUMAN_CONFIRMATION:
|
|
820
|
+
return "human_confirmation"
|
|
821
|
+
case MachineLinkState.COMPLETED | MachineLinkState.COMPLETED_WITH_LINK_BLOCKERS:
|
|
822
|
+
return "completed"
|
|
823
|
+
case MachineLinkState.APPLY_CANCELLED:
|
|
824
|
+
return "apply_cancelled"
|
|
825
|
+
case MachineLinkState.GRAPH_DIAGNOSIS_BLOCKED:
|
|
826
|
+
return "graph_diagnosis_blocked"
|
|
827
|
+
case MachineLinkState.BODY_LINKS_BLOCKED:
|
|
828
|
+
return "body_links_blocked"
|
|
829
|
+
case MachineLinkState.RELATED_NOTES_BLOCKED:
|
|
830
|
+
return "related_notes_blocked"
|
|
831
|
+
case MachineLinkState.VOCABULARY_SEMANTIC_REPAIR_BLOCKED:
|
|
832
|
+
return "vocabulary_semantic_repair_blocked"
|
|
833
|
+
case MachineLinkState.FAILED:
|
|
834
|
+
return "failed"
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def _machine_message_for_state(state: MachineLinkState) -> str:
|
|
838
|
+
match state:
|
|
839
|
+
case MachineLinkState.STALE_DIAGNOSIS:
|
|
840
|
+
return "O diagnostico de links ficou desatualizado."
|
|
841
|
+
case MachineLinkState.VOCABULARY_BOOTSTRAP_REQUIRED:
|
|
842
|
+
return "O vocabulario precisa ser preparado antes dos links."
|
|
843
|
+
case MachineLinkState.WAITING_EXTERNAL_RELATED_NOTES_QUOTA:
|
|
844
|
+
return "Notas Relacionadas aguardam cota externa para continuar."
|
|
845
|
+
case MachineLinkState.WAITING_AGENT_RELATED_NOTES_EXPORT_RECOVERY:
|
|
846
|
+
return "O export do Related Notes precisa ser recuperado pela rota oficial."
|
|
847
|
+
case MachineLinkState.WAITING_HUMAN_CONFIRMATION:
|
|
848
|
+
return "Preciso de confirmacao antes de aplicar links."
|
|
849
|
+
case MachineLinkState.GRAPH_DIAGNOSIS_BLOCKED:
|
|
850
|
+
return "Diagnóstico do grafo de links bloqueado."
|
|
851
|
+
case MachineLinkState.BODY_LINKS_BLOCKED:
|
|
852
|
+
return "Planejamento ou aplicação dos links de corpo bloqueada."
|
|
853
|
+
case MachineLinkState.RELATED_NOTES_BLOCKED:
|
|
854
|
+
return "Atualização de Notas Relacionadas bloqueada."
|
|
855
|
+
case MachineLinkState.VOCABULARY_SEMANTIC_REPAIR_BLOCKED:
|
|
856
|
+
return "Reparo semântico do vocabulário bloqueado."
|
|
857
|
+
case MachineLinkState.COMPLETED:
|
|
858
|
+
return "Links atualizados e conferidos."
|
|
859
|
+
case MachineLinkState.COMPLETED_WITH_LINK_BLOCKERS:
|
|
860
|
+
return "Links concluidos com bloqueios pendentes."
|
|
861
|
+
case MachineLinkState.FAILED:
|
|
862
|
+
return "O workflow de links falhou antes de concluir."
|
|
863
|
+
case _:
|
|
864
|
+
return "Workflow de links em andamento."
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _machine_resume_action(model: WorkflowModel, state: MachineLinkState) -> str:
|
|
868
|
+
if state in {MachineLinkState.COMPLETED, MachineLinkState.COMPLETED_WITH_LINK_BLOCKERS}:
|
|
869
|
+
return ""
|
|
870
|
+
if model.last_transition is not None and model.last_transition.resume_action:
|
|
871
|
+
return model.last_transition.resume_action
|
|
872
|
+
match state:
|
|
873
|
+
case MachineLinkState.STALE_DIAGNOSIS:
|
|
874
|
+
return "link:diagnose"
|
|
875
|
+
case MachineLinkState.VOCABULARY_BOOTSTRAP_REQUIRED:
|
|
876
|
+
return "link:bootstrap-vocabulary"
|
|
877
|
+
case MachineLinkState.WAITING_AGENT_DISAMBIGUATION:
|
|
878
|
+
return "link:run-agent-disambiguation"
|
|
879
|
+
case MachineLinkState.WAITING_AGENT_RELATED_NOTES_EXPORT_RECOVERY:
|
|
880
|
+
return "link:recover-related-notes-export"
|
|
881
|
+
case MachineLinkState.WAITING_AGENT_VOCABULARY_CURATOR:
|
|
882
|
+
return "link:run-vocabulary-curator"
|
|
883
|
+
case MachineLinkState.WAITING_EXTERNAL_RELATED_NOTES_QUOTA:
|
|
884
|
+
return "link:retry-related-notes-export"
|
|
885
|
+
case MachineLinkState.WAITING_HUMAN_CONFIRMATION:
|
|
886
|
+
return "link:confirm-apply"
|
|
887
|
+
case MachineLinkState.APPLYING_BODY_LINKS:
|
|
888
|
+
return "link:apply-body-links"
|
|
889
|
+
case MachineLinkState.APPLYING_RELATED_NOTES:
|
|
890
|
+
return "link:apply-related-notes"
|
|
891
|
+
case MachineLinkState.APPLYING_VOCABULARY_SEMANTIC_REPAIR:
|
|
892
|
+
return "link:apply-vocabulary-semantic-repair"
|
|
893
|
+
case MachineLinkState.GRAPH_DIAGNOSIS_BLOCKED:
|
|
894
|
+
return "link:diagnose"
|
|
895
|
+
case MachineLinkState.BODY_LINKS_BLOCKED:
|
|
896
|
+
return "link:repair-body-links"
|
|
897
|
+
case MachineLinkState.RELATED_NOTES_BLOCKED:
|
|
898
|
+
return "link:repair-related-notes"
|
|
899
|
+
case MachineLinkState.VOCABULARY_SEMANTIC_REPAIR_BLOCKED:
|
|
900
|
+
return "link:repair-vocabulary-semantics"
|
|
901
|
+
case _:
|
|
902
|
+
return "link:diagnose"
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _machine_reason_code(model: WorkflowModel, state: MachineLinkState) -> str:
|
|
906
|
+
if model.last_transition is not None:
|
|
907
|
+
return model.last_transition.reason_code
|
|
908
|
+
return state.value
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def _machine_blockers(
|
|
912
|
+
category: WorkflowStateCategory,
|
|
913
|
+
model: WorkflowModel,
|
|
914
|
+
state: MachineLinkState,
|
|
915
|
+
) -> list[str]:
|
|
916
|
+
if category in {
|
|
917
|
+
WorkflowStateCategory.WAITING_AGENT,
|
|
918
|
+
WorkflowStateCategory.WAITING_EXTERNAL,
|
|
919
|
+
WorkflowStateCategory.WAITING_HUMAN,
|
|
920
|
+
WorkflowStateCategory.BLOCKED,
|
|
921
|
+
WorkflowStateCategory.FAILED,
|
|
922
|
+
}:
|
|
923
|
+
return [_machine_reason_code(model, state)]
|
|
924
|
+
return []
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def _error_context_from_model(
|
|
928
|
+
model: WorkflowModel,
|
|
929
|
+
state: MachineLinkState,
|
|
930
|
+
category: WorkflowStateCategory,
|
|
931
|
+
) -> JsonObject:
|
|
932
|
+
"""Synthesize the minimal recovery context owned by the LinkMachine state."""
|
|
933
|
+
|
|
934
|
+
if category not in {WorkflowStateCategory.BLOCKED, WorkflowStateCategory.FAILED}:
|
|
935
|
+
return {}
|
|
936
|
+
reason = _machine_reason_code(model, state) or state.value
|
|
937
|
+
return JsonObjectAdapter.validate_python(
|
|
938
|
+
{
|
|
939
|
+
"blocked_reason": reason,
|
|
940
|
+
"root_cause": reason,
|
|
941
|
+
"affected_artifact": state.value,
|
|
942
|
+
"next_action": _machine_resume_action(model, state) or "link:diagnose",
|
|
943
|
+
"retry_scope": "link",
|
|
944
|
+
}
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def _machine_agent_instructions(category: WorkflowStateCategory) -> list[str]:
|
|
949
|
+
if category == WorkflowStateCategory.WAITING_AGENT:
|
|
950
|
+
return ["Execute somente os efeitos em agent_directive.control.effects e retome /mednotes:link pelo resultado tipado."]
|
|
951
|
+
if category == WorkflowStateCategory.WAITING_EXTERNAL:
|
|
952
|
+
return ["Aguarde a condicao externa indicada antes de retomar /mednotes:link."]
|
|
953
|
+
if category == WorkflowStateCategory.WAITING_HUMAN:
|
|
954
|
+
return ["Peca a decisao humana fechada antes de aplicar links."]
|
|
955
|
+
if category in {WorkflowStateCategory.BLOCKED, WorkflowStateCategory.FAILED}:
|
|
956
|
+
return ["Use a decisao e o resume_action da FSM para recuperar o workflow de links."]
|
|
957
|
+
return ["Use a LinkMachine como fonte de verdade do estado de links."]
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def _version_control_safety(value: VersionControlSafety | dict[str, object]) -> VersionControlSafety:
|
|
961
|
+
if isinstance(value, VersionControlSafety):
|
|
962
|
+
return value
|
|
963
|
+
return VersionControlSafety.model_validate(value)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def assert_link_fsm_payload(payload: JsonObject) -> None:
|
|
967
|
+
payload = JsonObjectAdapter.validate_python(payload)
|
|
968
|
+
legacy_keys = set(payload) & LINK_FORBIDDEN_ROOT_KEYS
|
|
969
|
+
if legacy_keys:
|
|
970
|
+
raise ValueError(f"link FSM payload contains adapter root fields: {sorted(legacy_keys)}")
|
|
971
|
+
required_root_keys = LINK_ALLOWED_ROOT_KEYS - {"diagnostic_context"}
|
|
972
|
+
missing_keys = required_root_keys - set(payload)
|
|
973
|
+
if missing_keys:
|
|
974
|
+
raise ValueError(f"link FSM payload missing canonical root fields: {sorted(missing_keys)}")
|
|
975
|
+
unexpected_keys = set(payload) - LINK_ALLOWED_ROOT_KEYS
|
|
976
|
+
if unexpected_keys:
|
|
977
|
+
raise ValueError(f"link FSM payload contains unexpected root fields: {sorted(unexpected_keys)}")
|
|
978
|
+
try:
|
|
979
|
+
diagnostic_context = payload["diagnostic_context"]
|
|
980
|
+
except KeyError:
|
|
981
|
+
diagnostic_context = {}
|
|
982
|
+
assert_diagnostic_context_evidence_only(diagnostic_context)
|
|
983
|
+
if isinstance(diagnostic_context, dict) and "agent_directive" in diagnostic_context:
|
|
984
|
+
raise ValueError("link FSM diagnostic_context must not contain agent_directive")
|
|
985
|
+
fields = _link_payload_fields(payload)
|
|
986
|
+
if fields.workflow not in LINK_PUBLIC_WORKFLOWS:
|
|
987
|
+
raise ValueError("link FSM payload has invalid workflow")
|
|
988
|
+
if fields.progress_view_model.status != fields.state_machine_snapshot.current_category:
|
|
989
|
+
raise ValueError("link FSM status must match state_machine_snapshot category")
|
|
990
|
+
if fields.receipt.status != fields.progress_view_model.status:
|
|
991
|
+
raise ValueError("link FSM receipt status must match progress view status")
|
|
992
|
+
if fields.progress_view_model.status in {
|
|
993
|
+
WorkflowStateCategory.BLOCKED.value,
|
|
994
|
+
WorkflowStateCategory.FAILED.value,
|
|
995
|
+
} and not payload["error_context"]:
|
|
996
|
+
raise ValueError("link FSM blocked/failed payload requires error_context")
|
|
997
|
+
reports_model = WorkflowReports.model_validate(payload["reports"])
|
|
998
|
+
snapshot = WorkflowStateMachineSnapshot.model_validate(payload["state_machine_snapshot"])
|
|
999
|
+
progress_view_model = WorkflowProgressViewModel.model_validate(payload["progress_view_model"])
|
|
1000
|
+
assert_public_report_matches_progress(
|
|
1001
|
+
reports_model.public_report,
|
|
1002
|
+
workflow=fields.workflow,
|
|
1003
|
+
run_id=str(payload["run_id"]),
|
|
1004
|
+
progress_view_model=progress_view_model,
|
|
1005
|
+
label="link FSM",
|
|
1006
|
+
)
|
|
1007
|
+
assert_agent_directive_matches_progress(
|
|
1008
|
+
AgentDirective.model_validate(payload[LINK_AGENT_DIRECTIVE_FIELD]),
|
|
1009
|
+
workflow=fields.workflow,
|
|
1010
|
+
run_id=str(payload["run_id"]),
|
|
1011
|
+
progress_view_model=progress_view_model,
|
|
1012
|
+
snapshot=snapshot,
|
|
1013
|
+
allowed_effect_kinds=_allowed_agent_effect_kinds_for_category(snapshot.current_category),
|
|
1014
|
+
label="link FSM",
|
|
1015
|
+
)
|
|
1016
|
+
_assert_link_snapshot(snapshot)
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def _allowed_agent_effect_kinds_for_category(category: WorkflowStateCategory) -> set[WorkflowEffectKind]:
|
|
1020
|
+
"""Keep executable linker effects tied to the current FSM lane."""
|
|
1021
|
+
|
|
1022
|
+
match category:
|
|
1023
|
+
case WorkflowStateCategory.WAITING_AGENT:
|
|
1024
|
+
return {WorkflowEffectKind.RUN_SUBWORKFLOW, WorkflowEffectKind.CALL_SPECIALIST_MODEL}
|
|
1025
|
+
case WorkflowStateCategory.WAITING_EXTERNAL:
|
|
1026
|
+
return {WorkflowEffectKind.WAIT_EXTERNAL}
|
|
1027
|
+
case WorkflowStateCategory.WAITING_HUMAN:
|
|
1028
|
+
return {WorkflowEffectKind.ASK_HUMAN}
|
|
1029
|
+
case _:
|
|
1030
|
+
return set()
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def _assert_link_snapshot(snapshot: WorkflowStateMachineSnapshot) -> None:
|
|
1034
|
+
if snapshot.workflow not in LINK_PUBLIC_WORKFLOWS:
|
|
1035
|
+
raise ValueError("link FSM snapshot has invalid workflow")
|
|
1036
|
+
if snapshot.current_category != category_for_state(snapshot.current_state):
|
|
1037
|
+
raise ValueError("link FSM snapshot category does not match state")
|
|
1038
|
+
edges = _link_machine_edges()
|
|
1039
|
+
for transition in snapshot.transitions:
|
|
1040
|
+
if transition.to_category != category_for_state(transition.to_state):
|
|
1041
|
+
raise ValueError("link FSM transition category does not match state")
|
|
1042
|
+
edge = (transition.trigger, transition.from_state, transition.to_state)
|
|
1043
|
+
if edge not in edges:
|
|
1044
|
+
raise ValueError(f"unauthorized FSM transition: {edge}")
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def _link_machine_edges() -> set[tuple[str, str, str]]:
|
|
1048
|
+
"""Return every transition edge declared by the canonical LinkMachine."""
|
|
1049
|
+
|
|
1050
|
+
edges: set[tuple[str, str, str]] = set()
|
|
1051
|
+
for event in LinkMachine.events:
|
|
1052
|
+
for transition in event._transitions:
|
|
1053
|
+
for target in transition._targets:
|
|
1054
|
+
edges.add((event.id, str(transition.source.value), str(target.value)))
|
|
1055
|
+
return edges
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def _link_payload_fields(payload: JsonObject) -> _LinkPayloadFields:
|
|
1059
|
+
raw_fields: JsonObject = {
|
|
1060
|
+
"workflow": payload["workflow"],
|
|
1061
|
+
"progress_view_model": _json_object_subset(payload, "progress_view_model", ("status",)),
|
|
1062
|
+
"state_machine_snapshot": _json_object_subset(payload, "state_machine_snapshot", ("current_category",)),
|
|
1063
|
+
"receipt": _json_object_subset(payload, "receipt", ("status",)),
|
|
1064
|
+
}
|
|
1065
|
+
try:
|
|
1066
|
+
return _LinkPayloadFields.model_validate(raw_fields)
|
|
1067
|
+
except PydanticValidationError as exc:
|
|
1068
|
+
first = exc.errors()[0] if exc.errors() else {}
|
|
1069
|
+
loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
|
|
1070
|
+
msg = str(first.get("msg") or str(exc))
|
|
1071
|
+
raise ValueError(f"link FSM payload invalid: {loc}: {msg}") from exc
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def _json_object_subset(payload: JsonObject, field_name: str, keys: tuple[str, ...]) -> JsonObject:
|
|
1075
|
+
try:
|
|
1076
|
+
source = JsonObjectAdapter.validate_python(payload[field_name])
|
|
1077
|
+
except PydanticValidationError as exc:
|
|
1078
|
+
raise ValueError(f"link FSM payload invalid: {field_name} must be an object") from exc
|
|
1079
|
+
return {key: source[key] for key in keys if key in source}
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def link_cli_exit_code(payload: JsonObject) -> int:
|
|
1083
|
+
fields = _link_cli_exit_code_fields(payload)
|
|
1084
|
+
status = fields.progress_view_model.status
|
|
1085
|
+
match status:
|
|
1086
|
+
case "completed" | "completed_with_warnings":
|
|
1087
|
+
return EXIT_OK
|
|
1088
|
+
case "waiting_agent" | "waiting_external" | "waiting_human" | "blocked":
|
|
1089
|
+
return EXIT_VALIDATION
|
|
1090
|
+
case "failed":
|
|
1091
|
+
if _link_error_context_missing_path(payload):
|
|
1092
|
+
return EXIT_MISSING
|
|
1093
|
+
return EXIT_IO
|
|
1094
|
+
case _:
|
|
1095
|
+
return EXIT_USAGE
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def _link_error_context_missing_path(payload: JsonObject) -> bool:
|
|
1099
|
+
try:
|
|
1100
|
+
fields = _LinkErrorContextFields.model_validate(
|
|
1101
|
+
_json_object_subset(payload, "error_context", ("missing_inputs",))
|
|
1102
|
+
)
|
|
1103
|
+
except (KeyError, ValueError, PydanticValidationError):
|
|
1104
|
+
return False
|
|
1105
|
+
return "wiki_dir" in fields.missing_inputs
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _link_cli_exit_code_fields(payload: JsonObject) -> _LinkCliExitCodeFields:
|
|
1109
|
+
payload = JsonObjectAdapter.validate_python(payload)
|
|
1110
|
+
raw_fields: JsonObject = {
|
|
1111
|
+
"progress_view_model": _json_object_subset(payload, "progress_view_model", ("status",)),
|
|
1112
|
+
}
|
|
1113
|
+
try:
|
|
1114
|
+
return _LinkCliExitCodeFields.model_validate(raw_fields)
|
|
1115
|
+
except PydanticValidationError as exc:
|
|
1116
|
+
first = exc.errors()[0] if exc.errors() else {}
|
|
1117
|
+
loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
|
|
1118
|
+
msg = str(first.get("msg") or str(exc))
|
|
1119
|
+
raise ValueError(f"link FSM exit-code payload invalid: {loc}: {msg}") from exc
|