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,1855 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Annotated, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import ConfigDict, Field, StrictStr, field_validator, model_validator
|
|
8
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
9
|
+
from pydantic.json_schema import SkipJsonSchema
|
|
10
|
+
from statemachine import StateChart
|
|
11
|
+
from statemachine.states import States
|
|
12
|
+
|
|
13
|
+
from mednotes.domains.wiki.contracts.effect_payloads import (
|
|
14
|
+
RelatedNotesRecoveryStateEffectPayload,
|
|
15
|
+
WaitExternalEffectPayload,
|
|
16
|
+
)
|
|
17
|
+
from mednotes.domains.wiki.contracts.related_notes_runtime import LinkRelatedSyncResult, RelatedNotesRecoveryState
|
|
18
|
+
from mednotes.domains.wiki.contracts.workflow_outcomes import DecisionEvidence, WorkflowDecision
|
|
19
|
+
from mednotes.domains.wiki.flows.link_related.link_related_machine import (
|
|
20
|
+
LinkRelatedBoundaryEvent,
|
|
21
|
+
LinkRelatedMachine,
|
|
22
|
+
LinkRelatedRuntimeObservation,
|
|
23
|
+
LinkRelatedRuntimeObservedEvent,
|
|
24
|
+
category_for_link_related_state,
|
|
25
|
+
)
|
|
26
|
+
from mednotes.domains.wiki.flows.link_related.link_related_machine import (
|
|
27
|
+
LinkRelatedState as MachineLinkRelatedState,
|
|
28
|
+
)
|
|
29
|
+
from mednotes.kernel.agent_directive import (
|
|
30
|
+
AgentDirective,
|
|
31
|
+
agent_directive_from_progress_view_model,
|
|
32
|
+
assert_agent_directive_matches_progress,
|
|
33
|
+
)
|
|
34
|
+
from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
|
|
35
|
+
from mednotes.kernel.effect_intent import WorkflowEffect, WorkflowEffectKind
|
|
36
|
+
from mednotes.kernel.fsm_event import WorkflowEventLike
|
|
37
|
+
from mednotes.kernel.fsm_model import WorkflowModel
|
|
38
|
+
from mednotes.kernel.fsm_transition_result import WorkflowTransitionResult
|
|
39
|
+
from mednotes.kernel.progress import (
|
|
40
|
+
WorkflowProgressCounts,
|
|
41
|
+
WorkflowProgressEventType,
|
|
42
|
+
WorkflowProgressState,
|
|
43
|
+
WorkflowProgressStatus,
|
|
44
|
+
WorkflowProgressViewModel,
|
|
45
|
+
build_progress_view_model,
|
|
46
|
+
progress_state_from_view_model,
|
|
47
|
+
)
|
|
48
|
+
from mednotes.kernel.public_report import (
|
|
49
|
+
WorkflowPrimaryObjectiveSummary,
|
|
50
|
+
WorkflowPublicReport,
|
|
51
|
+
WorkflowReports,
|
|
52
|
+
assert_public_report_matches_progress,
|
|
53
|
+
public_progress_followup_line,
|
|
54
|
+
)
|
|
55
|
+
from mednotes.kernel.state_machine import (
|
|
56
|
+
WorkflowStateCategory,
|
|
57
|
+
WorkflowStateMachineSnapshot,
|
|
58
|
+
WorkflowTransition,
|
|
59
|
+
send_workflow_event,
|
|
60
|
+
)
|
|
61
|
+
from mednotes.kernel.workflow import (
|
|
62
|
+
HumanDecisionPacket,
|
|
63
|
+
ReceiptStatus,
|
|
64
|
+
VersionControlSafety,
|
|
65
|
+
WorkflowReceiptPayload,
|
|
66
|
+
assert_diagnostic_context_evidence_only,
|
|
67
|
+
diagnostic_context_evidence_only,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
LINK_RELATED_WORKFLOW = "/mednotes:link-related"
|
|
71
|
+
LINK_RELATED_SCHEMA = "medical-notes-workbench.link-related-fsm-result.v1"
|
|
72
|
+
LINK_RELATED_RECEIPT_SCHEMA = "medical-notes-workbench.link-related-receipt.v1"
|
|
73
|
+
LINK_RELATED_AGENT_DIRECTIVE_FIELD = "agent_directive"
|
|
74
|
+
|
|
75
|
+
_PHASE = "related_notes_recovery"
|
|
76
|
+
_WAITING_QUOTA_STATE = "waiting_for_external_quota"
|
|
77
|
+
_RECOVERY_BLOCKED_STATE = "related_notes_recovery_blocked"
|
|
78
|
+
_RECOVERING_STATE = "recovering_related_notes"
|
|
79
|
+
|
|
80
|
+
LINK_RELATED_ALLOWED_ROOT_KEYS = frozenset(
|
|
81
|
+
{
|
|
82
|
+
"schema",
|
|
83
|
+
"workflow",
|
|
84
|
+
"run_id",
|
|
85
|
+
"state_machine_snapshot",
|
|
86
|
+
"progress_view_model",
|
|
87
|
+
"decision",
|
|
88
|
+
"human_decision_packet",
|
|
89
|
+
"receipt",
|
|
90
|
+
"reports",
|
|
91
|
+
"agent_directive",
|
|
92
|
+
"artifacts",
|
|
93
|
+
"version_control_safety",
|
|
94
|
+
"diagnostic_context",
|
|
95
|
+
"error_context",
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
LINK_RELATED_FORBIDDEN_ROOT_KEYS = frozenset(
|
|
99
|
+
{
|
|
100
|
+
"status",
|
|
101
|
+
"phase",
|
|
102
|
+
"blocked_reason",
|
|
103
|
+
"next_action",
|
|
104
|
+
"required_inputs",
|
|
105
|
+
"human_decision_required",
|
|
106
|
+
"manual_instruction_allowed",
|
|
107
|
+
"selected_recovery_mode",
|
|
108
|
+
"retry_command",
|
|
109
|
+
"wiki_dir",
|
|
110
|
+
"export_path",
|
|
111
|
+
"planned_note_count",
|
|
112
|
+
"proposed_link_count",
|
|
113
|
+
"cleared_link_count",
|
|
114
|
+
"skipped_edge_count",
|
|
115
|
+
"applied_note_count",
|
|
116
|
+
"updates",
|
|
117
|
+
"skipped_edges",
|
|
118
|
+
"related_notes_recovery_state",
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def category_for_state(state: str) -> WorkflowStateCategory:
|
|
124
|
+
"""Map link-related leaf states to the public FSM category."""
|
|
125
|
+
|
|
126
|
+
return category_for_link_related_state(MachineLinkRelatedState(state))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class LinkRelatedFsmFacts(ContractModel):
|
|
130
|
+
run_id: str = Field(min_length=1)
|
|
131
|
+
initial_state: MachineLinkRelatedState
|
|
132
|
+
event: LinkRelatedBoundaryEvent
|
|
133
|
+
changed_files: list[str] = Field(default_factory=list)
|
|
134
|
+
mutated: bool = False
|
|
135
|
+
artifacts: JsonObject = Field(default_factory=dict)
|
|
136
|
+
version_control_safety: VersionControlSafety
|
|
137
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
138
|
+
|
|
139
|
+
@model_validator(mode="after")
|
|
140
|
+
def _event_must_match_fsm_entry(self) -> LinkRelatedFsmFacts:
|
|
141
|
+
if self.event.workflow != LINK_RELATED_WORKFLOW:
|
|
142
|
+
raise ValueError(f"link-related event workflow must be {LINK_RELATED_WORKFLOW}")
|
|
143
|
+
if self.event.run_id != self.run_id:
|
|
144
|
+
raise ValueError("link-related event run_id must match LinkRelatedFsmFacts.run_id")
|
|
145
|
+
if self.event.current_state != self.initial_state.value:
|
|
146
|
+
raise ValueError("link-related event current_state must match initial_state")
|
|
147
|
+
return self
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class _LinkRelatedRuntimeFacts(ContractModel):
|
|
151
|
+
"""Typed runtime bridge from Related Notes sync output to a machine event."""
|
|
152
|
+
|
|
153
|
+
run_id: str = Field(min_length=1)
|
|
154
|
+
mode: Literal["dry_run", "apply", "recover_export"]
|
|
155
|
+
sync_result: LinkRelatedSyncResult = Field(default_factory=LinkRelatedSyncResult)
|
|
156
|
+
version_control_safety: VersionControlSafety
|
|
157
|
+
next_action: str = ""
|
|
158
|
+
human_decision_packet: HumanDecisionPacket | None = None
|
|
159
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
160
|
+
|
|
161
|
+
@field_validator("sync_result", mode="before")
|
|
162
|
+
@classmethod
|
|
163
|
+
def _coerce_sync_result(cls, value: object) -> LinkRelatedSyncResult:
|
|
164
|
+
return LinkRelatedSyncResult.from_payload(value)
|
|
165
|
+
|
|
166
|
+
@model_validator(mode="after")
|
|
167
|
+
def _observation_must_be_modeled(self) -> _LinkRelatedRuntimeFacts:
|
|
168
|
+
observation = _link_related_runtime_observation(self)
|
|
169
|
+
if not (
|
|
170
|
+
observation.failed
|
|
171
|
+
or observation.export_missing
|
|
172
|
+
or observation.export_stale
|
|
173
|
+
or observation.preview_ready
|
|
174
|
+
or observation.applied
|
|
175
|
+
or observation.blocked
|
|
176
|
+
or observation.waiting_external
|
|
177
|
+
):
|
|
178
|
+
raise ValueError("effect_payload_contract_invalid: unmodeled related-notes runtime status")
|
|
179
|
+
return self
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class _LinkRelatedOperationDecisionFields(ContractModel):
|
|
183
|
+
"""Typed lens for the optional ask-human packet embedded in adapter output."""
|
|
184
|
+
|
|
185
|
+
model_config = ConfigDict(extra="ignore")
|
|
186
|
+
|
|
187
|
+
human_decision_packet: HumanDecisionPacket | None = None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class _LinkRelatedReportOperationFields(ContractModel):
|
|
191
|
+
"""Small projection lens for audit-only Related Notes operation payloads."""
|
|
192
|
+
|
|
193
|
+
model_config = ConfigDict(extra="ignore")
|
|
194
|
+
|
|
195
|
+
status: StrictStr = ""
|
|
196
|
+
phase: StrictStr = ""
|
|
197
|
+
blocked_reason: StrictStr = ""
|
|
198
|
+
next_action: StrictStr = ""
|
|
199
|
+
planned_note_count: int = Field(default=0, ge=0, strict=True)
|
|
200
|
+
proposed_link_count: int = Field(default=0, ge=0, strict=True)
|
|
201
|
+
cleared_link_count: int = Field(default=0, ge=0, strict=True)
|
|
202
|
+
skipped_edge_count: int = Field(default=0, ge=0, strict=True)
|
|
203
|
+
applied_note_count: int = Field(default=0, ge=0, strict=True)
|
|
204
|
+
updates: list[JsonObject] = Field(default_factory=list)
|
|
205
|
+
skipped_edges: list[JsonObject] = Field(default_factory=list)
|
|
206
|
+
export_relocation: JsonObject = Field(default_factory=dict)
|
|
207
|
+
related_notes_recovery_state: JsonObject = Field(default_factory=dict)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class _LinkRelatedErrorPayloadFields(ContractModel):
|
|
211
|
+
"""Typed lens for error evidence stored in the adapter operation payload."""
|
|
212
|
+
|
|
213
|
+
model_config = ConfigDict(extra="ignore")
|
|
214
|
+
|
|
215
|
+
validation_errors: list[object] = Field(default_factory=list)
|
|
216
|
+
contract_errors: list[object] = Field(default_factory=list)
|
|
217
|
+
hash_errors: list[object] = Field(default_factory=list)
|
|
218
|
+
stale_notes: list[object] = Field(default_factory=list)
|
|
219
|
+
forbidden_keys: list[object] = Field(default_factory=list)
|
|
220
|
+
detail: object = ""
|
|
221
|
+
selected_recovery_mode: object = ""
|
|
222
|
+
command_returncode: object = ""
|
|
223
|
+
error: object = ""
|
|
224
|
+
parse_error: object = ""
|
|
225
|
+
skipped_reason: object = ""
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def has_error_detail(self) -> bool:
|
|
229
|
+
return any(
|
|
230
|
+
(
|
|
231
|
+
self.validation_errors,
|
|
232
|
+
self.contract_errors,
|
|
233
|
+
self.hash_errors,
|
|
234
|
+
self.stale_notes,
|
|
235
|
+
self.forbidden_keys,
|
|
236
|
+
self.detail,
|
|
237
|
+
self.error,
|
|
238
|
+
self.parse_error,
|
|
239
|
+
self.skipped_reason,
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class _LinkRelatedPayloadProgressViewFields(ContractModel):
|
|
245
|
+
status: StrictStr
|
|
246
|
+
state: StrictStr = ""
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class _LinkRelatedPayloadSnapshotFields(ContractModel):
|
|
250
|
+
current_category: StrictStr
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class _LinkRelatedPayloadReceiptFields(ContractModel):
|
|
254
|
+
status: StrictStr
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class _LinkRelatedPayloadFields(ContractModel):
|
|
258
|
+
workflow: StrictStr
|
|
259
|
+
progress_view_model: _LinkRelatedPayloadProgressViewFields
|
|
260
|
+
state_machine_snapshot: _LinkRelatedPayloadSnapshotFields
|
|
261
|
+
receipt: _LinkRelatedPayloadReceiptFields
|
|
262
|
+
diagnostic_context: JsonObject = Field(default_factory=dict)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class LinkRelatedFsmResult(ContractModel):
|
|
266
|
+
schema_id: Literal["medical-notes-workbench.link-related-fsm-result.v1"] = Field(
|
|
267
|
+
default=LINK_RELATED_SCHEMA,
|
|
268
|
+
alias="schema",
|
|
269
|
+
)
|
|
270
|
+
workflow: Literal["/mednotes:link-related"] = LINK_RELATED_WORKFLOW
|
|
271
|
+
run_id: str = Field(min_length=1)
|
|
272
|
+
progress_state: SkipJsonSchema[WorkflowProgressState]
|
|
273
|
+
progress_view_model: WorkflowProgressViewModel
|
|
274
|
+
state_machine_snapshot: WorkflowStateMachineSnapshot
|
|
275
|
+
decision: WorkflowDecision | None = None
|
|
276
|
+
human_decision_packet: HumanDecisionPacket | None = None
|
|
277
|
+
receipt: WorkflowReceiptPayload
|
|
278
|
+
reports: WorkflowReports
|
|
279
|
+
agent_directive: JsonObject
|
|
280
|
+
artifacts: JsonObject = Field(default_factory=dict)
|
|
281
|
+
version_control_safety: VersionControlSafety
|
|
282
|
+
diagnostic_context: JsonObject = Field(default_factory=dict)
|
|
283
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
284
|
+
|
|
285
|
+
@model_validator(mode="before")
|
|
286
|
+
@classmethod
|
|
287
|
+
def _hydrate_progress_state_from_public_payload(cls, value: object) -> object:
|
|
288
|
+
"""Accept public payloads where progress_state is intentionally hidden."""
|
|
289
|
+
|
|
290
|
+
if not isinstance(value, dict) or "progress_state" in value or "progress_view_model" not in value:
|
|
291
|
+
return value
|
|
292
|
+
hydrated = dict(value)
|
|
293
|
+
progress_view = WorkflowProgressViewModel.model_validate(value["progress_view_model"])
|
|
294
|
+
hydrated["progress_state"] = progress_state_from_view_model(progress_view).to_payload()
|
|
295
|
+
return hydrated
|
|
296
|
+
|
|
297
|
+
@model_validator(mode="after")
|
|
298
|
+
def _progress_view_model_matches_state(self) -> LinkRelatedFsmResult:
|
|
299
|
+
expected = build_progress_view_model(self.progress_state).to_payload()
|
|
300
|
+
if self.progress_view_model.to_payload() != expected:
|
|
301
|
+
raise ValueError("progress_view_model must match progress_state")
|
|
302
|
+
return self
|
|
303
|
+
|
|
304
|
+
def to_payload(self) -> JsonObject:
|
|
305
|
+
payload: JsonObject = {
|
|
306
|
+
"schema": self.schema_id,
|
|
307
|
+
"workflow": self.workflow,
|
|
308
|
+
"run_id": self.run_id,
|
|
309
|
+
"state_machine_snapshot": self.state_machine_snapshot.to_payload(),
|
|
310
|
+
"progress_view_model": self.progress_view_model.to_payload(),
|
|
311
|
+
"decision": self.decision.to_payload() if self.decision is not None else None,
|
|
312
|
+
"human_decision_packet": self.human_decision_packet.to_payload()
|
|
313
|
+
if self.human_decision_packet is not None
|
|
314
|
+
else None,
|
|
315
|
+
"receipt": self.receipt.to_payload(),
|
|
316
|
+
"reports": self.reports.to_payload(),
|
|
317
|
+
"agent_directive": dict(self.agent_directive),
|
|
318
|
+
"artifacts": dict(self.artifacts),
|
|
319
|
+
"version_control_safety": self.version_control_safety.to_payload(),
|
|
320
|
+
"error_context": dict(self.error_context),
|
|
321
|
+
}
|
|
322
|
+
if self.diagnostic_context:
|
|
323
|
+
payload["diagnostic_context"] = dict(self.diagnostic_context)
|
|
324
|
+
payload = JsonObjectAdapter.validate_python(payload)
|
|
325
|
+
assert_link_related_fsm_payload(payload)
|
|
326
|
+
return payload
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def build_link_related_fsm_result(facts: LinkRelatedFsmFacts) -> LinkRelatedFsmResult:
|
|
330
|
+
"""Project one typed LinkRelatedMachine event into the public payload."""
|
|
331
|
+
|
|
332
|
+
return build_link_related_fsm_result_from_model(
|
|
333
|
+
_link_related_model_after_event(facts.initial_state, facts.event),
|
|
334
|
+
version_control_safety=facts.version_control_safety,
|
|
335
|
+
error_context=facts.error_context,
|
|
336
|
+
artifacts=facts.artifacts,
|
|
337
|
+
changed_files=facts.changed_files,
|
|
338
|
+
mutated=facts.mutated,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def link_related_fsm_payload_from_sync_result(
|
|
343
|
+
result: JsonObject,
|
|
344
|
+
*,
|
|
345
|
+
run_id: str,
|
|
346
|
+
mode: Literal["dry_run", "apply", "recover_export"],
|
|
347
|
+
version_control_safety: VersionControlSafety | dict[str, object],
|
|
348
|
+
) -> JsonObject:
|
|
349
|
+
return build_link_related_fsm_result(
|
|
350
|
+
link_related_fsm_facts_from_sync_result(
|
|
351
|
+
result,
|
|
352
|
+
run_id=run_id,
|
|
353
|
+
mode=mode,
|
|
354
|
+
version_control_safety=version_control_safety,
|
|
355
|
+
)
|
|
356
|
+
).to_payload()
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def build_link_related_fsm_result_from_model(
|
|
360
|
+
model: WorkflowModel,
|
|
361
|
+
*,
|
|
362
|
+
version_control_safety: VersionControlSafety | dict[str, object],
|
|
363
|
+
error_context: JsonObject | None = None,
|
|
364
|
+
artifacts: JsonObject | None = None,
|
|
365
|
+
changed_files: list[str] | None = None,
|
|
366
|
+
mutated: bool | None = None,
|
|
367
|
+
) -> LinkRelatedFsmResult:
|
|
368
|
+
"""Project the real LinkRelatedMachine model without reading operation payloads."""
|
|
369
|
+
|
|
370
|
+
_validate_link_related_machine_model(model)
|
|
371
|
+
state = MachineLinkRelatedState(model.state)
|
|
372
|
+
category = category_for_link_related_state(state)
|
|
373
|
+
progress_state = _progress_state_from_model(model, state, category)
|
|
374
|
+
progress_view_model = build_progress_view_model(progress_state)
|
|
375
|
+
snapshot = _snapshot_from_model(model, state, category)
|
|
376
|
+
safety = _version_control_safety(version_control_safety)
|
|
377
|
+
receipt = _receipt_from_model(
|
|
378
|
+
model,
|
|
379
|
+
progress_state=progress_state,
|
|
380
|
+
progress_view_model=progress_view_model,
|
|
381
|
+
snapshot=snapshot,
|
|
382
|
+
version_control_safety=safety,
|
|
383
|
+
changed_files=changed_files or [],
|
|
384
|
+
mutated=mutated,
|
|
385
|
+
)
|
|
386
|
+
reports_model = _reports_from_model(model, state, progress_state)
|
|
387
|
+
public_report = reports_model.public_report
|
|
388
|
+
diagnostic_context = _diagnostic_context_from_model(model, state, category)
|
|
389
|
+
agent_directive = agent_directive_from_progress_view_model(
|
|
390
|
+
progress_view_model,
|
|
391
|
+
schema="medical-notes-workbench.agent-directive.v1",
|
|
392
|
+
reason=_machine_reason_code(model, state),
|
|
393
|
+
effects=model.pending_effects,
|
|
394
|
+
blockers=_machine_blockers(category, model, state),
|
|
395
|
+
resume=progress_state.resume_action,
|
|
396
|
+
report_requires=["related_notes"],
|
|
397
|
+
summary=public_report.summary_text(),
|
|
398
|
+
instructions=_machine_agent_instructions(category),
|
|
399
|
+
).to_payload()
|
|
400
|
+
machine_error_context = error_context or _error_context_from_model(model, state, category)
|
|
401
|
+
return LinkRelatedFsmResult(
|
|
402
|
+
run_id=model.run_id,
|
|
403
|
+
progress_state=progress_state,
|
|
404
|
+
progress_view_model=progress_view_model,
|
|
405
|
+
state_machine_snapshot=snapshot,
|
|
406
|
+
decision=model.last_transition.decision if model.last_transition is not None else None,
|
|
407
|
+
human_decision_packet=model.last_transition.human_decision_packet if model.last_transition is not None else None,
|
|
408
|
+
receipt=receipt,
|
|
409
|
+
reports=reports_model,
|
|
410
|
+
agent_directive=JsonObjectAdapter.validate_python(agent_directive),
|
|
411
|
+
artifacts=artifacts or {},
|
|
412
|
+
version_control_safety=safety,
|
|
413
|
+
diagnostic_context=diagnostic_context,
|
|
414
|
+
error_context=machine_error_context,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def link_related_fsm_payload_from_model(
|
|
419
|
+
model: WorkflowModel,
|
|
420
|
+
*,
|
|
421
|
+
version_control_safety: VersionControlSafety | dict[str, object],
|
|
422
|
+
) -> JsonObject:
|
|
423
|
+
"""JSON boundary for the machine-driven link-related FSM projection."""
|
|
424
|
+
|
|
425
|
+
return build_link_related_fsm_result_from_model(
|
|
426
|
+
model,
|
|
427
|
+
version_control_safety=version_control_safety,
|
|
428
|
+
).to_payload()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def link_related_fsm_facts_from_sync_result(
|
|
432
|
+
result: JsonObject,
|
|
433
|
+
*,
|
|
434
|
+
run_id: str,
|
|
435
|
+
mode: Literal["dry_run", "apply", "recover_export"],
|
|
436
|
+
version_control_safety: VersionControlSafety | dict[str, object],
|
|
437
|
+
) -> LinkRelatedFsmFacts:
|
|
438
|
+
sync_result = LinkRelatedSyncResult.from_payload(result)
|
|
439
|
+
explicit_error_context = sync_result.error_context
|
|
440
|
+
operation_decision = _LinkRelatedOperationDecisionFields.model_validate(sync_result.operation_payload)
|
|
441
|
+
runtime_facts = _LinkRelatedRuntimeFacts(
|
|
442
|
+
run_id=run_id,
|
|
443
|
+
mode=mode,
|
|
444
|
+
sync_result=sync_result,
|
|
445
|
+
version_control_safety=version_control_safety,
|
|
446
|
+
next_action=sync_result.next_action,
|
|
447
|
+
human_decision_packet=operation_decision.human_decision_packet,
|
|
448
|
+
error_context=(
|
|
449
|
+
explicit_error_context.to_payload()
|
|
450
|
+
if explicit_error_context is not None
|
|
451
|
+
else _link_related_error_context_from_result(sync_result)
|
|
452
|
+
),
|
|
453
|
+
)
|
|
454
|
+
observation = _link_related_runtime_observation(runtime_facts)
|
|
455
|
+
initial_state = _link_related_runtime_source_state(runtime_facts, observation)
|
|
456
|
+
reason = _link_related_runtime_reason_code(runtime_facts, fallback=_link_related_observation_fallback_reason(observation))
|
|
457
|
+
event = LinkRelatedRuntimeObservedEvent(
|
|
458
|
+
workflow=LINK_RELATED_WORKFLOW,
|
|
459
|
+
run_id=run_id,
|
|
460
|
+
current_state=initial_state.value,
|
|
461
|
+
observation=observation,
|
|
462
|
+
audit_evidence=_link_related_runtime_audit_evidence(runtime_facts, reason),
|
|
463
|
+
)
|
|
464
|
+
changed_files = _link_related_changed_files(sync_result)
|
|
465
|
+
return LinkRelatedFsmFacts(
|
|
466
|
+
run_id=run_id,
|
|
467
|
+
initial_state=initial_state,
|
|
468
|
+
event=event,
|
|
469
|
+
changed_files=changed_files,
|
|
470
|
+
mutated=mode == "apply" and bool(changed_files or sync_result.applied_note_count),
|
|
471
|
+
artifacts=_link_related_artifacts(sync_result),
|
|
472
|
+
version_control_safety=version_control_safety,
|
|
473
|
+
error_context=runtime_facts.error_context,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _link_related_model_after_event(initial_state: MachineLinkRelatedState, event: WorkflowEventLike) -> WorkflowModel:
|
|
478
|
+
model = WorkflowModel.start(
|
|
479
|
+
workflow=LINK_RELATED_WORKFLOW,
|
|
480
|
+
run_id=event.run_id,
|
|
481
|
+
initial_state=initial_state.value,
|
|
482
|
+
)
|
|
483
|
+
send_workflow_event(
|
|
484
|
+
LinkRelatedMachine(model=model, state_field=WorkflowModel.STATECHART_STATE_FIELD),
|
|
485
|
+
event,
|
|
486
|
+
)
|
|
487
|
+
return model
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _link_related_runtime_source_state(
|
|
491
|
+
facts: _LinkRelatedRuntimeFacts,
|
|
492
|
+
observation: LinkRelatedRuntimeObservation,
|
|
493
|
+
) -> MachineLinkRelatedState:
|
|
494
|
+
"""Choose only the legal source state; LinkRelatedMachine still chooses the leaf."""
|
|
495
|
+
|
|
496
|
+
if facts.mode == "apply":
|
|
497
|
+
return MachineLinkRelatedState.APPLYING_RELATED_NOTES
|
|
498
|
+
if facts.mode == "recover_export":
|
|
499
|
+
return MachineLinkRelatedState.STALE_EXPORT
|
|
500
|
+
if observation.blocked and observation.preview_ready:
|
|
501
|
+
return MachineLinkRelatedState.PREVIEW_READY
|
|
502
|
+
return MachineLinkRelatedState.CHECKING_EXPORT
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _link_related_runtime_reason_code(facts: _LinkRelatedRuntimeFacts, *, fallback: str) -> str:
|
|
506
|
+
result = facts.sync_result
|
|
507
|
+
for value in (result.blocked_reason, result.skipped_reason, result.error, result.parse_error):
|
|
508
|
+
cleaned = value.strip()
|
|
509
|
+
if cleaned:
|
|
510
|
+
return cleaned
|
|
511
|
+
return fallback
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _link_related_export_missing(facts: _LinkRelatedRuntimeFacts) -> bool:
|
|
515
|
+
return _link_related_runtime_reason_code(facts, fallback="") == "related_notes_export_missing"
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _link_related_export_stale(facts: _LinkRelatedRuntimeFacts) -> bool:
|
|
519
|
+
return _link_related_runtime_reason_code(facts, fallback="") in {
|
|
520
|
+
"related_notes_export_stale",
|
|
521
|
+
"related_notes_export_still_stale",
|
|
522
|
+
"related_notes_hash_mismatch",
|
|
523
|
+
"related_notes_vault_mismatch",
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _link_related_runtime_audit_evidence(
|
|
528
|
+
facts: _LinkRelatedRuntimeFacts,
|
|
529
|
+
reason: str,
|
|
530
|
+
) -> JsonObject:
|
|
531
|
+
result = facts.sync_result
|
|
532
|
+
recovery = result.related_notes_recovery_state
|
|
533
|
+
report_operation = _link_related_report_operation(result)
|
|
534
|
+
related_notes_evidence: JsonObject = {
|
|
535
|
+
"status": result.status,
|
|
536
|
+
"blocked_reason": _link_related_runtime_reason_code(facts, fallback=""),
|
|
537
|
+
"selected_recovery_mode": result.selected_recovery_mode,
|
|
538
|
+
"manual_instruction_allowed": result.manual_instruction_allowed,
|
|
539
|
+
}
|
|
540
|
+
for key in ("automatic_recovery_unavailable_reason", "export_relocation"):
|
|
541
|
+
if key in result.operation_payload:
|
|
542
|
+
related_notes_evidence[key] = result.operation_payload[key]
|
|
543
|
+
return JsonObjectAdapter.validate_python(
|
|
544
|
+
{
|
|
545
|
+
"mode": facts.mode,
|
|
546
|
+
"runtime_status": result.status,
|
|
547
|
+
"runtime_phase": result.phase,
|
|
548
|
+
"runtime_reason": reason,
|
|
549
|
+
"related_notes": related_notes_evidence,
|
|
550
|
+
"report_operation": report_operation.model_dump(),
|
|
551
|
+
"counts": {
|
|
552
|
+
"planned_note_count": result.planned_note_count,
|
|
553
|
+
"proposed_link_count": result.proposed_link_count,
|
|
554
|
+
"cleared_link_count": result.cleared_link_count,
|
|
555
|
+
"skipped_edge_count": result.skipped_edge_count,
|
|
556
|
+
"applied_note_count": result.applied_note_count,
|
|
557
|
+
"fresh_record_count": recovery.fresh_record_count,
|
|
558
|
+
"stale_record_count": recovery.stale_record_count,
|
|
559
|
+
"remaining_count": recovery.remaining_count,
|
|
560
|
+
},
|
|
561
|
+
"settings": {
|
|
562
|
+
"min_score": result.min_score,
|
|
563
|
+
"max_links": result.max_links,
|
|
564
|
+
},
|
|
565
|
+
}
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _link_related_report_operation(result: LinkRelatedSyncResult) -> _LinkRelatedReportOperationFields:
|
|
570
|
+
"""Project adapter details into report-only facts without making them FSM state."""
|
|
571
|
+
|
|
572
|
+
return _LinkRelatedReportOperationFields.model_validate(
|
|
573
|
+
{
|
|
574
|
+
**result.operation_payload,
|
|
575
|
+
"status": result.status,
|
|
576
|
+
"phase": result.phase,
|
|
577
|
+
"blocked_reason": result.blocked_reason,
|
|
578
|
+
"next_action": result.next_action,
|
|
579
|
+
"planned_note_count": result.planned_note_count,
|
|
580
|
+
"proposed_link_count": result.proposed_link_count,
|
|
581
|
+
"cleared_link_count": result.cleared_link_count,
|
|
582
|
+
"skipped_edge_count": result.skipped_edge_count,
|
|
583
|
+
"applied_note_count": result.applied_note_count,
|
|
584
|
+
}
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _validate_link_related_machine_model(model: WorkflowModel) -> None:
|
|
589
|
+
if model.workflow != LINK_RELATED_WORKFLOW:
|
|
590
|
+
raise ValueError(f"link-related FSM projector requires workflow={LINK_RELATED_WORKFLOW}")
|
|
591
|
+
MachineLinkRelatedState(model.state)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _progress_state_from_model(
|
|
595
|
+
model: WorkflowModel,
|
|
596
|
+
state: MachineLinkRelatedState,
|
|
597
|
+
category: WorkflowStateCategory,
|
|
598
|
+
) -> WorkflowProgressState:
|
|
599
|
+
status = _machine_progress_status(category)
|
|
600
|
+
current, total, counts = _machine_counts(model, state, status)
|
|
601
|
+
return WorkflowProgressState(
|
|
602
|
+
workflow=LINK_RELATED_WORKFLOW,
|
|
603
|
+
run_id=model.run_id,
|
|
604
|
+
state=state.value,
|
|
605
|
+
phase=_machine_phase_for_state(state),
|
|
606
|
+
event_type=_machine_event_type(status),
|
|
607
|
+
message=_machine_message_for_state(state),
|
|
608
|
+
status=status,
|
|
609
|
+
current=current,
|
|
610
|
+
total=total,
|
|
611
|
+
counts=counts,
|
|
612
|
+
resume_action=_machine_resume_action(model, state),
|
|
613
|
+
resume_supported=status
|
|
614
|
+
in {
|
|
615
|
+
WorkflowProgressStatus.WAITING_AGENT,
|
|
616
|
+
WorkflowProgressStatus.WAITING_EXTERNAL,
|
|
617
|
+
WorkflowProgressStatus.WAITING_HUMAN,
|
|
618
|
+
WorkflowProgressStatus.BLOCKED,
|
|
619
|
+
},
|
|
620
|
+
can_continue_now=status
|
|
621
|
+
in {
|
|
622
|
+
WorkflowProgressStatus.RUNNING,
|
|
623
|
+
WorkflowProgressStatus.WAITING_AGENT,
|
|
624
|
+
},
|
|
625
|
+
decision=model.last_transition.decision.decision_summary()
|
|
626
|
+
if model.last_transition is not None and model.last_transition.decision is not None
|
|
627
|
+
else None,
|
|
628
|
+
technical_context=_machine_technical_context(model, state, category),
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _machine_counts(
|
|
633
|
+
model: WorkflowModel,
|
|
634
|
+
state: MachineLinkRelatedState,
|
|
635
|
+
status: WorkflowProgressStatus,
|
|
636
|
+
) -> tuple[int, int, WorkflowProgressCounts]:
|
|
637
|
+
event = _last_machine_event(model)
|
|
638
|
+
planned = _event_int(event, "planned_note_count")
|
|
639
|
+
fresh = _event_int(event, "fresh_record_count")
|
|
640
|
+
stale = _event_int(event, "stale_record_count")
|
|
641
|
+
remaining = _event_int(event, "remaining_count")
|
|
642
|
+
changed = _event_int(event, "changed_file_count")
|
|
643
|
+
|
|
644
|
+
if state == MachineLinkRelatedState.WAITING_EXTERNAL_QUOTA:
|
|
645
|
+
total = fresh + remaining
|
|
646
|
+
return (
|
|
647
|
+
fresh,
|
|
648
|
+
total,
|
|
649
|
+
WorkflowProgressCounts(
|
|
650
|
+
planned_items=total,
|
|
651
|
+
processed_items=fresh,
|
|
652
|
+
remaining_items=remaining,
|
|
653
|
+
blocked_items=remaining,
|
|
654
|
+
deferred_items=remaining,
|
|
655
|
+
),
|
|
656
|
+
)
|
|
657
|
+
if state == MachineLinkRelatedState.COMPLETED:
|
|
658
|
+
total = max(changed, planned)
|
|
659
|
+
return (
|
|
660
|
+
total,
|
|
661
|
+
total,
|
|
662
|
+
WorkflowProgressCounts(
|
|
663
|
+
planned_items=total,
|
|
664
|
+
processed_items=total,
|
|
665
|
+
mutated_files=changed,
|
|
666
|
+
written_files=changed,
|
|
667
|
+
),
|
|
668
|
+
)
|
|
669
|
+
if state == MachineLinkRelatedState.PREVIEW_READY:
|
|
670
|
+
return (
|
|
671
|
+
planned,
|
|
672
|
+
planned,
|
|
673
|
+
WorkflowProgressCounts(
|
|
674
|
+
planned_items=planned,
|
|
675
|
+
processed_items=planned,
|
|
676
|
+
),
|
|
677
|
+
)
|
|
678
|
+
if state == MachineLinkRelatedState.WAITING_HUMAN_CONFIRMATION:
|
|
679
|
+
return (
|
|
680
|
+
0,
|
|
681
|
+
planned,
|
|
682
|
+
WorkflowProgressCounts(
|
|
683
|
+
planned_items=planned,
|
|
684
|
+
remaining_items=planned,
|
|
685
|
+
blocked_items=planned,
|
|
686
|
+
),
|
|
687
|
+
)
|
|
688
|
+
if state in {MachineLinkRelatedState.EXPORT_REQUIRED, MachineLinkRelatedState.STALE_EXPORT}:
|
|
689
|
+
blocked = max(planned, stale, 1)
|
|
690
|
+
return (
|
|
691
|
+
0,
|
|
692
|
+
blocked,
|
|
693
|
+
WorkflowProgressCounts(
|
|
694
|
+
planned_items=planned,
|
|
695
|
+
remaining_items=blocked,
|
|
696
|
+
blocked_items=blocked,
|
|
697
|
+
),
|
|
698
|
+
)
|
|
699
|
+
if status in {WorkflowProgressStatus.BLOCKED, WorkflowProgressStatus.FAILED}:
|
|
700
|
+
blocked = max(planned, remaining, stale, 1)
|
|
701
|
+
return (
|
|
702
|
+
0,
|
|
703
|
+
blocked,
|
|
704
|
+
WorkflowProgressCounts(
|
|
705
|
+
planned_items=planned,
|
|
706
|
+
remaining_items=blocked,
|
|
707
|
+
blocked_items=blocked,
|
|
708
|
+
),
|
|
709
|
+
)
|
|
710
|
+
return 0, planned, WorkflowProgressCounts(planned_items=planned)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _snapshot_from_model(
|
|
714
|
+
model: WorkflowModel,
|
|
715
|
+
state: MachineLinkRelatedState,
|
|
716
|
+
category: WorkflowStateCategory,
|
|
717
|
+
) -> WorkflowStateMachineSnapshot:
|
|
718
|
+
return WorkflowStateMachineSnapshot(
|
|
719
|
+
workflow=LINK_RELATED_WORKFLOW,
|
|
720
|
+
run_id=model.run_id,
|
|
721
|
+
current_state=state.value,
|
|
722
|
+
current_category=category,
|
|
723
|
+
transitions=[_machine_snapshot_transition(transition) for transition in model.transition_log],
|
|
724
|
+
metadata={"reason": _machine_reason_code(model, state), "source": "LinkRelatedMachine"},
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _machine_snapshot_transition(transition: WorkflowTransitionResult) -> WorkflowTransition:
|
|
729
|
+
return WorkflowTransition(
|
|
730
|
+
workflow=transition.workflow,
|
|
731
|
+
from_state=transition.from_state,
|
|
732
|
+
to_state=transition.to_state,
|
|
733
|
+
to_category=category_for_link_related_state(MachineLinkRelatedState(transition.to_state)),
|
|
734
|
+
trigger=transition.trigger,
|
|
735
|
+
effects=list(transition.effects),
|
|
736
|
+
decision=transition.decision,
|
|
737
|
+
resume_action=transition.resume_action,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _receipt_from_model(
|
|
742
|
+
model: WorkflowModel,
|
|
743
|
+
*,
|
|
744
|
+
progress_state: WorkflowProgressState,
|
|
745
|
+
progress_view_model: WorkflowProgressViewModel,
|
|
746
|
+
snapshot: WorkflowStateMachineSnapshot,
|
|
747
|
+
version_control_safety: VersionControlSafety,
|
|
748
|
+
changed_files: list[str],
|
|
749
|
+
mutated: bool | None,
|
|
750
|
+
) -> WorkflowReceiptPayload:
|
|
751
|
+
return WorkflowReceiptPayload(
|
|
752
|
+
schema=LINK_RELATED_RECEIPT_SCHEMA,
|
|
753
|
+
workflow=LINK_RELATED_WORKFLOW,
|
|
754
|
+
run_id=model.run_id,
|
|
755
|
+
status=_machine_receipt_status(progress_state.status),
|
|
756
|
+
mutated=mutated if mutated is not None else version_control_safety.changed_file_count > 0,
|
|
757
|
+
next_action="" if progress_state.status == WorkflowProgressStatus.COMPLETED else progress_state.resume_action,
|
|
758
|
+
human_decision_required=progress_state.status == WorkflowProgressStatus.WAITING_HUMAN,
|
|
759
|
+
human_decision_packet=model.last_transition.human_decision_packet if model.last_transition is not None else None,
|
|
760
|
+
changed_files=changed_files,
|
|
761
|
+
version_control_safety=version_control_safety,
|
|
762
|
+
progress_state=progress_state,
|
|
763
|
+
progress_view_model=progress_view_model,
|
|
764
|
+
state_machine_snapshot=snapshot,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _reports_from_model(
|
|
769
|
+
model: WorkflowModel,
|
|
770
|
+
state: MachineLinkRelatedState,
|
|
771
|
+
progress_state: WorkflowProgressState,
|
|
772
|
+
) -> WorkflowReports:
|
|
773
|
+
summary = _machine_message_for_state(state)
|
|
774
|
+
public_lines = [summary]
|
|
775
|
+
followup_line = public_progress_followup_line(progress_state)
|
|
776
|
+
if followup_line:
|
|
777
|
+
public_lines.append(followup_line)
|
|
778
|
+
public_report = WorkflowPublicReport(
|
|
779
|
+
workflow=LINK_RELATED_WORKFLOW,
|
|
780
|
+
run_id=model.run_id,
|
|
781
|
+
headline=summary,
|
|
782
|
+
lines=public_lines,
|
|
783
|
+
)
|
|
784
|
+
related_notes_report = _related_notes_report_from_model(model, state, progress_state)
|
|
785
|
+
return WorkflowReports(
|
|
786
|
+
summary=summary,
|
|
787
|
+
public_report=public_report,
|
|
788
|
+
details={
|
|
789
|
+
"primary_objective_summary": _primary_objective_summary(model, state, progress_state).to_payload(),
|
|
790
|
+
"related_notes": related_notes_report,
|
|
791
|
+
},
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _primary_objective_summary(
|
|
796
|
+
model: WorkflowModel,
|
|
797
|
+
state: MachineLinkRelatedState,
|
|
798
|
+
progress_state: WorkflowProgressState,
|
|
799
|
+
) -> WorkflowPrimaryObjectiveSummary:
|
|
800
|
+
"""State-owned answer to whether Related Notes were actually updated."""
|
|
801
|
+
|
|
802
|
+
completed = state == MachineLinkRelatedState.COMPLETED or (
|
|
803
|
+
state == MachineLinkRelatedState.PREVIEW_READY and progress_state.counts.planned_items == 0
|
|
804
|
+
)
|
|
805
|
+
changed_count = max(progress_state.counts.mutated_files, progress_state.counts.written_files)
|
|
806
|
+
if state == MachineLinkRelatedState.PREVIEW_READY and completed:
|
|
807
|
+
mutation_summary = "Notas Relacionadas conferidas; nenhuma alteração era necessária."
|
|
808
|
+
elif state == MachineLinkRelatedState.PREVIEW_READY:
|
|
809
|
+
mutation_summary = "Prévia de Notas Relacionadas pronta; nada foi alterado ainda."
|
|
810
|
+
elif changed_count > 0:
|
|
811
|
+
mutation_summary = f"{changed_count} nota(s) tiveram Notas Relacionadas atualizadas."
|
|
812
|
+
else:
|
|
813
|
+
mutation_summary = "Nenhuma seção de Notas Relacionadas foi alterada nesta etapa."
|
|
814
|
+
return WorkflowPrimaryObjectiveSummary(
|
|
815
|
+
workflow=LINK_RELATED_WORKFLOW,
|
|
816
|
+
run_id=model.run_id,
|
|
817
|
+
objective="Atualizar a seção Notas Relacionadas a partir do export oficial.",
|
|
818
|
+
completed=completed,
|
|
819
|
+
status=state.value,
|
|
820
|
+
mutation_state="changed" if changed_count > 0 else "unchanged",
|
|
821
|
+
mutation_summary=mutation_summary,
|
|
822
|
+
remaining_work_summary=_link_related_remaining_work_summary(state, completed),
|
|
823
|
+
next_step_summary=_link_related_next_step_summary(progress_state, completed),
|
|
824
|
+
blocked_reason="" if completed else state.value,
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _link_related_remaining_work_summary(state: MachineLinkRelatedState, completed: bool) -> str:
|
|
829
|
+
if completed:
|
|
830
|
+
if state == MachineLinkRelatedState.PREVIEW_READY:
|
|
831
|
+
return "Export conferido; não havia alterações de Notas Relacionadas para aplicar."
|
|
832
|
+
return "Notas Relacionadas foram atualizadas e conferidas."
|
|
833
|
+
if state == MachineLinkRelatedState.PREVIEW_READY:
|
|
834
|
+
return "Ainda falta confirmar/aplicar a prévia para alterar a Wiki."
|
|
835
|
+
return _machine_message_for_state(state)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _link_related_next_step_summary(progress_state: WorkflowProgressState, completed: bool) -> str:
|
|
839
|
+
if completed:
|
|
840
|
+
return "Nenhuma ação pendente para Notas Relacionadas."
|
|
841
|
+
return progress_state.resume_action or "Retomar /mednotes:link-related pela rota oficial."
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def _related_notes_report_from_model(
|
|
845
|
+
model: WorkflowModel,
|
|
846
|
+
state: MachineLinkRelatedState,
|
|
847
|
+
progress_state: WorkflowProgressState,
|
|
848
|
+
) -> JsonObject:
|
|
849
|
+
"""Build the human/report projection without making it state truth.
|
|
850
|
+
|
|
851
|
+
The state remains owned by LinkRelatedMachine; this report carries the
|
|
852
|
+
adapter's typed operational details so users/tests can audit planned or
|
|
853
|
+
applied Related Notes changes without reintroducing non-FSM root fields.
|
|
854
|
+
"""
|
|
855
|
+
|
|
856
|
+
evidence = _machine_audit_evidence(model)
|
|
857
|
+
raw_operation = evidence.get("report_operation")
|
|
858
|
+
operation = (
|
|
859
|
+
_LinkRelatedReportOperationFields.model_validate(raw_operation)
|
|
860
|
+
if isinstance(raw_operation, dict)
|
|
861
|
+
else None
|
|
862
|
+
)
|
|
863
|
+
counts = _dict_from(evidence.get("counts")) or progress_state.counts.to_payload()
|
|
864
|
+
settings = _dict_from(evidence.get("settings"))
|
|
865
|
+
report: JsonObject = {
|
|
866
|
+
"schema": "medical-notes-workbench.link-related-machine-report.v1",
|
|
867
|
+
"source": "LinkRelatedMachine",
|
|
868
|
+
"counts": counts,
|
|
869
|
+
}
|
|
870
|
+
if settings:
|
|
871
|
+
report["settings"] = settings
|
|
872
|
+
if operation is not None:
|
|
873
|
+
operation_counts = {
|
|
874
|
+
"planned_note_count": operation.planned_note_count,
|
|
875
|
+
"proposed_link_count": operation.proposed_link_count,
|
|
876
|
+
"cleared_link_count": operation.cleared_link_count,
|
|
877
|
+
"skipped_edge_count": operation.skipped_edge_count,
|
|
878
|
+
"applied_note_count": operation.applied_note_count,
|
|
879
|
+
}
|
|
880
|
+
report["counts"] = {**counts, **operation_counts}
|
|
881
|
+
report["planned_changes"] = {
|
|
882
|
+
"updates": operation.updates,
|
|
883
|
+
"skipped_edges": operation.skipped_edges,
|
|
884
|
+
}
|
|
885
|
+
recovery_state = RelatedNotesRecoveryState.from_payload(operation.related_notes_recovery_state)
|
|
886
|
+
report["related_notes"] = {
|
|
887
|
+
"export_relocation": operation.export_relocation,
|
|
888
|
+
"recovery_progress": {
|
|
889
|
+
"fresh_record_count": recovery_state.fresh_record_count,
|
|
890
|
+
"partial_record_count": recovery_state.partial_record_count,
|
|
891
|
+
"stale_record_count": recovery_state.stale_record_count,
|
|
892
|
+
"record_count": recovery_state.record_count,
|
|
893
|
+
"total_note_count": recovery_state.total_note_count,
|
|
894
|
+
"remaining_count": recovery_state.remaining_count,
|
|
895
|
+
"embedded_count": recovery_state.embedded_count,
|
|
896
|
+
"reused_count": recovery_state.reused_count,
|
|
897
|
+
"attempt_count": recovery_state.attempt_count,
|
|
898
|
+
},
|
|
899
|
+
}
|
|
900
|
+
return JsonObjectAdapter.validate_python(report)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def _dict_from(value: object) -> JsonObject:
|
|
904
|
+
if isinstance(value, dict):
|
|
905
|
+
return JsonObjectAdapter.validate_python(value)
|
|
906
|
+
return {}
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _list_from(value: object) -> list[object]:
|
|
910
|
+
if isinstance(value, list):
|
|
911
|
+
return list(value)
|
|
912
|
+
return []
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _int_from(value: object) -> int:
|
|
916
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
917
|
+
return value
|
|
918
|
+
return 0
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def _diagnostic_context_from_model(
|
|
922
|
+
model: WorkflowModel,
|
|
923
|
+
state: MachineLinkRelatedState,
|
|
924
|
+
category: WorkflowStateCategory,
|
|
925
|
+
) -> JsonObject:
|
|
926
|
+
if category == WorkflowStateCategory.COMPLETED:
|
|
927
|
+
return {}
|
|
928
|
+
context: JsonObject = {
|
|
929
|
+
"schema": "medical-notes-workbench.link-related-fsm-diagnostic-context.v2",
|
|
930
|
+
"state": state.value,
|
|
931
|
+
"category": category.value,
|
|
932
|
+
"reason": _machine_reason_code(model, state),
|
|
933
|
+
"source": "LinkRelatedMachine",
|
|
934
|
+
}
|
|
935
|
+
evidence = _machine_audit_evidence(model)
|
|
936
|
+
for key, value in evidence.items():
|
|
937
|
+
if key == "report_operation":
|
|
938
|
+
continue
|
|
939
|
+
if key not in context:
|
|
940
|
+
context[key] = value
|
|
941
|
+
return diagnostic_context_evidence_only(context)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _machine_audit_evidence(model: WorkflowModel) -> JsonObject:
|
|
945
|
+
if not model.event_log:
|
|
946
|
+
return {}
|
|
947
|
+
event = _LinkRelatedMachineEventEvidence.model_validate(model.event_log[-1])
|
|
948
|
+
return JsonObjectAdapter.validate_python(event.audit_evidence)
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def _machine_technical_context(
|
|
952
|
+
model: WorkflowModel,
|
|
953
|
+
state: MachineLinkRelatedState,
|
|
954
|
+
category: WorkflowStateCategory,
|
|
955
|
+
) -> JsonObject:
|
|
956
|
+
event = _last_machine_event(model)
|
|
957
|
+
return JsonObjectAdapter.validate_python(
|
|
958
|
+
{
|
|
959
|
+
"reason": _machine_reason_code(model, state),
|
|
960
|
+
"category": category.value,
|
|
961
|
+
"source": "LinkRelatedMachine",
|
|
962
|
+
"trigger": _machine_trigger(model),
|
|
963
|
+
"fresh_record_count": _event_int(event, "fresh_record_count"),
|
|
964
|
+
"stale_record_count": _event_int(event, "stale_record_count"),
|
|
965
|
+
"remaining_count": _event_int(event, "remaining_count"),
|
|
966
|
+
"planned_note_count": _event_int(event, "planned_note_count"),
|
|
967
|
+
"changed_file_count": _event_int(event, "changed_file_count"),
|
|
968
|
+
}
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def _machine_progress_status(category: WorkflowStateCategory) -> WorkflowProgressStatus:
|
|
973
|
+
match category:
|
|
974
|
+
case WorkflowStateCategory.PREPARING | WorkflowStateCategory.RUNNING:
|
|
975
|
+
return WorkflowProgressStatus.RUNNING
|
|
976
|
+
case WorkflowStateCategory.WAITING_AGENT:
|
|
977
|
+
return WorkflowProgressStatus.WAITING_AGENT
|
|
978
|
+
case WorkflowStateCategory.WAITING_EXTERNAL:
|
|
979
|
+
return WorkflowProgressStatus.WAITING_EXTERNAL
|
|
980
|
+
case WorkflowStateCategory.WAITING_HUMAN:
|
|
981
|
+
return WorkflowProgressStatus.WAITING_HUMAN
|
|
982
|
+
case WorkflowStateCategory.BLOCKED:
|
|
983
|
+
return WorkflowProgressStatus.BLOCKED
|
|
984
|
+
case WorkflowStateCategory.FAILED:
|
|
985
|
+
return WorkflowProgressStatus.FAILED
|
|
986
|
+
case WorkflowStateCategory.COMPLETED:
|
|
987
|
+
return WorkflowProgressStatus.COMPLETED
|
|
988
|
+
case WorkflowStateCategory.COMPLETED_WITH_WARNINGS:
|
|
989
|
+
return WorkflowProgressStatus.COMPLETED_WITH_WARNINGS
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _machine_receipt_status(status: WorkflowProgressStatus) -> ReceiptStatus:
|
|
993
|
+
match status:
|
|
994
|
+
case WorkflowProgressStatus.RUNNING:
|
|
995
|
+
return "running"
|
|
996
|
+
case WorkflowProgressStatus.COMPLETED:
|
|
997
|
+
return "completed"
|
|
998
|
+
case WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
|
|
999
|
+
return "completed_with_warnings"
|
|
1000
|
+
case WorkflowProgressStatus.WAITING_AGENT:
|
|
1001
|
+
return "waiting_agent"
|
|
1002
|
+
case WorkflowProgressStatus.WAITING_EXTERNAL:
|
|
1003
|
+
return "waiting_external"
|
|
1004
|
+
case WorkflowProgressStatus.WAITING_HUMAN:
|
|
1005
|
+
return "waiting_human"
|
|
1006
|
+
case WorkflowProgressStatus.FAILED:
|
|
1007
|
+
return "failed"
|
|
1008
|
+
case WorkflowProgressStatus.BLOCKED:
|
|
1009
|
+
return "blocked"
|
|
1010
|
+
case _:
|
|
1011
|
+
return "blocked"
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def _machine_event_type(status: WorkflowProgressStatus) -> WorkflowProgressEventType:
|
|
1015
|
+
match status:
|
|
1016
|
+
case WorkflowProgressStatus.COMPLETED | WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
|
|
1017
|
+
return WorkflowProgressEventType.WORKFLOW_COMPLETED
|
|
1018
|
+
case WorkflowProgressStatus.FAILED:
|
|
1019
|
+
return WorkflowProgressEventType.WORKFLOW_FAILED
|
|
1020
|
+
case WorkflowProgressStatus.WAITING_EXTERNAL:
|
|
1021
|
+
return WorkflowProgressEventType.EXTERNAL_WAIT_STARTED
|
|
1022
|
+
case WorkflowProgressStatus.WAITING_HUMAN | WorkflowProgressStatus.BLOCKED:
|
|
1023
|
+
return WorkflowProgressEventType.DECISION_EMITTED
|
|
1024
|
+
case _:
|
|
1025
|
+
return WorkflowProgressEventType.STATE_ENTERED
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def _machine_phase_for_state(state: MachineLinkRelatedState) -> str:
|
|
1029
|
+
match state:
|
|
1030
|
+
case MachineLinkRelatedState.CHECKING_EXPORT | MachineLinkRelatedState.EXPORT_REQUIRED:
|
|
1031
|
+
return "related_notes_export"
|
|
1032
|
+
case MachineLinkRelatedState.STALE_EXPORT:
|
|
1033
|
+
return "related_notes_export_recovery"
|
|
1034
|
+
case MachineLinkRelatedState.PREVIEW_READY | MachineLinkRelatedState.WAITING_HUMAN_CONFIRMATION:
|
|
1035
|
+
return "related_notes_preview"
|
|
1036
|
+
case MachineLinkRelatedState.APPLYING_RELATED_NOTES | MachineLinkRelatedState.COMPLETED:
|
|
1037
|
+
return "related_notes_apply"
|
|
1038
|
+
case MachineLinkRelatedState.WAITING_EXTERNAL_QUOTA:
|
|
1039
|
+
return "related_notes_recovery"
|
|
1040
|
+
case MachineLinkRelatedState.APPLY_CANCELLED:
|
|
1041
|
+
return "related_notes_apply_cancelled"
|
|
1042
|
+
case MachineLinkRelatedState.RELATED_NOTES_EXPORT_BLOCKED:
|
|
1043
|
+
return "related_notes_export_blocked"
|
|
1044
|
+
case MachineLinkRelatedState.RELATED_NOTES_PREVIEW_BLOCKED:
|
|
1045
|
+
return "related_notes_preview_blocked"
|
|
1046
|
+
case MachineLinkRelatedState.RELATED_NOTES_APPLY_BLOCKED:
|
|
1047
|
+
return "related_notes_apply_blocked"
|
|
1048
|
+
case MachineLinkRelatedState.FAILED:
|
|
1049
|
+
return "related_notes_failed"
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _machine_message_for_state(state: MachineLinkRelatedState) -> str:
|
|
1053
|
+
match state:
|
|
1054
|
+
case MachineLinkRelatedState.EXPORT_REQUIRED:
|
|
1055
|
+
return "Export do Related Notes precisa ser gerado antes da sincronização."
|
|
1056
|
+
case MachineLinkRelatedState.STALE_EXPORT:
|
|
1057
|
+
return "Export do Related Notes ficou desatualizado."
|
|
1058
|
+
case MachineLinkRelatedState.PREVIEW_READY:
|
|
1059
|
+
return "Prévia das Notas Relacionadas pronta; nada foi alterado."
|
|
1060
|
+
case MachineLinkRelatedState.WAITING_HUMAN_CONFIRMATION:
|
|
1061
|
+
return "Preciso de confirmação antes de atualizar Notas Relacionadas."
|
|
1062
|
+
case MachineLinkRelatedState.APPLYING_RELATED_NOTES:
|
|
1063
|
+
return "Atualização das Notas Relacionadas está em execução."
|
|
1064
|
+
case MachineLinkRelatedState.WAITING_EXTERNAL_QUOTA:
|
|
1065
|
+
return "Notas Relacionadas aguardam cota externa para continuar."
|
|
1066
|
+
case MachineLinkRelatedState.COMPLETED:
|
|
1067
|
+
return "Notas Relacionadas atualizadas e conferidas."
|
|
1068
|
+
case MachineLinkRelatedState.APPLY_CANCELLED:
|
|
1069
|
+
return "Atualização das Notas Relacionadas cancelada antes de alterar o vault."
|
|
1070
|
+
case MachineLinkRelatedState.RELATED_NOTES_EXPORT_BLOCKED:
|
|
1071
|
+
return "Export das Notas Relacionadas bloqueado."
|
|
1072
|
+
case MachineLinkRelatedState.RELATED_NOTES_PREVIEW_BLOCKED:
|
|
1073
|
+
return "Prévia das Notas Relacionadas bloqueada."
|
|
1074
|
+
case MachineLinkRelatedState.RELATED_NOTES_APPLY_BLOCKED:
|
|
1075
|
+
return "Aplicação das Notas Relacionadas bloqueada."
|
|
1076
|
+
case MachineLinkRelatedState.FAILED:
|
|
1077
|
+
return "Notas Relacionadas falharam antes de concluir."
|
|
1078
|
+
case _:
|
|
1079
|
+
return "Workflow de Notas Relacionadas em andamento."
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _machine_resume_action(model: WorkflowModel, state: MachineLinkRelatedState) -> str:
|
|
1083
|
+
if state == MachineLinkRelatedState.COMPLETED:
|
|
1084
|
+
return ""
|
|
1085
|
+
if model.last_transition is not None and model.last_transition.resume_action:
|
|
1086
|
+
return model.last_transition.resume_action
|
|
1087
|
+
match state:
|
|
1088
|
+
case MachineLinkRelatedState.EXPORT_REQUIRED | MachineLinkRelatedState.STALE_EXPORT:
|
|
1089
|
+
return "link-related:recover-export"
|
|
1090
|
+
case MachineLinkRelatedState.WAITING_HUMAN_CONFIRMATION:
|
|
1091
|
+
return "link-related:confirm-apply"
|
|
1092
|
+
case MachineLinkRelatedState.APPLYING_RELATED_NOTES:
|
|
1093
|
+
return "link-related:apply"
|
|
1094
|
+
case MachineLinkRelatedState.WAITING_EXTERNAL_QUOTA:
|
|
1095
|
+
return "link-related:retry-export"
|
|
1096
|
+
case (
|
|
1097
|
+
MachineLinkRelatedState.APPLY_CANCELLED
|
|
1098
|
+
| MachineLinkRelatedState.RELATED_NOTES_EXPORT_BLOCKED
|
|
1099
|
+
| MachineLinkRelatedState.RELATED_NOTES_PREVIEW_BLOCKED
|
|
1100
|
+
| MachineLinkRelatedState.RELATED_NOTES_APPLY_BLOCKED
|
|
1101
|
+
| MachineLinkRelatedState.FAILED
|
|
1102
|
+
):
|
|
1103
|
+
return "link-related:diagnose"
|
|
1104
|
+
case _:
|
|
1105
|
+
return ""
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _machine_reason_code(model: WorkflowModel, state: MachineLinkRelatedState) -> str:
|
|
1109
|
+
if model.last_transition is not None:
|
|
1110
|
+
return model.last_transition.reason_code
|
|
1111
|
+
return state.value
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
def _machine_trigger(model: WorkflowModel) -> str:
|
|
1115
|
+
if model.last_transition is not None:
|
|
1116
|
+
return model.last_transition.trigger
|
|
1117
|
+
return ""
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def _machine_blockers(
|
|
1121
|
+
category: WorkflowStateCategory,
|
|
1122
|
+
model: WorkflowModel,
|
|
1123
|
+
state: MachineLinkRelatedState,
|
|
1124
|
+
) -> list[str]:
|
|
1125
|
+
if category in {
|
|
1126
|
+
WorkflowStateCategory.WAITING_AGENT,
|
|
1127
|
+
WorkflowStateCategory.WAITING_EXTERNAL,
|
|
1128
|
+
WorkflowStateCategory.WAITING_HUMAN,
|
|
1129
|
+
WorkflowStateCategory.BLOCKED,
|
|
1130
|
+
WorkflowStateCategory.FAILED,
|
|
1131
|
+
}:
|
|
1132
|
+
return [_machine_reason_code(model, state)]
|
|
1133
|
+
return []
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def _error_context_from_model(
|
|
1137
|
+
model: WorkflowModel,
|
|
1138
|
+
state: MachineLinkRelatedState,
|
|
1139
|
+
category: WorkflowStateCategory,
|
|
1140
|
+
) -> JsonObject:
|
|
1141
|
+
"""Synthesize recovery context from the LinkRelatedMachine leaf state."""
|
|
1142
|
+
|
|
1143
|
+
if category not in {WorkflowStateCategory.BLOCKED, WorkflowStateCategory.FAILED}:
|
|
1144
|
+
return {}
|
|
1145
|
+
reason = _machine_reason_code(model, state) or state.value
|
|
1146
|
+
return JsonObjectAdapter.validate_python(
|
|
1147
|
+
{
|
|
1148
|
+
"blocked_reason": reason,
|
|
1149
|
+
"root_cause": reason,
|
|
1150
|
+
"affected_artifact": state.value,
|
|
1151
|
+
"next_action": _machine_resume_action(model, state) or "link-related:diagnose",
|
|
1152
|
+
"retry_scope": "link-related",
|
|
1153
|
+
}
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def _machine_agent_instructions(category: WorkflowStateCategory) -> list[str]:
|
|
1158
|
+
if category == WorkflowStateCategory.WAITING_AGENT:
|
|
1159
|
+
return ["Execute somente os efeitos em agent_directive.control.effects e retome /mednotes:link-related pelo resultado tipado."]
|
|
1160
|
+
if category == WorkflowStateCategory.WAITING_EXTERNAL:
|
|
1161
|
+
return ["Aguarde a condição externa indicada antes de retomar /mednotes:link-related."]
|
|
1162
|
+
if category == WorkflowStateCategory.WAITING_HUMAN:
|
|
1163
|
+
return ["Peça a decisão humana fechada antes de atualizar Notas Relacionadas."]
|
|
1164
|
+
if category in {WorkflowStateCategory.BLOCKED, WorkflowStateCategory.FAILED}:
|
|
1165
|
+
return ["Use a decisão e o resume_action da FSM para recuperar /mednotes:link-related."]
|
|
1166
|
+
return ["Use a LinkRelatedMachine como fonte de verdade do estado de Notas Relacionadas."]
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def _last_machine_event(model: WorkflowModel) -> JsonObject | None:
|
|
1170
|
+
if not model.event_log:
|
|
1171
|
+
return None
|
|
1172
|
+
return model.event_log[-1]
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
class _LinkRelatedMachineEventEvidence(ContractModel):
|
|
1176
|
+
"""Typed lens over persisted link-related machine event evidence."""
|
|
1177
|
+
|
|
1178
|
+
model_config = ConfigDict(extra="ignore")
|
|
1179
|
+
|
|
1180
|
+
audit_evidence: JsonObject = Field(default_factory=dict)
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def _event_int(event: object, field_name: str) -> int:
|
|
1184
|
+
if isinstance(event, dict):
|
|
1185
|
+
if field_name in event:
|
|
1186
|
+
value = event[field_name]
|
|
1187
|
+
elif "observation" in event and isinstance(event["observation"], dict):
|
|
1188
|
+
observation = LinkRelatedRuntimeObservation.model_validate(event["observation"])
|
|
1189
|
+
value = getattr(observation, field_name, 0)
|
|
1190
|
+
else:
|
|
1191
|
+
value = 0
|
|
1192
|
+
else:
|
|
1193
|
+
value = getattr(event, field_name, 0) if event is not None else 0
|
|
1194
|
+
if isinstance(value, bool):
|
|
1195
|
+
return 0
|
|
1196
|
+
if isinstance(value, int) and value >= 0:
|
|
1197
|
+
return value
|
|
1198
|
+
return 0
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def _version_control_safety(value: VersionControlSafety | dict[str, object]) -> VersionControlSafety:
|
|
1202
|
+
if isinstance(value, VersionControlSafety):
|
|
1203
|
+
return value
|
|
1204
|
+
return VersionControlSafety.model_validate(value)
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def _link_related_runtime_observation(facts: _LinkRelatedRuntimeFacts) -> LinkRelatedRuntimeObservation:
|
|
1208
|
+
"""Convert adapter output to facts; the machine owns final state priority."""
|
|
1209
|
+
|
|
1210
|
+
result = facts.sync_result
|
|
1211
|
+
recovery = result.related_notes_recovery_state
|
|
1212
|
+
reason = _link_related_runtime_reason_code(facts, fallback="")
|
|
1213
|
+
current = _fresh_current(recovery)
|
|
1214
|
+
total = recovery.total_note_count
|
|
1215
|
+
return LinkRelatedRuntimeObservation(
|
|
1216
|
+
mode=facts.mode,
|
|
1217
|
+
failed=_link_related_observed_failed(facts),
|
|
1218
|
+
export_missing=reason == "related_notes_export_missing",
|
|
1219
|
+
export_stale=reason
|
|
1220
|
+
in {
|
|
1221
|
+
"related_notes_export_stale",
|
|
1222
|
+
"related_notes_export_still_stale",
|
|
1223
|
+
"related_notes_hash_mismatch",
|
|
1224
|
+
"related_notes_vault_mismatch",
|
|
1225
|
+
},
|
|
1226
|
+
preview_ready=facts.mode != "apply" and result.status in {"preview_ready", "completed", "recovered"},
|
|
1227
|
+
applied=facts.mode == "apply" and result.status == "completed",
|
|
1228
|
+
blocked=result.status == "blocked" or bool(reason and not _link_related_waiting_external_from_recovery(result)),
|
|
1229
|
+
waiting_external=_link_related_waiting_external_from_recovery(result),
|
|
1230
|
+
planned_note_count=result.planned_note_count,
|
|
1231
|
+
proposed_link_count=result.proposed_link_count,
|
|
1232
|
+
cleared_link_count=result.cleared_link_count,
|
|
1233
|
+
applied_note_count=result.applied_note_count,
|
|
1234
|
+
fresh_record_count=current,
|
|
1235
|
+
stale_record_count=recovery.stale_record_count,
|
|
1236
|
+
remaining_count=_remaining_count(recovery, current=current, total=total),
|
|
1237
|
+
reason_code=reason,
|
|
1238
|
+
next_action=_link_related_default_next_action(facts, reason or result.status or "related_notes"),
|
|
1239
|
+
export_path=result.export_path or result.default_export_name,
|
|
1240
|
+
related_notes_recovery_state=recovery.to_payload(),
|
|
1241
|
+
)
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def _link_related_observed_failed(facts: _LinkRelatedRuntimeFacts) -> bool:
|
|
1245
|
+
result = facts.sync_result
|
|
1246
|
+
return bool(result.error.strip() or result.parse_error.strip() or result.status == "failed")
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
def _link_related_waiting_external_from_recovery(result: LinkRelatedSyncResult) -> bool:
|
|
1250
|
+
recovery = result.related_notes_recovery_state
|
|
1251
|
+
reason = result.blocked_reason or recovery.blocked_reason
|
|
1252
|
+
return (
|
|
1253
|
+
result.status == "blocked"
|
|
1254
|
+
and recovery.status == "waiting_for_retry"
|
|
1255
|
+
and reason
|
|
1256
|
+
in {
|
|
1257
|
+
"related_notes_headless_quota_exhausted",
|
|
1258
|
+
"related_notes_headless_time_budget_exhausted",
|
|
1259
|
+
}
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def _link_related_observation_fallback_reason(observation: LinkRelatedRuntimeObservation) -> str:
|
|
1264
|
+
if observation.failed:
|
|
1265
|
+
return "related_notes_failed"
|
|
1266
|
+
if observation.export_missing:
|
|
1267
|
+
return "related_notes_export_missing"
|
|
1268
|
+
if observation.export_stale:
|
|
1269
|
+
return "related_notes_export_stale"
|
|
1270
|
+
if observation.waiting_external:
|
|
1271
|
+
return "related_notes_quota_wait"
|
|
1272
|
+
if observation.blocked:
|
|
1273
|
+
return "related_notes_blocked"
|
|
1274
|
+
if observation.applied:
|
|
1275
|
+
return "completed"
|
|
1276
|
+
if observation.preview_ready:
|
|
1277
|
+
return "preview_ready"
|
|
1278
|
+
return "related_notes"
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def _link_related_default_next_action(facts: _LinkRelatedRuntimeFacts, reason: str) -> str:
|
|
1282
|
+
if facts.next_action.strip():
|
|
1283
|
+
return facts.next_action.strip()
|
|
1284
|
+
result_next = facts.sync_result.next_action.strip()
|
|
1285
|
+
if result_next:
|
|
1286
|
+
return result_next
|
|
1287
|
+
match reason:
|
|
1288
|
+
case "preview_ready":
|
|
1289
|
+
if facts.sync_result.planned_note_count == 0:
|
|
1290
|
+
return ""
|
|
1291
|
+
return "Revisar a prévia e confirmar a atualização das Notas Relacionadas."
|
|
1292
|
+
case "waiting_external_related_notes":
|
|
1293
|
+
return "Aguardar a condição externa e retomar /mednotes:link-related pela rota oficial."
|
|
1294
|
+
case "related_notes_blocked":
|
|
1295
|
+
return "Corrigir o bloqueio informado e repetir /mednotes:link-related pela rota oficial."
|
|
1296
|
+
case "failed":
|
|
1297
|
+
return "Revisar o erro e retomar /mednotes:link-related pela rota oficial."
|
|
1298
|
+
case "completed" | "recovered":
|
|
1299
|
+
return ""
|
|
1300
|
+
raise AssertionError(f"unsupported link-related reason: {reason}")
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
def _link_related_artifacts(sync_result: LinkRelatedSyncResult) -> JsonObject:
|
|
1304
|
+
artifacts: JsonObject = {}
|
|
1305
|
+
if sync_result.export_path:
|
|
1306
|
+
artifacts["export_path"] = sync_result.export_path
|
|
1307
|
+
if sync_result.receipt_path:
|
|
1308
|
+
artifacts["receipt_path"] = sync_result.receipt_path
|
|
1309
|
+
return artifacts
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def _link_related_changed_files(sync_result: LinkRelatedSyncResult) -> list[str]:
|
|
1313
|
+
return [
|
|
1314
|
+
update.path
|
|
1315
|
+
for update in sync_result.updates
|
|
1316
|
+
if update.changed and update.path
|
|
1317
|
+
]
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
def _link_related_error_context_from_result(result: LinkRelatedSyncResult) -> JsonObject:
|
|
1321
|
+
evidence = _LinkRelatedErrorPayloadFields.model_validate(result.operation_payload)
|
|
1322
|
+
blocked_reason = (
|
|
1323
|
+
result.blocked_reason.strip()
|
|
1324
|
+
or result.skipped_reason.strip()
|
|
1325
|
+
or result.error.strip()
|
|
1326
|
+
or result.parse_error.strip()
|
|
1327
|
+
)
|
|
1328
|
+
if not blocked_reason and not evidence.has_error_detail:
|
|
1329
|
+
return {}
|
|
1330
|
+
next_action = result.next_action or "Revisar o erro e retomar /mednotes:link-related pela rota oficial."
|
|
1331
|
+
context: JsonObject = {
|
|
1332
|
+
"blocked_reason": blocked_reason,
|
|
1333
|
+
"root_cause": blocked_reason or "related_notes_error",
|
|
1334
|
+
"affected_artifact": "related_notes_export",
|
|
1335
|
+
"next_action": next_action,
|
|
1336
|
+
}
|
|
1337
|
+
for key in (
|
|
1338
|
+
"validation_errors",
|
|
1339
|
+
"contract_errors",
|
|
1340
|
+
"hash_errors",
|
|
1341
|
+
"stale_notes",
|
|
1342
|
+
"forbidden_keys",
|
|
1343
|
+
"detail",
|
|
1344
|
+
"selected_recovery_mode",
|
|
1345
|
+
"command_returncode",
|
|
1346
|
+
"error",
|
|
1347
|
+
"parse_error",
|
|
1348
|
+
"skipped_reason",
|
|
1349
|
+
):
|
|
1350
|
+
value = getattr(evidence, key)
|
|
1351
|
+
if value not in (None, "", [], {}):
|
|
1352
|
+
context[key] = value
|
|
1353
|
+
return JsonObjectAdapter.validate_python(context)
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def assert_link_related_fsm_payload(payload: JsonObject) -> None:
|
|
1357
|
+
payload = JsonObjectAdapter.validate_python(payload)
|
|
1358
|
+
forbidden_keys = set(payload) & LINK_RELATED_FORBIDDEN_ROOT_KEYS
|
|
1359
|
+
if forbidden_keys:
|
|
1360
|
+
raise ValueError(f"link-related FSM payload contains non-FSM root fields: {sorted(forbidden_keys)}")
|
|
1361
|
+
required_root_keys = LINK_RELATED_ALLOWED_ROOT_KEYS - {"diagnostic_context"}
|
|
1362
|
+
missing_keys = required_root_keys - set(payload)
|
|
1363
|
+
if missing_keys:
|
|
1364
|
+
raise ValueError(f"link-related FSM payload missing canonical root fields: {sorted(missing_keys)}")
|
|
1365
|
+
unexpected_keys = set(payload) - LINK_RELATED_ALLOWED_ROOT_KEYS
|
|
1366
|
+
if unexpected_keys:
|
|
1367
|
+
raise ValueError(f"link-related FSM payload contains unexpected root fields: {sorted(unexpected_keys)}")
|
|
1368
|
+
fields = _link_related_payload_fields(payload)
|
|
1369
|
+
assert_diagnostic_context_evidence_only(fields.diagnostic_context)
|
|
1370
|
+
if "agent_directive" in fields.diagnostic_context:
|
|
1371
|
+
raise ValueError("link-related FSM diagnostic_context must not contain agent_directive")
|
|
1372
|
+
if fields.workflow != LINK_RELATED_WORKFLOW:
|
|
1373
|
+
raise ValueError("link-related FSM payload has invalid workflow")
|
|
1374
|
+
if fields.progress_view_model.status != fields.state_machine_snapshot.current_category:
|
|
1375
|
+
raise ValueError("link-related FSM status must match state_machine_snapshot category")
|
|
1376
|
+
if fields.receipt.status != fields.progress_view_model.status:
|
|
1377
|
+
raise ValueError("link-related FSM receipt status must match progress view status")
|
|
1378
|
+
if fields.progress_view_model.status in {
|
|
1379
|
+
WorkflowStateCategory.BLOCKED.value,
|
|
1380
|
+
WorkflowStateCategory.FAILED.value,
|
|
1381
|
+
} and not payload["error_context"]:
|
|
1382
|
+
raise ValueError("link-related FSM blocked/failed payload requires error_context")
|
|
1383
|
+
reports_model = WorkflowReports.model_validate(payload["reports"])
|
|
1384
|
+
snapshot = WorkflowStateMachineSnapshot.model_validate(payload["state_machine_snapshot"])
|
|
1385
|
+
progress_view_model = WorkflowProgressViewModel.model_validate(payload["progress_view_model"])
|
|
1386
|
+
assert_public_report_matches_progress(
|
|
1387
|
+
reports_model.public_report,
|
|
1388
|
+
workflow=LINK_RELATED_WORKFLOW,
|
|
1389
|
+
run_id=str(payload["run_id"]),
|
|
1390
|
+
progress_view_model=progress_view_model,
|
|
1391
|
+
label="link-related FSM",
|
|
1392
|
+
)
|
|
1393
|
+
assert_agent_directive_matches_progress(
|
|
1394
|
+
AgentDirective.model_validate(payload[LINK_RELATED_AGENT_DIRECTIVE_FIELD]),
|
|
1395
|
+
workflow=LINK_RELATED_WORKFLOW,
|
|
1396
|
+
run_id=str(payload["run_id"]),
|
|
1397
|
+
progress_view_model=progress_view_model,
|
|
1398
|
+
snapshot=snapshot,
|
|
1399
|
+
allowed_effect_kinds=_allowed_agent_effect_kinds_for_category(snapshot.current_category),
|
|
1400
|
+
label="link-related FSM",
|
|
1401
|
+
)
|
|
1402
|
+
_assert_link_related_snapshot(snapshot)
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
def _allowed_agent_effect_kinds_for_category(category: WorkflowStateCategory) -> set[WorkflowEffectKind]:
|
|
1406
|
+
"""Keep Related Notes recovery effects tied to the current FSM lane."""
|
|
1407
|
+
|
|
1408
|
+
match category:
|
|
1409
|
+
case WorkflowStateCategory.WAITING_AGENT:
|
|
1410
|
+
return {WorkflowEffectKind.RUN_SUBWORKFLOW}
|
|
1411
|
+
case WorkflowStateCategory.WAITING_EXTERNAL:
|
|
1412
|
+
return {WorkflowEffectKind.WAIT_EXTERNAL}
|
|
1413
|
+
case WorkflowStateCategory.WAITING_HUMAN:
|
|
1414
|
+
return {WorkflowEffectKind.ASK_HUMAN}
|
|
1415
|
+
case _:
|
|
1416
|
+
return set()
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
def _assert_link_related_snapshot(snapshot: WorkflowStateMachineSnapshot) -> None:
|
|
1420
|
+
if snapshot.workflow != LINK_RELATED_WORKFLOW:
|
|
1421
|
+
raise ValueError("link-related FSM snapshot has invalid workflow")
|
|
1422
|
+
if snapshot.current_category != category_for_state(snapshot.current_state):
|
|
1423
|
+
raise ValueError("link-related FSM snapshot category does not match state")
|
|
1424
|
+
edges = _link_related_machine_edges()
|
|
1425
|
+
for transition in snapshot.transitions:
|
|
1426
|
+
if transition.to_category != category_for_state(transition.to_state):
|
|
1427
|
+
raise ValueError("link-related FSM transition category does not match state")
|
|
1428
|
+
edge = (transition.trigger, transition.from_state, transition.to_state)
|
|
1429
|
+
if edge not in edges:
|
|
1430
|
+
raise ValueError(f"unauthorized FSM transition: {edge}")
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
def _link_related_machine_edges() -> set[tuple[str, str, str]]:
|
|
1434
|
+
"""Return every transition edge declared by the canonical LinkRelatedMachine."""
|
|
1435
|
+
|
|
1436
|
+
edges: set[tuple[str, str, str]] = set()
|
|
1437
|
+
for event in LinkRelatedMachine.events:
|
|
1438
|
+
for transition in event._transitions:
|
|
1439
|
+
for target in transition._targets:
|
|
1440
|
+
edges.add((event.id, str(transition.source.value), str(target.value)))
|
|
1441
|
+
return edges
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
def _link_related_payload_fields(payload: JsonObject) -> _LinkRelatedPayloadFields:
|
|
1445
|
+
raw_fields: JsonObject = {
|
|
1446
|
+
"workflow": payload["workflow"],
|
|
1447
|
+
"progress_view_model": _json_object_subset(payload, "progress_view_model", ("status",)),
|
|
1448
|
+
"state_machine_snapshot": _json_object_subset(payload, "state_machine_snapshot", ("current_category",)),
|
|
1449
|
+
"receipt": _json_object_subset(payload, "receipt", ("status",)),
|
|
1450
|
+
"diagnostic_context": payload["diagnostic_context"] if "diagnostic_context" in payload else {},
|
|
1451
|
+
}
|
|
1452
|
+
try:
|
|
1453
|
+
return _LinkRelatedPayloadFields.model_validate(raw_fields)
|
|
1454
|
+
except PydanticValidationError as exc:
|
|
1455
|
+
first = exc.errors()[0] if exc.errors() else {}
|
|
1456
|
+
loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
|
|
1457
|
+
msg = str(first.get("msg") or str(exc))
|
|
1458
|
+
raise ValueError(f"link-related FSM payload invalid: {loc}: {msg}") from exc
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
def _json_object_subset(payload: JsonObject, field_name: str, keys: tuple[str, ...]) -> JsonObject:
|
|
1462
|
+
try:
|
|
1463
|
+
source = JsonObjectAdapter.validate_python(payload[field_name])
|
|
1464
|
+
except PydanticValidationError as exc:
|
|
1465
|
+
raise ValueError(f"link-related FSM payload invalid: {field_name} must be an object") from exc
|
|
1466
|
+
return {key: source[key] for key in keys if key in source}
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def link_related_cli_exit_code(payload: JsonObject) -> int:
|
|
1470
|
+
progress = _LinkRelatedPayloadProgressViewFields.model_validate(
|
|
1471
|
+
_json_object_subset(payload, "progress_view_model", ("status", "state"))
|
|
1472
|
+
)
|
|
1473
|
+
status = progress.status
|
|
1474
|
+
match status:
|
|
1475
|
+
case "completed" | "completed_with_warnings":
|
|
1476
|
+
return 0
|
|
1477
|
+
case "waiting_human" if progress.state in {
|
|
1478
|
+
MachineLinkRelatedState.PREVIEW_READY.value,
|
|
1479
|
+
MachineLinkRelatedState.WAITING_HUMAN_CONFIRMATION.value,
|
|
1480
|
+
}:
|
|
1481
|
+
return 0
|
|
1482
|
+
case "waiting_agent" | "waiting_external" | "waiting_human" | "blocked":
|
|
1483
|
+
return 3
|
|
1484
|
+
case "failed":
|
|
1485
|
+
return 5
|
|
1486
|
+
case _:
|
|
1487
|
+
return 1
|
|
1488
|
+
|
|
1489
|
+
|
|
1490
|
+
class RelatedNotesRecoveryMachineState(StrEnum):
|
|
1491
|
+
"""Small StateChart used when Related Notes recovery is embedded by a parent workflow."""
|
|
1492
|
+
|
|
1493
|
+
RECOVERING_RELATED_NOTES = _RECOVERING_STATE
|
|
1494
|
+
WAITING_EXTERNAL_QUOTA = _WAITING_QUOTA_STATE
|
|
1495
|
+
RELATED_NOTES_RECOVERY_BLOCKED = _RECOVERY_BLOCKED_STATE
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
class RelatedNotesRecoveryEvent(ContractModel):
|
|
1499
|
+
"""Base event for the embedded Related Notes recovery StateChart."""
|
|
1500
|
+
|
|
1501
|
+
workflow: str = Field(min_length=1)
|
|
1502
|
+
run_id: str = Field(min_length=1)
|
|
1503
|
+
current_state: str = Field(min_length=1)
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
class RelatedNotesRecoveryQuotaWaitEvent(RelatedNotesRecoveryEvent):
|
|
1507
|
+
name: Literal["related_notes_quota_wait"] = "related_notes_quota_wait"
|
|
1508
|
+
reason_code: str = Field(default="related_notes_quota_wait", min_length=1)
|
|
1509
|
+
next_action: str = Field(min_length=1)
|
|
1510
|
+
related_notes_recovery_state: RelatedNotesRecoveryStateEffectPayload = Field(
|
|
1511
|
+
default_factory=RelatedNotesRecoveryStateEffectPayload
|
|
1512
|
+
)
|
|
1513
|
+
|
|
1514
|
+
@field_validator("related_notes_recovery_state", mode="before")
|
|
1515
|
+
@classmethod
|
|
1516
|
+
def _coerce_recovery_state(cls, value: object) -> RelatedNotesRecoveryStateEffectPayload:
|
|
1517
|
+
return RelatedNotesRecoveryStateEffectPayload.from_payload(value)
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
class RelatedNotesRecoveryQuotaReadyEvent(RelatedNotesRecoveryEvent):
|
|
1521
|
+
name: Literal["related_notes_quota_ready"] = "related_notes_quota_ready"
|
|
1522
|
+
restored_by: str = Field(min_length=1)
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
class RelatedNotesRecoveryBlockedEvent(RelatedNotesRecoveryEvent):
|
|
1526
|
+
name: Literal["related_notes_recovery_blocked"] = "related_notes_recovery_blocked"
|
|
1527
|
+
reason_code: str = Field(min_length=1)
|
|
1528
|
+
next_action: str = Field(min_length=1)
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
RelatedNotesRecoveryBoundaryEvent = Annotated[
|
|
1532
|
+
RelatedNotesRecoveryQuotaWaitEvent | RelatedNotesRecoveryQuotaReadyEvent | RelatedNotesRecoveryBlockedEvent,
|
|
1533
|
+
Field(discriminator="name"),
|
|
1534
|
+
]
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
class RelatedNotesRecoveryMachine(StateChart[WorkflowModel]):
|
|
1538
|
+
"""StateChart for resumable Related Notes recovery embedded in parent workflows."""
|
|
1539
|
+
|
|
1540
|
+
allow_event_without_transition = False
|
|
1541
|
+
catch_errors_as_events = False
|
|
1542
|
+
states = States.from_enum(
|
|
1543
|
+
RelatedNotesRecoveryMachineState,
|
|
1544
|
+
initial=RelatedNotesRecoveryMachineState.RECOVERING_RELATED_NOTES,
|
|
1545
|
+
final={RelatedNotesRecoveryMachineState.RELATED_NOTES_RECOVERY_BLOCKED},
|
|
1546
|
+
use_enum_instance=False,
|
|
1547
|
+
)
|
|
1548
|
+
|
|
1549
|
+
related_notes_quota_wait = states.RECOVERING_RELATED_NOTES.to(
|
|
1550
|
+
states.WAITING_EXTERNAL_QUOTA,
|
|
1551
|
+
on="_on_quota_wait",
|
|
1552
|
+
)
|
|
1553
|
+
related_notes_quota_ready = states.WAITING_EXTERNAL_QUOTA.to(
|
|
1554
|
+
states.RECOVERING_RELATED_NOTES,
|
|
1555
|
+
on="_on_transition",
|
|
1556
|
+
)
|
|
1557
|
+
related_notes_recovery_blocked = states.RECOVERING_RELATED_NOTES.to(
|
|
1558
|
+
states.RELATED_NOTES_RECOVERY_BLOCKED,
|
|
1559
|
+
on="_on_blocked",
|
|
1560
|
+
)
|
|
1561
|
+
|
|
1562
|
+
def category_for_state(self, state: str) -> WorkflowStateCategory:
|
|
1563
|
+
return _category_for_recovery_state(RelatedNotesRecoveryMachineState(state))
|
|
1564
|
+
|
|
1565
|
+
def _on_transition(
|
|
1566
|
+
self,
|
|
1567
|
+
workflow_event: RelatedNotesRecoveryEvent,
|
|
1568
|
+
target: object,
|
|
1569
|
+
) -> WorkflowTransitionResult:
|
|
1570
|
+
return _recovery_transition(
|
|
1571
|
+
workflow_event,
|
|
1572
|
+
_recovery_target_state(target),
|
|
1573
|
+
reason_code=str(getattr(workflow_event, "name", "")),
|
|
1574
|
+
)
|
|
1575
|
+
|
|
1576
|
+
def _on_quota_wait(
|
|
1577
|
+
self,
|
|
1578
|
+
workflow_event: RelatedNotesRecoveryQuotaWaitEvent,
|
|
1579
|
+
target: object,
|
|
1580
|
+
) -> WorkflowTransitionResult:
|
|
1581
|
+
to_state = _recovery_target_state(target)
|
|
1582
|
+
effect = WorkflowEffect(
|
|
1583
|
+
workflow=workflow_event.workflow,
|
|
1584
|
+
run_id=workflow_event.run_id,
|
|
1585
|
+
effect_id="related-notes-recovery-quota-wait",
|
|
1586
|
+
origin_state=to_state.value,
|
|
1587
|
+
kind=WorkflowEffectKind.WAIT_EXTERNAL,
|
|
1588
|
+
target="related_notes.quota",
|
|
1589
|
+
payload=WaitExternalEffectPayload(
|
|
1590
|
+
related_notes_recovery_state=workflow_event.related_notes_recovery_state,
|
|
1591
|
+
next_action=workflow_event.next_action,
|
|
1592
|
+
).to_payload(),
|
|
1593
|
+
requires_receipt=False,
|
|
1594
|
+
no_resource_mutation=True,
|
|
1595
|
+
resume_action=workflow_event.next_action,
|
|
1596
|
+
)
|
|
1597
|
+
return _recovery_transition(
|
|
1598
|
+
workflow_event,
|
|
1599
|
+
to_state,
|
|
1600
|
+
reason_code=workflow_event.reason_code,
|
|
1601
|
+
effects=[effect],
|
|
1602
|
+
resume_action=workflow_event.next_action,
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1605
|
+
def _on_blocked(
|
|
1606
|
+
self,
|
|
1607
|
+
workflow_event: RelatedNotesRecoveryBlockedEvent,
|
|
1608
|
+
target: object,
|
|
1609
|
+
) -> WorkflowTransitionResult:
|
|
1610
|
+
to_state = _recovery_target_state(target)
|
|
1611
|
+
return _recovery_transition(
|
|
1612
|
+
workflow_event,
|
|
1613
|
+
to_state,
|
|
1614
|
+
reason_code=workflow_event.reason_code,
|
|
1615
|
+
decision=_hard_block_decision(
|
|
1616
|
+
reason_code=workflow_event.reason_code,
|
|
1617
|
+
next_action=workflow_event.next_action,
|
|
1618
|
+
),
|
|
1619
|
+
)
|
|
1620
|
+
|
|
1621
|
+
|
|
1622
|
+
def _category_for_recovery_state(state: RelatedNotesRecoveryMachineState) -> WorkflowStateCategory:
|
|
1623
|
+
match state:
|
|
1624
|
+
case RelatedNotesRecoveryMachineState.RECOVERING_RELATED_NOTES:
|
|
1625
|
+
return WorkflowStateCategory.RUNNING
|
|
1626
|
+
case RelatedNotesRecoveryMachineState.WAITING_EXTERNAL_QUOTA:
|
|
1627
|
+
return WorkflowStateCategory.WAITING_EXTERNAL
|
|
1628
|
+
case RelatedNotesRecoveryMachineState.RELATED_NOTES_RECOVERY_BLOCKED:
|
|
1629
|
+
return WorkflowStateCategory.BLOCKED
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
def _recovery_target_state(target: object) -> RelatedNotesRecoveryMachineState:
|
|
1633
|
+
value = getattr(target, "value", target)
|
|
1634
|
+
return RelatedNotesRecoveryMachineState(str(value))
|
|
1635
|
+
|
|
1636
|
+
|
|
1637
|
+
def _recovery_transition(
|
|
1638
|
+
workflow_event: RelatedNotesRecoveryEvent,
|
|
1639
|
+
to_state: RelatedNotesRecoveryMachineState,
|
|
1640
|
+
*,
|
|
1641
|
+
reason_code: str,
|
|
1642
|
+
effects: list[WorkflowEffect] | None = None,
|
|
1643
|
+
decision: WorkflowDecision | None = None,
|
|
1644
|
+
resume_action: str = "",
|
|
1645
|
+
) -> WorkflowTransitionResult:
|
|
1646
|
+
return WorkflowTransitionResult(
|
|
1647
|
+
workflow=workflow_event.workflow,
|
|
1648
|
+
run_id=workflow_event.run_id,
|
|
1649
|
+
from_state=workflow_event.current_state,
|
|
1650
|
+
to_state=to_state.value,
|
|
1651
|
+
trigger=str(getattr(workflow_event, "name", "")),
|
|
1652
|
+
reason_code=reason_code,
|
|
1653
|
+
effects=list(effects or []),
|
|
1654
|
+
decision=decision,
|
|
1655
|
+
resume_action=resume_action,
|
|
1656
|
+
)
|
|
1657
|
+
|
|
1658
|
+
|
|
1659
|
+
def _related_notes_recovery_model_after_event(
|
|
1660
|
+
*,
|
|
1661
|
+
workflow: str,
|
|
1662
|
+
run_id: str,
|
|
1663
|
+
event: RelatedNotesRecoveryBoundaryEvent,
|
|
1664
|
+
) -> WorkflowModel:
|
|
1665
|
+
model = WorkflowModel.start(
|
|
1666
|
+
workflow=workflow,
|
|
1667
|
+
run_id=run_id,
|
|
1668
|
+
initial_state=RelatedNotesRecoveryMachineState.RECOVERING_RELATED_NOTES.value,
|
|
1669
|
+
)
|
|
1670
|
+
send_workflow_event(
|
|
1671
|
+
RelatedNotesRecoveryMachine(model=model, state_field=WorkflowModel.STATECHART_STATE_FIELD),
|
|
1672
|
+
event,
|
|
1673
|
+
)
|
|
1674
|
+
return model
|
|
1675
|
+
|
|
1676
|
+
|
|
1677
|
+
def _recovery_snapshot_transition(transition: WorkflowTransitionResult) -> WorkflowTransition:
|
|
1678
|
+
return WorkflowTransition(
|
|
1679
|
+
workflow=transition.workflow,
|
|
1680
|
+
from_state=transition.from_state,
|
|
1681
|
+
to_state=transition.to_state,
|
|
1682
|
+
to_category=_category_for_recovery_state(RelatedNotesRecoveryMachineState(transition.to_state)),
|
|
1683
|
+
trigger=transition.trigger,
|
|
1684
|
+
effects=list(transition.effects),
|
|
1685
|
+
decision=transition.decision,
|
|
1686
|
+
resume_action=transition.resume_action,
|
|
1687
|
+
)
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
@dataclass(frozen=True)
|
|
1691
|
+
class RelatedNotesRecoveryMachineProjection:
|
|
1692
|
+
"""Recovery progress lens derived only from `RelatedNotesRecoveryMachine`.
|
|
1693
|
+
|
|
1694
|
+
The input is typed Related Notes recovery evidence, and the carrier state is
|
|
1695
|
+
the recovery StateChart. This object serializes that machine view; it does
|
|
1696
|
+
not define a parallel recovery status.
|
|
1697
|
+
"""
|
|
1698
|
+
|
|
1699
|
+
progress_state: WorkflowProgressState
|
|
1700
|
+
progress_view_model: WorkflowProgressViewModel
|
|
1701
|
+
snapshot: WorkflowStateMachineSnapshot
|
|
1702
|
+
|
|
1703
|
+
def to_payload(self) -> JsonObject:
|
|
1704
|
+
return JsonObjectAdapter.validate_python(
|
|
1705
|
+
{
|
|
1706
|
+
"progress_view_model": self.progress_view_model.to_payload(),
|
|
1707
|
+
"state_machine_snapshot": self.snapshot.to_payload(),
|
|
1708
|
+
}
|
|
1709
|
+
)
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
def build_related_notes_recovery_projection(
|
|
1713
|
+
*,
|
|
1714
|
+
workflow: str,
|
|
1715
|
+
run_id: str,
|
|
1716
|
+
recovery_state: object,
|
|
1717
|
+
next_action: str,
|
|
1718
|
+
) -> RelatedNotesRecoveryMachineProjection:
|
|
1719
|
+
typed_recovery = RelatedNotesRecoveryState.from_payload(recovery_state)
|
|
1720
|
+
reason_code = typed_recovery.blocked_reason or "related_notes_recovery_blocked"
|
|
1721
|
+
waiting_external = typed_recovery.status == "waiting_for_retry"
|
|
1722
|
+
current = _fresh_current(typed_recovery)
|
|
1723
|
+
total = typed_recovery.total_note_count
|
|
1724
|
+
remaining = _remaining_count(typed_recovery, current=current, total=total)
|
|
1725
|
+
if total and current > total:
|
|
1726
|
+
current = max(0, total - remaining) if remaining else total
|
|
1727
|
+
counts = WorkflowProgressCounts(
|
|
1728
|
+
planned_items=total,
|
|
1729
|
+
processed_items=current,
|
|
1730
|
+
cache_hits=typed_recovery.reused_count,
|
|
1731
|
+
api_calls=typed_recovery.embedded_count,
|
|
1732
|
+
remaining_items=remaining,
|
|
1733
|
+
blocked_items=0 if waiting_external else remaining or total,
|
|
1734
|
+
)
|
|
1735
|
+
|
|
1736
|
+
if waiting_external:
|
|
1737
|
+
event: RelatedNotesRecoveryBoundaryEvent = RelatedNotesRecoveryQuotaWaitEvent(
|
|
1738
|
+
workflow=workflow,
|
|
1739
|
+
run_id=run_id,
|
|
1740
|
+
current_state=RelatedNotesRecoveryMachineState.RECOVERING_RELATED_NOTES.value,
|
|
1741
|
+
reason_code=reason_code,
|
|
1742
|
+
next_action=next_action,
|
|
1743
|
+
related_notes_recovery_state=typed_recovery,
|
|
1744
|
+
)
|
|
1745
|
+
resume_supported = typed_recovery.resume_supported
|
|
1746
|
+
can_continue_now = False
|
|
1747
|
+
else:
|
|
1748
|
+
event = RelatedNotesRecoveryBlockedEvent(
|
|
1749
|
+
workflow=workflow,
|
|
1750
|
+
run_id=run_id,
|
|
1751
|
+
current_state=RelatedNotesRecoveryMachineState.RECOVERING_RELATED_NOTES.value,
|
|
1752
|
+
reason_code=reason_code,
|
|
1753
|
+
next_action=next_action,
|
|
1754
|
+
)
|
|
1755
|
+
resume_supported = False
|
|
1756
|
+
can_continue_now = False
|
|
1757
|
+
|
|
1758
|
+
model = _related_notes_recovery_model_after_event(workflow=workflow, run_id=run_id, event=event)
|
|
1759
|
+
state = RelatedNotesRecoveryMachineState(model.state)
|
|
1760
|
+
category = _category_for_recovery_state(state)
|
|
1761
|
+
status = _machine_progress_status(category)
|
|
1762
|
+
event_type = (
|
|
1763
|
+
WorkflowProgressEventType.EXTERNAL_WAIT_STARTED
|
|
1764
|
+
if status == WorkflowProgressStatus.WAITING_EXTERNAL
|
|
1765
|
+
else WorkflowProgressEventType.DECISION_EMITTED
|
|
1766
|
+
)
|
|
1767
|
+
transition = model.last_transition
|
|
1768
|
+
if transition is None:
|
|
1769
|
+
raise ValueError("related-notes recovery event did not produce a machine transition")
|
|
1770
|
+
decision = transition.decision
|
|
1771
|
+
resume_action = transition.resume_action
|
|
1772
|
+
|
|
1773
|
+
progress_state = WorkflowProgressState(
|
|
1774
|
+
workflow=workflow,
|
|
1775
|
+
run_id=run_id,
|
|
1776
|
+
state=state.value,
|
|
1777
|
+
phase=_PHASE,
|
|
1778
|
+
event_type=event_type,
|
|
1779
|
+
message=_message_for(waiting_external=waiting_external, reason_code=reason_code),
|
|
1780
|
+
status=status,
|
|
1781
|
+
current=current,
|
|
1782
|
+
total=total,
|
|
1783
|
+
counts=counts,
|
|
1784
|
+
resume_action=resume_action,
|
|
1785
|
+
resume_supported=resume_supported,
|
|
1786
|
+
can_continue_now=can_continue_now,
|
|
1787
|
+
decision=decision.decision_summary() if decision is not None else None,
|
|
1788
|
+
technical_context={
|
|
1789
|
+
"recovery_state_status": typed_recovery.status,
|
|
1790
|
+
"blocked_reason": reason_code,
|
|
1791
|
+
"attempt_count": typed_recovery.attempt_count,
|
|
1792
|
+
"fresh_record_count": typed_recovery.fresh_record_count,
|
|
1793
|
+
"stale_record_count": typed_recovery.stale_record_count,
|
|
1794
|
+
"total_note_count": total,
|
|
1795
|
+
"remaining_count": remaining,
|
|
1796
|
+
},
|
|
1797
|
+
)
|
|
1798
|
+
snapshot = WorkflowStateMachineSnapshot(
|
|
1799
|
+
workflow=workflow,
|
|
1800
|
+
run_id=run_id,
|
|
1801
|
+
current_state=state.value,
|
|
1802
|
+
current_category=category,
|
|
1803
|
+
transitions=[_recovery_snapshot_transition(item) for item in model.transition_log],
|
|
1804
|
+
metadata={
|
|
1805
|
+
"source": "RelatedNotesRecoveryMachine",
|
|
1806
|
+
"recovery_state_schema": typed_recovery.schema_id,
|
|
1807
|
+
"blocked_reason": reason_code,
|
|
1808
|
+
},
|
|
1809
|
+
)
|
|
1810
|
+
|
|
1811
|
+
return RelatedNotesRecoveryMachineProjection(
|
|
1812
|
+
progress_state=progress_state,
|
|
1813
|
+
progress_view_model=build_progress_view_model(progress_state),
|
|
1814
|
+
snapshot=snapshot,
|
|
1815
|
+
)
|
|
1816
|
+
|
|
1817
|
+
|
|
1818
|
+
def _hard_block_decision(*, reason_code: str, next_action: str) -> WorkflowDecision:
|
|
1819
|
+
return WorkflowDecision(
|
|
1820
|
+
kind="hard_block",
|
|
1821
|
+
phase=_PHASE,
|
|
1822
|
+
reason_code=reason_code,
|
|
1823
|
+
public_summary="A recuperacao do Related Notes esta bloqueada antes de alterar a Wiki.",
|
|
1824
|
+
developer_summary="Related Notes recovery emitted a hard block before vault mutation.",
|
|
1825
|
+
evidence=[
|
|
1826
|
+
DecisionEvidence(
|
|
1827
|
+
summary="A recuperacao informou bloqueio operacional.",
|
|
1828
|
+
technical_code=reason_code,
|
|
1829
|
+
source="related_notes_recovery_state",
|
|
1830
|
+
)
|
|
1831
|
+
],
|
|
1832
|
+
next_action=next_action,
|
|
1833
|
+
)
|
|
1834
|
+
|
|
1835
|
+
|
|
1836
|
+
def _fresh_current(payload: RelatedNotesRecoveryState | dict[str, object]) -> int:
|
|
1837
|
+
state = RelatedNotesRecoveryState.from_payload(payload)
|
|
1838
|
+
return state.fresh_record_count or state.partial_record_count
|
|
1839
|
+
|
|
1840
|
+
|
|
1841
|
+
def _remaining_count(payload: RelatedNotesRecoveryState | dict[str, object], *, current: int, total: int) -> int:
|
|
1842
|
+
value = RelatedNotesRecoveryState.from_payload(payload).remaining_count
|
|
1843
|
+
if value:
|
|
1844
|
+
return min(value, total) if total else value
|
|
1845
|
+
return max(0, total - current)
|
|
1846
|
+
|
|
1847
|
+
|
|
1848
|
+
def _message_for(*, waiting_external: bool, reason_code: str) -> str:
|
|
1849
|
+
if waiting_external:
|
|
1850
|
+
if reason_code == "related_notes_headless_quota_exhausted":
|
|
1851
|
+
return "Related Notes aguardando cota externa para retomar pela rota oficial."
|
|
1852
|
+
if reason_code == "related_notes_headless_time_budget_exhausted":
|
|
1853
|
+
return "Related Notes pausou a indexação para evitar uma execução longa; a próxima tentativa retoma do índice parcial."
|
|
1854
|
+
return "Related Notes aguardando condição externa para retomar pela rota oficial."
|
|
1855
|
+
return f"Related Notes bloqueado: {reason_code}."
|