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,2768 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import ConfigDict, Field, StrictStr, field_validator, model_validator
|
|
7
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
8
|
+
from pydantic.json_schema import SkipJsonSchema
|
|
9
|
+
|
|
10
|
+
from mednotes.domains.wiki.contracts.effect_payloads import (
|
|
11
|
+
LinkWorkflowRunEffectPayload,
|
|
12
|
+
RelatedNotesRecoveryStateEffectPayload,
|
|
13
|
+
)
|
|
14
|
+
from mednotes.domains.wiki.contracts.related_notes_runtime import RelatedNotesRecoveryState
|
|
15
|
+
from mednotes.domains.wiki.contracts.workflow_guardrails import error_context as build_error_context
|
|
16
|
+
from mednotes.domains.wiki.contracts.workflow_outcomes import (
|
|
17
|
+
DecisionEvidence,
|
|
18
|
+
HumanDecisionOption,
|
|
19
|
+
RejectedAutomation,
|
|
20
|
+
WorkflowDecision,
|
|
21
|
+
)
|
|
22
|
+
from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_machine import (
|
|
23
|
+
FixWikiBoundaryEvent,
|
|
24
|
+
FixWikiBoundaryEventAdapter,
|
|
25
|
+
FixWikiMachine,
|
|
26
|
+
FixWikiRuntimeObservation,
|
|
27
|
+
RuntimeObservedEvent,
|
|
28
|
+
)
|
|
29
|
+
from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_primary_objective import fix_wiki_primary_objective_summary
|
|
30
|
+
from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_states import (
|
|
31
|
+
FIX_WIKI_WORKFLOW,
|
|
32
|
+
FixWikiReason,
|
|
33
|
+
FixWikiState,
|
|
34
|
+
category_for_state,
|
|
35
|
+
reason_for_state,
|
|
36
|
+
)
|
|
37
|
+
from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_states import (
|
|
38
|
+
FixWikiDiagnosisLane as FixWikiDiagnosisLane,
|
|
39
|
+
)
|
|
40
|
+
from mednotes.kernel.agent_directive import (
|
|
41
|
+
AgentDirective,
|
|
42
|
+
agent_directive_from_progress_view_model,
|
|
43
|
+
assert_agent_directive_matches_progress,
|
|
44
|
+
)
|
|
45
|
+
from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
|
|
46
|
+
from mednotes.kernel.effects import WorkflowEffect, WorkflowEffectKind
|
|
47
|
+
from mednotes.kernel.fsm_model import WorkflowModel
|
|
48
|
+
from mednotes.kernel.progress import (
|
|
49
|
+
WorkflowProgressCounts,
|
|
50
|
+
WorkflowProgressEvent,
|
|
51
|
+
WorkflowProgressEventType,
|
|
52
|
+
WorkflowProgressState,
|
|
53
|
+
WorkflowProgressStatus,
|
|
54
|
+
WorkflowProgressViewModel,
|
|
55
|
+
build_progress_view_model,
|
|
56
|
+
progress_state_from_view_model,
|
|
57
|
+
)
|
|
58
|
+
from mednotes.kernel.public_report import (
|
|
59
|
+
WorkflowPublicReport,
|
|
60
|
+
WorkflowReports,
|
|
61
|
+
assert_public_report_matches_progress,
|
|
62
|
+
public_progress_followup_line,
|
|
63
|
+
)
|
|
64
|
+
from mednotes.kernel.state_machine import (
|
|
65
|
+
WorkflowStateCategory,
|
|
66
|
+
WorkflowStateMachineSnapshot,
|
|
67
|
+
WorkflowTransition,
|
|
68
|
+
send_workflow_event,
|
|
69
|
+
)
|
|
70
|
+
from mednotes.kernel.workflow import (
|
|
71
|
+
HumanDecisionPacket,
|
|
72
|
+
ReceiptStatus,
|
|
73
|
+
VersionControlSafety,
|
|
74
|
+
WorkflowPhaseOutcome,
|
|
75
|
+
WorkflowReceiptPayload,
|
|
76
|
+
assert_diagnostic_context_evidence_only,
|
|
77
|
+
diagnostic_context_evidence_only,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
FIX_WIKI_SCHEMA = "medical-notes-workbench.fix-wiki-fsm-result.v1"
|
|
81
|
+
FIX_WIKI_RECEIPT_SCHEMA = "medical-notes-workbench.fix-wiki-receipt.v3"
|
|
82
|
+
MEDNOTES_AGENT_DIRECTIVE_SCHEMA = "medical-notes-workbench.agent-directive.v1"
|
|
83
|
+
|
|
84
|
+
FIX_WIKI_ALLOWED_ROOT_KEYS = frozenset(
|
|
85
|
+
{
|
|
86
|
+
"schema",
|
|
87
|
+
"workflow",
|
|
88
|
+
"run_id",
|
|
89
|
+
"state_machine_snapshot",
|
|
90
|
+
"progress_view_model",
|
|
91
|
+
"decision",
|
|
92
|
+
"human_decision_packet",
|
|
93
|
+
"receipt",
|
|
94
|
+
"reports",
|
|
95
|
+
"agent_directive",
|
|
96
|
+
"artifacts",
|
|
97
|
+
"version_control_safety",
|
|
98
|
+
"diagnostic_context",
|
|
99
|
+
"error_context",
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
FIX_WIKI_FORBIDDEN_ROOT_KEYS = frozenset(
|
|
103
|
+
{
|
|
104
|
+
"status",
|
|
105
|
+
"phase",
|
|
106
|
+
"blocked_reason",
|
|
107
|
+
"next_action",
|
|
108
|
+
"next_command",
|
|
109
|
+
"execution_gate",
|
|
110
|
+
"resume_after_resolution",
|
|
111
|
+
"orchestration_plan",
|
|
112
|
+
"workflow_exit_code",
|
|
113
|
+
"public_report",
|
|
114
|
+
"requested_apply",
|
|
115
|
+
"effective_apply",
|
|
116
|
+
"blocker_resolution",
|
|
117
|
+
"final_validation",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
FIX_WIKI_DIAGNOSTIC_PARALLEL_TRUTH_KEYS = frozenset(
|
|
121
|
+
{
|
|
122
|
+
"status",
|
|
123
|
+
"phase",
|
|
124
|
+
"blocked_reason",
|
|
125
|
+
"next_action",
|
|
126
|
+
"next_command",
|
|
127
|
+
"workflow_exit_code",
|
|
128
|
+
"requested_apply",
|
|
129
|
+
"effective_apply",
|
|
130
|
+
"required_inputs",
|
|
131
|
+
"human_decision_required",
|
|
132
|
+
"human_decision_kinds",
|
|
133
|
+
"primary_human_decision_kind",
|
|
134
|
+
"human_decision_packet",
|
|
135
|
+
"resume_action",
|
|
136
|
+
"action_directives",
|
|
137
|
+
"pending_effects",
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
FIX_WIKI_DIAGNOSTIC_OPERATIONAL_PLAN_KEYS = frozenset(
|
|
141
|
+
{
|
|
142
|
+
"status",
|
|
143
|
+
"phase",
|
|
144
|
+
"route",
|
|
145
|
+
"blocked_reason",
|
|
146
|
+
"next_action",
|
|
147
|
+
"next_command",
|
|
148
|
+
"agent_instruction",
|
|
149
|
+
"executable_now",
|
|
150
|
+
"current_work_item",
|
|
151
|
+
"current_batch_items",
|
|
152
|
+
"continuation_steps",
|
|
153
|
+
"parent_steps",
|
|
154
|
+
"execution_contract",
|
|
155
|
+
"runtime_execution",
|
|
156
|
+
"resume_action",
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
FIX_WIKI_RELATED_RECOVERY_COUNT_FIELDS = frozenset(
|
|
160
|
+
{
|
|
161
|
+
"fresh_record_count",
|
|
162
|
+
"partial_record_count",
|
|
163
|
+
"stale_record_count",
|
|
164
|
+
"record_count",
|
|
165
|
+
"total_note_count",
|
|
166
|
+
"remaining_count",
|
|
167
|
+
"embedded_count",
|
|
168
|
+
"reused_count",
|
|
169
|
+
"attempt_count",
|
|
170
|
+
"next_retry_after_seconds",
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
_PHASE_BY_STATE = {
|
|
175
|
+
"diagnosis.running": "diagnosis",
|
|
176
|
+
"environment.paths_missing": "environment",
|
|
177
|
+
"environment.wiki_dir_missing": "environment",
|
|
178
|
+
"environment.windows_path_or_venv_blocked": "environment",
|
|
179
|
+
"vault_guard.running": "vault_guard",
|
|
180
|
+
"vault_guard.decision_required": "vault_guard",
|
|
181
|
+
"subagent_plan_attestation.required": "subagent_plan_attestation",
|
|
182
|
+
"subagent_plan_attestation.invalid": "subagent_plan_attestation",
|
|
183
|
+
"agent_tool_contract_violation": "agent_tool_contract",
|
|
184
|
+
"deterministic_repairs.running": "deterministic_repairs",
|
|
185
|
+
"deterministic_repairs.failed": "deterministic_repairs",
|
|
186
|
+
"style_rewrite.specialist_requested": "style_rewrite",
|
|
187
|
+
"style_rewrite.capacity_wait": "style_rewrite",
|
|
188
|
+
"style_rewrite.review_required": "style_rewrite",
|
|
189
|
+
"style_rewrite.apply_running": "style_rewrite",
|
|
190
|
+
"taxonomy.decision_required": "taxonomy",
|
|
191
|
+
"taxonomy.apply_running": "taxonomy",
|
|
192
|
+
"vocabulary.curator_running": "vocabulary",
|
|
193
|
+
"vocabulary.semantic_ingestion_pending": "vocabulary",
|
|
194
|
+
"vocabulary.eval_running": "vocabulary",
|
|
195
|
+
"vocabulary.eval_needs_review": "vocabulary",
|
|
196
|
+
"vocabulary.apply_running": "vocabulary",
|
|
197
|
+
"vocabulary.sqlite_integrity_failed": "vocabulary",
|
|
198
|
+
"atomicity_split.running": "atomicity_split",
|
|
199
|
+
"atomicity_split.review_required": "atomicity_split",
|
|
200
|
+
"related_notes.export_running": "related_notes",
|
|
201
|
+
"related_notes.quota_wait": "related_notes_recovery",
|
|
202
|
+
"related_notes.obsidian_not_ready": "related_notes",
|
|
203
|
+
"related_notes.blocked": "related_notes",
|
|
204
|
+
"link.run_requested": "link",
|
|
205
|
+
"link.graph_blocked": "link",
|
|
206
|
+
"link.graph_review_required": "link",
|
|
207
|
+
"link.linker_blocked": "link",
|
|
208
|
+
"merge.running": "merge",
|
|
209
|
+
"merge.review_required": "merge",
|
|
210
|
+
"contract_gap.missing_next_action": "contract_gap",
|
|
211
|
+
"contract_gap.missing_error_context": "contract_gap",
|
|
212
|
+
"rollback.running": "rollback",
|
|
213
|
+
"rollback.performed": "rollback",
|
|
214
|
+
"rollback.failed": "rollback",
|
|
215
|
+
"final_validation.running": "final_validation",
|
|
216
|
+
"final_validation.failed": "final_validation",
|
|
217
|
+
"preview.ready": "preview",
|
|
218
|
+
"completed": "final_validation",
|
|
219
|
+
"completed_with_warnings": "final_validation",
|
|
220
|
+
"waiting_agent": "style_rewrite",
|
|
221
|
+
"waiting_external": "external_wait",
|
|
222
|
+
"waiting_for_external_quota": "related_notes_recovery",
|
|
223
|
+
"waiting_human": "human_decision",
|
|
224
|
+
"blocked": "blocked",
|
|
225
|
+
"failed": "failure",
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class FixWikiRuntimeFacts(ContractModel):
|
|
231
|
+
"""Typed adapter input produced by health/runtime before entering the FSM.
|
|
232
|
+
|
|
233
|
+
This model is deliberately not the public FSM facts. It is the current
|
|
234
|
+
runtime boundary that turns validated health facts into one canonical
|
|
235
|
+
`FixWikiMachine` event, so diagnostic-only fields cannot fabricate a
|
|
236
|
+
public state after this point.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
run_id: str = Field(min_length=1)
|
|
240
|
+
requested_apply: bool = Field(strict=True)
|
|
241
|
+
effective_apply: bool = Field(strict=True)
|
|
242
|
+
total_changed_count: int = Field(default=0, ge=0, strict=True)
|
|
243
|
+
vault_changed_file_count: int = Field(default=0, ge=0, strict=True)
|
|
244
|
+
written_count: int = Field(default=0, ge=0, strict=True)
|
|
245
|
+
warning_count: int = Field(default=0, ge=0, strict=True)
|
|
246
|
+
requires_llm_rewrite_count: int = Field(default=0, ge=0, strict=True)
|
|
247
|
+
final_validation: JsonObject = Field(default_factory=dict)
|
|
248
|
+
version_control_safety: VersionControlSafety
|
|
249
|
+
artifacts: JsonObject = Field(default_factory=dict)
|
|
250
|
+
related_notes_blocked: bool = Field(default=False, strict=True)
|
|
251
|
+
related_notes_recovery_state: RelatedNotesRecoveryState = Field(default_factory=RelatedNotesRecoveryState)
|
|
252
|
+
vocabulary_semantic_ingestion_pending: bool = Field(default=False, strict=True)
|
|
253
|
+
vocabulary_eval_needs_review: bool = Field(default=False, strict=True)
|
|
254
|
+
atomicity_split_required: bool = Field(default=False, strict=True)
|
|
255
|
+
merge_review_required: bool = Field(default=False, strict=True)
|
|
256
|
+
human_decision_required: bool = Field(default=False, strict=True)
|
|
257
|
+
decision: WorkflowDecision | None = None
|
|
258
|
+
human_decision_packet: HumanDecisionPacket | None = None
|
|
259
|
+
changed_files: list[str] = Field(default_factory=list)
|
|
260
|
+
graph_error_count: int = Field(default=0, ge=0, strict=True)
|
|
261
|
+
graph_blocker_count: int = Field(default=0, ge=0, strict=True)
|
|
262
|
+
graph_review_required: bool = Field(default=False, strict=True)
|
|
263
|
+
linker_blocked: bool = Field(default=False, strict=True)
|
|
264
|
+
linker_apply_attempted: bool = Field(default=False, strict=True)
|
|
265
|
+
taxonomy_action_required: bool = Field(default=False, strict=True)
|
|
266
|
+
failed: bool = Field(default=False, strict=True)
|
|
267
|
+
failed_reason_code: str = ""
|
|
268
|
+
vault_guard_required: bool = Field(default=False, strict=True)
|
|
269
|
+
environment_windows_path_or_venv_blocked: bool = Field(default=False, strict=True)
|
|
270
|
+
next_action: str = ""
|
|
271
|
+
required_inputs: list[str] = Field(default_factory=list)
|
|
272
|
+
resume_action: str = ""
|
|
273
|
+
pending_effects: list[WorkflowEffect] = Field(default_factory=list)
|
|
274
|
+
external_wait_reason_code: str = ""
|
|
275
|
+
external_wait_resume_action: str = ""
|
|
276
|
+
external_wait_payload: JsonObject = Field(default_factory=dict)
|
|
277
|
+
diagnostic_context: JsonObject = Field(default_factory=dict)
|
|
278
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
279
|
+
|
|
280
|
+
@model_validator(mode="before")
|
|
281
|
+
@classmethod
|
|
282
|
+
def _reject_noncanonical_pending_effects_at_boundary(cls, value: object) -> object:
|
|
283
|
+
"""Reject noncanonical effect shims before projection logic can inspect them.
|
|
284
|
+
|
|
285
|
+
`pending_effects` is an FSM-owned contract. The projector may validate a
|
|
286
|
+
canonical `WorkflowEffect`, but it must not fill `phase`,
|
|
287
|
+
`origin_state`, `workflow`, `run_id`, or `model_policy` on behalf of a
|
|
288
|
+
noncanonical producer because that would make success depend on adapter
|
|
289
|
+
glue rather than the StateChart transition that emitted the effect.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
if not isinstance(value, dict):
|
|
293
|
+
return value
|
|
294
|
+
if "pending_effects" not in value:
|
|
295
|
+
return value
|
|
296
|
+
for raw_effect in value["pending_effects"]:
|
|
297
|
+
if isinstance(raw_effect, WorkflowEffect):
|
|
298
|
+
data = raw_effect.to_payload()
|
|
299
|
+
else:
|
|
300
|
+
data = dict(JsonObjectAdapter.validate_python(raw_effect))
|
|
301
|
+
if "phase" in data:
|
|
302
|
+
raise ValueError("pending effect must use origin_state, not phase")
|
|
303
|
+
kind = str(data["kind"]) if "kind" in data else ""
|
|
304
|
+
origin_state = str(data["origin_state"]).strip() if "origin_state" in data else ""
|
|
305
|
+
specialist_origin = FixWikiState.STYLE_REWRITE_SPECIALIST_REQUESTED.value
|
|
306
|
+
if not origin_state:
|
|
307
|
+
raise ValueError("pending effect origin_state is required")
|
|
308
|
+
if kind == WorkflowEffectKind.CALL_SPECIALIST_MODEL.value and origin_state != specialist_origin:
|
|
309
|
+
raise ValueError("pending effect origin_state must match style_rewrite.specialist_requested")
|
|
310
|
+
if (
|
|
311
|
+
"kind" in data
|
|
312
|
+
and data["kind"] == WorkflowEffectKind.CALL_SPECIALIST_MODEL.value
|
|
313
|
+
and ("model_policy" not in data or not data["model_policy"])
|
|
314
|
+
):
|
|
315
|
+
raise ValueError("call_specialist_model pending effect requires model_policy")
|
|
316
|
+
return value
|
|
317
|
+
|
|
318
|
+
@field_validator("related_notes_recovery_state", mode="before")
|
|
319
|
+
@classmethod
|
|
320
|
+
def _coerce_related_notes_recovery_state(cls, value: object) -> RelatedNotesRecoveryState:
|
|
321
|
+
if isinstance(value, RelatedNotesRecoveryState):
|
|
322
|
+
return value
|
|
323
|
+
if isinstance(value, dict):
|
|
324
|
+
for field_name in FIX_WIKI_RELATED_RECOVERY_COUNT_FIELDS:
|
|
325
|
+
if field_name not in value:
|
|
326
|
+
continue
|
|
327
|
+
raw_value = value[field_name]
|
|
328
|
+
if type(raw_value) is not int:
|
|
329
|
+
raise ValueError(f"invalid numeric recovery_state value: {raw_value}")
|
|
330
|
+
return RelatedNotesRecoveryState.from_payload(value)
|
|
331
|
+
|
|
332
|
+
@model_validator(mode="after")
|
|
333
|
+
def _human_wait_requires_closed_packet(self) -> FixWikiRuntimeFacts:
|
|
334
|
+
if self.human_decision_required:
|
|
335
|
+
if self.decision is None:
|
|
336
|
+
raise ValueError("human_decision_required requires decision")
|
|
337
|
+
if self.human_decision_packet is None:
|
|
338
|
+
raise ValueError("human_decision_required requires human_decision_packet")
|
|
339
|
+
if self.decision.kind != "ask_human":
|
|
340
|
+
raise ValueError("human_decision_required requires ask_human decision")
|
|
341
|
+
if self.human_decision_packet.to_payload() != self.decision.to_human_decision_packet():
|
|
342
|
+
raise ValueError("human_decision_packet must match decision")
|
|
343
|
+
if self._would_complete_apply() and not _final_validation_has_evidence(self.final_validation):
|
|
344
|
+
raise ValueError("final_validation evidence required before completed fix-wiki apply")
|
|
345
|
+
_assert_final_validation_graph_matches_counters(
|
|
346
|
+
self.final_validation,
|
|
347
|
+
graph_error_count=self.graph_error_count,
|
|
348
|
+
graph_blocker_count=self.graph_blocker_count,
|
|
349
|
+
)
|
|
350
|
+
return self
|
|
351
|
+
|
|
352
|
+
def _would_complete_apply(self) -> bool:
|
|
353
|
+
"""Return true only for the clean apply path that would enter a terminal success state."""
|
|
354
|
+
|
|
355
|
+
return self.effective_apply and not any(
|
|
356
|
+
(
|
|
357
|
+
self.failed,
|
|
358
|
+
self.human_decision_required,
|
|
359
|
+
self.related_notes_blocked,
|
|
360
|
+
self.vocabulary_semantic_ingestion_pending,
|
|
361
|
+
self.vocabulary_eval_needs_review,
|
|
362
|
+
self.atomicity_split_required,
|
|
363
|
+
self.merge_review_required,
|
|
364
|
+
self.graph_review_required,
|
|
365
|
+
self.graph_error_count,
|
|
366
|
+
self.graph_blocker_count,
|
|
367
|
+
self.linker_blocked,
|
|
368
|
+
self.taxonomy_action_required,
|
|
369
|
+
self.requires_llm_rewrite_count,
|
|
370
|
+
self.pending_effects,
|
|
371
|
+
self.external_wait_reason_code,
|
|
372
|
+
self.related_notes_recovery_state.status,
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _final_validation_has_evidence(payload: JsonObject) -> bool:
|
|
378
|
+
"""Require concrete validation counters before apply can become success."""
|
|
379
|
+
|
|
380
|
+
graph = payload["graph"] if "graph" in payload else None
|
|
381
|
+
if not isinstance(graph, dict):
|
|
382
|
+
return False
|
|
383
|
+
for key in ("error_count", "blocker_count"):
|
|
384
|
+
if key in graph and type(graph[key]) is int:
|
|
385
|
+
return True
|
|
386
|
+
return False
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _assert_final_validation_graph_matches_counters(
|
|
390
|
+
payload: JsonObject,
|
|
391
|
+
*,
|
|
392
|
+
graph_error_count: int,
|
|
393
|
+
graph_blocker_count: int,
|
|
394
|
+
) -> None:
|
|
395
|
+
"""Keep final validation as evidence for the canonical graph counters."""
|
|
396
|
+
|
|
397
|
+
graph = payload["graph"] if "graph" in payload else None
|
|
398
|
+
if not isinstance(graph, dict):
|
|
399
|
+
return
|
|
400
|
+
expected = {
|
|
401
|
+
"error_count": graph_error_count,
|
|
402
|
+
"blocker_count": graph_blocker_count,
|
|
403
|
+
}
|
|
404
|
+
for key, canonical_value in expected.items():
|
|
405
|
+
if key not in graph:
|
|
406
|
+
continue
|
|
407
|
+
observed = graph[key]
|
|
408
|
+
if type(observed) is not int:
|
|
409
|
+
continue
|
|
410
|
+
if observed != canonical_value:
|
|
411
|
+
raise ValueError(f"final_validation graph {key} must match canonical graph counter")
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class FixWikiFsmFacts(ContractModel):
|
|
415
|
+
"""Canonical public projector input: one valid StateChart edge plus context."""
|
|
416
|
+
|
|
417
|
+
run_id: str = Field(min_length=1)
|
|
418
|
+
initial_state: FixWikiState
|
|
419
|
+
event: FixWikiBoundaryEvent
|
|
420
|
+
runtime: FixWikiRuntimeFacts
|
|
421
|
+
machine_effects: list[WorkflowEffect] = Field(default_factory=list)
|
|
422
|
+
|
|
423
|
+
@model_validator(mode="after")
|
|
424
|
+
def _event_must_match_fsm_entry(self) -> FixWikiFsmFacts:
|
|
425
|
+
if self.event.workflow != FIX_WIKI_WORKFLOW:
|
|
426
|
+
raise ValueError(f"fix-wiki event workflow must be {FIX_WIKI_WORKFLOW}")
|
|
427
|
+
if self.event.run_id != self.run_id:
|
|
428
|
+
raise ValueError("fix-wiki event run_id must match FixWikiFsmFacts.run_id")
|
|
429
|
+
if self.event.current_state != self.initial_state.value:
|
|
430
|
+
raise ValueError("fix-wiki event current_state must match initial_state")
|
|
431
|
+
if self.runtime.run_id != self.run_id:
|
|
432
|
+
raise ValueError("runtime run_id must match FixWikiFsmFacts.run_id")
|
|
433
|
+
return self
|
|
434
|
+
|
|
435
|
+
def with_runtime_updates(self, update: dict[str, object]) -> FixWikiFsmFacts:
|
|
436
|
+
"""Rebuild the canonical event after adapter/runtime facts change."""
|
|
437
|
+
|
|
438
|
+
runtime = FixWikiRuntimeFacts.model_validate({**self.runtime.model_dump(mode="python"), **update})
|
|
439
|
+
return _fix_wiki_fsm_facts_from_runtime_model(runtime)
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def requested_apply(self) -> bool:
|
|
443
|
+
return self.runtime.requested_apply
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def effective_apply(self) -> bool:
|
|
447
|
+
return self.runtime.effective_apply
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def total_changed_count(self) -> int:
|
|
451
|
+
return self.runtime.total_changed_count
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def vault_changed_file_count(self) -> int:
|
|
455
|
+
return self.runtime.vault_changed_file_count
|
|
456
|
+
|
|
457
|
+
@property
|
|
458
|
+
def written_count(self) -> int:
|
|
459
|
+
return self.runtime.written_count
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def warning_count(self) -> int:
|
|
463
|
+
return self.runtime.warning_count
|
|
464
|
+
|
|
465
|
+
@property
|
|
466
|
+
def requires_llm_rewrite_count(self) -> int:
|
|
467
|
+
return self.runtime.requires_llm_rewrite_count
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def final_validation(self) -> JsonObject:
|
|
471
|
+
return self.runtime.final_validation
|
|
472
|
+
|
|
473
|
+
@property
|
|
474
|
+
def version_control_safety(self) -> VersionControlSafety:
|
|
475
|
+
return self.runtime.version_control_safety
|
|
476
|
+
|
|
477
|
+
@property
|
|
478
|
+
def artifacts(self) -> JsonObject:
|
|
479
|
+
return self.runtime.artifacts
|
|
480
|
+
|
|
481
|
+
@property
|
|
482
|
+
def related_notes_blocked(self) -> bool:
|
|
483
|
+
return self.runtime.related_notes_blocked
|
|
484
|
+
|
|
485
|
+
@property
|
|
486
|
+
def related_notes_recovery_state(self) -> RelatedNotesRecoveryState:
|
|
487
|
+
return self.runtime.related_notes_recovery_state
|
|
488
|
+
|
|
489
|
+
@property
|
|
490
|
+
def vocabulary_semantic_ingestion_pending(self) -> bool:
|
|
491
|
+
return self.runtime.vocabulary_semantic_ingestion_pending
|
|
492
|
+
|
|
493
|
+
@property
|
|
494
|
+
def vocabulary_eval_needs_review(self) -> bool:
|
|
495
|
+
return self.runtime.vocabulary_eval_needs_review
|
|
496
|
+
|
|
497
|
+
@property
|
|
498
|
+
def atomicity_split_required(self) -> bool:
|
|
499
|
+
return self.runtime.atomicity_split_required
|
|
500
|
+
|
|
501
|
+
@property
|
|
502
|
+
def merge_review_required(self) -> bool:
|
|
503
|
+
return self.runtime.merge_review_required
|
|
504
|
+
|
|
505
|
+
@property
|
|
506
|
+
def human_decision_required(self) -> bool:
|
|
507
|
+
return self.runtime.human_decision_required
|
|
508
|
+
|
|
509
|
+
@property
|
|
510
|
+
def decision(self) -> WorkflowDecision | None:
|
|
511
|
+
return self.runtime.decision
|
|
512
|
+
|
|
513
|
+
@property
|
|
514
|
+
def human_decision_packet(self) -> HumanDecisionPacket | None:
|
|
515
|
+
return self.runtime.human_decision_packet
|
|
516
|
+
|
|
517
|
+
@property
|
|
518
|
+
def changed_files(self) -> list[str]:
|
|
519
|
+
return self.runtime.changed_files
|
|
520
|
+
|
|
521
|
+
@property
|
|
522
|
+
def graph_error_count(self) -> int:
|
|
523
|
+
return self.runtime.graph_error_count
|
|
524
|
+
|
|
525
|
+
@property
|
|
526
|
+
def graph_blocker_count(self) -> int:
|
|
527
|
+
return self.runtime.graph_blocker_count
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
def graph_review_required(self) -> bool:
|
|
531
|
+
return self.runtime.graph_review_required
|
|
532
|
+
|
|
533
|
+
@property
|
|
534
|
+
def linker_blocked(self) -> bool:
|
|
535
|
+
return self.runtime.linker_blocked
|
|
536
|
+
|
|
537
|
+
@property
|
|
538
|
+
def linker_apply_attempted(self) -> bool:
|
|
539
|
+
return self.runtime.linker_apply_attempted
|
|
540
|
+
|
|
541
|
+
@property
|
|
542
|
+
def taxonomy_action_required(self) -> bool:
|
|
543
|
+
return self.runtime.taxonomy_action_required
|
|
544
|
+
|
|
545
|
+
@property
|
|
546
|
+
def failed(self) -> bool:
|
|
547
|
+
return self.runtime.failed
|
|
548
|
+
|
|
549
|
+
@property
|
|
550
|
+
def failed_reason_code(self) -> str:
|
|
551
|
+
return self.runtime.failed_reason_code
|
|
552
|
+
|
|
553
|
+
@property
|
|
554
|
+
def next_action(self) -> str:
|
|
555
|
+
return self.runtime.next_action
|
|
556
|
+
|
|
557
|
+
@property
|
|
558
|
+
def required_inputs(self) -> list[str]:
|
|
559
|
+
return self.runtime.required_inputs
|
|
560
|
+
|
|
561
|
+
@property
|
|
562
|
+
def resume_action(self) -> str:
|
|
563
|
+
return self.runtime.resume_action
|
|
564
|
+
|
|
565
|
+
@property
|
|
566
|
+
def pending_effects(self) -> list[WorkflowEffect]:
|
|
567
|
+
return self.runtime.pending_effects
|
|
568
|
+
|
|
569
|
+
@property
|
|
570
|
+
def external_wait_reason_code(self) -> str:
|
|
571
|
+
return self.runtime.external_wait_reason_code
|
|
572
|
+
|
|
573
|
+
@property
|
|
574
|
+
def external_wait_resume_action(self) -> str:
|
|
575
|
+
return self.runtime.external_wait_resume_action
|
|
576
|
+
|
|
577
|
+
@property
|
|
578
|
+
def external_wait_payload(self) -> JsonObject:
|
|
579
|
+
return self.runtime.external_wait_payload
|
|
580
|
+
|
|
581
|
+
@property
|
|
582
|
+
def diagnostic_context(self) -> JsonObject:
|
|
583
|
+
return self.runtime.diagnostic_context
|
|
584
|
+
|
|
585
|
+
@property
|
|
586
|
+
def error_context(self) -> JsonObject:
|
|
587
|
+
return self.runtime.error_context
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
class _FixWikiStateView(ContractModel):
|
|
591
|
+
"""Display/effect view derived from FixWikiMachine state and transition."""
|
|
592
|
+
|
|
593
|
+
reason: FixWikiReason
|
|
594
|
+
state: FixWikiState
|
|
595
|
+
category: WorkflowStateCategory
|
|
596
|
+
status: WorkflowProgressStatus
|
|
597
|
+
event_type: WorkflowProgressEventType
|
|
598
|
+
decision: WorkflowDecision | None = None
|
|
599
|
+
next_action: str = ""
|
|
600
|
+
resume_action: str = ""
|
|
601
|
+
resume_supported: bool = False
|
|
602
|
+
can_continue_now: bool = False
|
|
603
|
+
message: str
|
|
604
|
+
trigger: str
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
class _FixWikiPayloadProgressView(ContractModel):
|
|
608
|
+
status: StrictStr
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
class _FixWikiPayloadSnapshot(ContractModel):
|
|
612
|
+
current_category: StrictStr
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class _FixWikiPayloadReceipt(ContractModel):
|
|
616
|
+
status: StrictStr
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
class _FixWikiExternalWaitEffectFields(ContractModel):
|
|
620
|
+
origin_state: StrictStr = ""
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
class _FixWikiExternalWaitProgressFields(ContractModel):
|
|
624
|
+
model_config = ConfigDict(extra="ignore")
|
|
625
|
+
|
|
626
|
+
status: StrictStr = ""
|
|
627
|
+
state: StrictStr = ""
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
class _FixWikiExternalWaitSnapshotFields(ContractModel):
|
|
631
|
+
model_config = ConfigDict(extra="ignore")
|
|
632
|
+
|
|
633
|
+
current_state: StrictStr = ""
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
class _FixWikiExternalWaitDiagnosticFields(ContractModel):
|
|
637
|
+
model_config = ConfigDict(extra="ignore")
|
|
638
|
+
|
|
639
|
+
related_notes_recovery_state: JsonObject = Field(default_factory=dict)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
class _FixWikiExternalWaitPayloadFields(ContractModel):
|
|
643
|
+
"""Typed lens for child FSM payloads returned by waiting-external effects."""
|
|
644
|
+
|
|
645
|
+
model_config = ConfigDict(extra="ignore")
|
|
646
|
+
|
|
647
|
+
progress_view_model: _FixWikiExternalWaitProgressFields = Field(default_factory=_FixWikiExternalWaitProgressFields)
|
|
648
|
+
state_machine_snapshot: _FixWikiExternalWaitSnapshotFields = Field(default_factory=_FixWikiExternalWaitSnapshotFields)
|
|
649
|
+
diagnostic_context: _FixWikiExternalWaitDiagnosticFields = Field(default_factory=_FixWikiExternalWaitDiagnosticFields)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
class _FixWikiExistingErrorContextFields(ContractModel):
|
|
653
|
+
model_config = ConfigDict(extra="ignore")
|
|
654
|
+
|
|
655
|
+
blocked_reason: StrictStr = ""
|
|
656
|
+
root_cause: StrictStr = ""
|
|
657
|
+
next_action: StrictStr = ""
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
class _FixWikiArtifactPathFields(ContractModel):
|
|
661
|
+
"""Typed artifact lens used when a FSM leaf needs a concrete recovery file."""
|
|
662
|
+
|
|
663
|
+
model_config = ConfigDict(extra="ignore")
|
|
664
|
+
|
|
665
|
+
atomicity_split_plan_path: StrictStr = ""
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class _FixWikiErrorRequiredInputs(ContractModel):
|
|
669
|
+
"""Typed lens for the only error-context field that can drive UX inputs."""
|
|
670
|
+
|
|
671
|
+
model_config = ConfigDict(extra="ignore")
|
|
672
|
+
|
|
673
|
+
required_inputs: list[StrictStr] = Field(default_factory=list)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class _FixWikiPendingEffectKind(ContractModel):
|
|
677
|
+
model_config = ConfigDict(extra="ignore")
|
|
678
|
+
|
|
679
|
+
kind: StrictStr = ""
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
class _FixWikiVocabularyBootstrapDiagnostic(ContractModel):
|
|
683
|
+
model_config = ConfigDict(extra="ignore")
|
|
684
|
+
|
|
685
|
+
trigger: StrictStr = ""
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
class _FixWikiPayloadFields(ContractModel):
|
|
689
|
+
workflow: Literal["/mednotes:fix-wiki"]
|
|
690
|
+
progress_view_model: _FixWikiPayloadProgressView
|
|
691
|
+
state_machine_snapshot: _FixWikiPayloadSnapshot
|
|
692
|
+
receipt: _FixWikiPayloadReceipt
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
class FixWikiFsmResult(ContractModel):
|
|
696
|
+
schema_id: Literal["medical-notes-workbench.fix-wiki-fsm-result.v1"] = Field(
|
|
697
|
+
default=FIX_WIKI_SCHEMA,
|
|
698
|
+
alias="schema",
|
|
699
|
+
)
|
|
700
|
+
workflow: Literal["/mednotes:fix-wiki"] = FIX_WIKI_WORKFLOW
|
|
701
|
+
run_id: str = Field(min_length=1)
|
|
702
|
+
progress_state: SkipJsonSchema[WorkflowProgressState]
|
|
703
|
+
progress_view_model: WorkflowProgressViewModel
|
|
704
|
+
state_machine_snapshot: WorkflowStateMachineSnapshot
|
|
705
|
+
decision: WorkflowDecision | None = None
|
|
706
|
+
human_decision_packet: HumanDecisionPacket | None = None
|
|
707
|
+
receipt: WorkflowReceiptPayload
|
|
708
|
+
reports: WorkflowReports
|
|
709
|
+
agent_directive: JsonObject
|
|
710
|
+
artifacts: JsonObject = Field(default_factory=dict)
|
|
711
|
+
version_control_safety: VersionControlSafety
|
|
712
|
+
diagnostic_context: JsonObject = Field(default_factory=dict)
|
|
713
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
714
|
+
|
|
715
|
+
@model_validator(mode="before")
|
|
716
|
+
@classmethod
|
|
717
|
+
def _hydrate_progress_state_from_public_payload(cls, value: object) -> object:
|
|
718
|
+
"""Accept public payloads where progress_state is intentionally hidden."""
|
|
719
|
+
|
|
720
|
+
if not isinstance(value, dict) or "progress_state" in value or "progress_view_model" not in value:
|
|
721
|
+
return value
|
|
722
|
+
hydrated = dict(value)
|
|
723
|
+
progress_view = WorkflowProgressViewModel.model_validate(value["progress_view_model"])
|
|
724
|
+
hydrated["progress_state"] = progress_state_from_view_model(progress_view).to_payload()
|
|
725
|
+
return hydrated
|
|
726
|
+
|
|
727
|
+
@model_validator(mode="after")
|
|
728
|
+
def _progress_view_model_matches_state(self) -> FixWikiFsmResult:
|
|
729
|
+
expected = build_progress_view_model(self.progress_state).to_payload()
|
|
730
|
+
if self.progress_view_model.to_payload() != expected:
|
|
731
|
+
raise ValueError("progress_view_model must match progress_state")
|
|
732
|
+
return self
|
|
733
|
+
|
|
734
|
+
def to_payload(self) -> JsonObject:
|
|
735
|
+
payload: JsonObject = {
|
|
736
|
+
"schema": self.schema_id,
|
|
737
|
+
"workflow": self.workflow,
|
|
738
|
+
"run_id": self.run_id,
|
|
739
|
+
"state_machine_snapshot": self.state_machine_snapshot.to_payload(),
|
|
740
|
+
"progress_view_model": self.progress_view_model.to_payload(),
|
|
741
|
+
"decision": self.decision.to_payload() if self.decision is not None else None,
|
|
742
|
+
"human_decision_packet": self.human_decision_packet.to_payload()
|
|
743
|
+
if self.human_decision_packet is not None
|
|
744
|
+
else None,
|
|
745
|
+
"receipt": self.receipt.to_payload(),
|
|
746
|
+
"reports": self.reports.to_payload(),
|
|
747
|
+
"agent_directive": dict(self.agent_directive),
|
|
748
|
+
"artifacts": dict(self.artifacts),
|
|
749
|
+
"version_control_safety": self.version_control_safety.to_payload(),
|
|
750
|
+
"error_context": dict(self.error_context),
|
|
751
|
+
}
|
|
752
|
+
if self.diagnostic_context:
|
|
753
|
+
payload["diagnostic_context"] = dict(self.diagnostic_context)
|
|
754
|
+
payload = _payload_with_primary_objective_summary(payload)
|
|
755
|
+
payload = JsonObjectAdapter.validate_python(payload)
|
|
756
|
+
assert_fix_wiki_fsm_payload(payload)
|
|
757
|
+
return payload
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def fix_wiki_fsm_facts_from_runtime(**runtime_fields: object) -> FixWikiFsmFacts:
|
|
761
|
+
"""Normalize existing fix-wiki runtime facts into one StateChart event."""
|
|
762
|
+
|
|
763
|
+
runtime = FixWikiRuntimeFacts.model_validate(runtime_fields)
|
|
764
|
+
return _fix_wiki_fsm_facts_from_runtime_model(runtime)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _fix_wiki_fsm_facts_from_runtime_model(runtime: FixWikiRuntimeFacts) -> FixWikiFsmFacts:
|
|
768
|
+
initial_state = FixWikiState.DIAGNOSIS_RUNNING
|
|
769
|
+
event = _runtime_observation_event_from_facts(runtime)
|
|
770
|
+
model = _fix_wiki_model_after_event(initial_state, event)
|
|
771
|
+
machine_effects = list(model.last_transition.effects) if model.last_transition is not None else []
|
|
772
|
+
return FixWikiFsmFacts(
|
|
773
|
+
run_id=runtime.run_id,
|
|
774
|
+
initial_state=initial_state,
|
|
775
|
+
event=event,
|
|
776
|
+
runtime=runtime,
|
|
777
|
+
machine_effects=machine_effects,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def build_fix_wiki_fsm_result(facts: FixWikiFsmFacts) -> FixWikiFsmResult:
|
|
782
|
+
model = _fix_wiki_model_after_event(facts.initial_state, facts.event)
|
|
783
|
+
state_view = _state_view_from_model(facts, model)
|
|
784
|
+
progress_state = _progress_state(facts, state_view)
|
|
785
|
+
progress_view_model = build_progress_view_model(progress_state)
|
|
786
|
+
snapshot = _snapshot_from_model(model, state_view, progress_state)
|
|
787
|
+
human_decision_packet = facts.human_decision_packet or _projection_human_decision_packet(state_view)
|
|
788
|
+
receipt = _receipt(facts, state_view, progress_state, snapshot, human_decision_packet=human_decision_packet)
|
|
789
|
+
reports_model = _reports(facts, state_view)
|
|
790
|
+
diagnostic_context = _diagnostic_context(
|
|
791
|
+
facts,
|
|
792
|
+
state_view,
|
|
793
|
+
)
|
|
794
|
+
agent_directive = _agent_directive(
|
|
795
|
+
facts,
|
|
796
|
+
state_view,
|
|
797
|
+
progress_view_model=progress_view_model,
|
|
798
|
+
user_visible_summary=_public_report_summary_text(reports_model.public_report),
|
|
799
|
+
)
|
|
800
|
+
diagnostic_context = _problem_diagnostic_context(diagnostic_context, state_view)
|
|
801
|
+
reports_model = _reports_with_primary_objective_summary(
|
|
802
|
+
reports_model,
|
|
803
|
+
run_id=facts.run_id,
|
|
804
|
+
progress_view_model=progress_view_model,
|
|
805
|
+
receipt=receipt,
|
|
806
|
+
diagnostic_context=diagnostic_context,
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
return FixWikiFsmResult(
|
|
810
|
+
run_id=facts.run_id,
|
|
811
|
+
progress_state=progress_state,
|
|
812
|
+
progress_view_model=progress_view_model,
|
|
813
|
+
state_machine_snapshot=snapshot,
|
|
814
|
+
decision=state_view.decision,
|
|
815
|
+
human_decision_packet=human_decision_packet,
|
|
816
|
+
receipt=receipt,
|
|
817
|
+
reports=reports_model,
|
|
818
|
+
agent_directive=agent_directive,
|
|
819
|
+
artifacts=facts.artifacts,
|
|
820
|
+
version_control_safety=facts.version_control_safety,
|
|
821
|
+
diagnostic_context=diagnostic_context,
|
|
822
|
+
error_context=_error_context(facts, state_view),
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _runtime_observation_event_from_facts(facts: FixWikiRuntimeFacts) -> RuntimeObservedEvent:
|
|
827
|
+
"""Build the only runtime bridge event; the StateChart owns leaf selection."""
|
|
828
|
+
|
|
829
|
+
return RuntimeObservedEvent(
|
|
830
|
+
run_id=facts.run_id,
|
|
831
|
+
current_state=FixWikiState.DIAGNOSIS_RUNNING.value,
|
|
832
|
+
observation=FixWikiRuntimeObservation(
|
|
833
|
+
failed=facts.failed,
|
|
834
|
+
failed_reason_code=facts.failed_reason_code,
|
|
835
|
+
vault_guard_required=facts.vault_guard_required,
|
|
836
|
+
environment_windows_path_or_venv_blocked=facts.environment_windows_path_or_venv_blocked,
|
|
837
|
+
next_action=facts.next_action,
|
|
838
|
+
human_decision_required=facts.human_decision_required,
|
|
839
|
+
external_wait_reason_code=facts.external_wait_reason_code,
|
|
840
|
+
related_notes_waiting_external=_related_notes_waiting_external(facts),
|
|
841
|
+
vocabulary_semantic_ingestion_pending=facts.vocabulary_semantic_ingestion_pending,
|
|
842
|
+
vocabulary_eval_needs_review=facts.vocabulary_eval_needs_review,
|
|
843
|
+
atomicity_split_required=facts.atomicity_split_required,
|
|
844
|
+
merge_review_required=facts.merge_review_required,
|
|
845
|
+
graph_review_required=facts.graph_review_required,
|
|
846
|
+
graph_blocker_count=facts.graph_blocker_count,
|
|
847
|
+
graph_error_count=facts.graph_error_count,
|
|
848
|
+
related_notes_blocked=facts.related_notes_blocked,
|
|
849
|
+
linker_blocked=facts.linker_blocked,
|
|
850
|
+
taxonomy_action_required=facts.taxonomy_action_required,
|
|
851
|
+
specialist_model_waiting_agent=_specialist_model_waiting_agent(facts),
|
|
852
|
+
requires_llm_rewrite_count=facts.requires_llm_rewrite_count,
|
|
853
|
+
effective_apply=facts.effective_apply,
|
|
854
|
+
warning_count=facts.warning_count,
|
|
855
|
+
style_rewrite_effect=_style_rewrite_effect_input_from_runtime(facts),
|
|
856
|
+
link_subworkflow_required=_link_subworkflow_required(facts),
|
|
857
|
+
link_effect=_link_effect_input_from_runtime(facts),
|
|
858
|
+
related_notes_recovery_state=RelatedNotesRecoveryStateEffectPayload.model_validate(
|
|
859
|
+
facts.related_notes_recovery_state.to_payload()
|
|
860
|
+
),
|
|
861
|
+
),
|
|
862
|
+
audit_evidence=_runtime_audit_evidence(facts, "runtime_observed"),
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _runtime_audit_evidence(facts: FixWikiRuntimeFacts, reason: str) -> JsonObject:
|
|
867
|
+
return JsonObjectAdapter.validate_python(
|
|
868
|
+
{
|
|
869
|
+
"runtime_reason": reason,
|
|
870
|
+
"requested_apply": facts.requested_apply,
|
|
871
|
+
"effective_apply": facts.effective_apply,
|
|
872
|
+
"counts": {
|
|
873
|
+
"total_changed_count": facts.total_changed_count,
|
|
874
|
+
"vault_changed_file_count": facts.vault_changed_file_count,
|
|
875
|
+
"written_count": facts.written_count,
|
|
876
|
+
"warning_count": facts.warning_count,
|
|
877
|
+
"requires_llm_rewrite_count": facts.requires_llm_rewrite_count,
|
|
878
|
+
"graph_error_count": facts.graph_error_count,
|
|
879
|
+
"graph_blocker_count": facts.graph_blocker_count,
|
|
880
|
+
},
|
|
881
|
+
}
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _style_rewrite_effect_input_from_runtime(facts: FixWikiRuntimeFacts) -> WorkflowEffect | None:
|
|
886
|
+
"""Pass typed batch evidence into the StateChart without making it public truth."""
|
|
887
|
+
|
|
888
|
+
if not _specialist_model_waiting_agent(facts):
|
|
889
|
+
return None
|
|
890
|
+
for effect in facts.pending_effects:
|
|
891
|
+
if effect.kind == WorkflowEffectKind.CALL_SPECIALIST_MODEL:
|
|
892
|
+
return effect
|
|
893
|
+
return None
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def _link_subworkflow_required(facts: FixWikiRuntimeFacts) -> bool:
|
|
897
|
+
"""Return true when a fix-wiki mutation must be followed by `/mednotes:link`."""
|
|
898
|
+
|
|
899
|
+
if facts.linker_apply_attempted or not facts.effective_apply:
|
|
900
|
+
return False
|
|
901
|
+
if _has_unresolved_work_before_link(facts):
|
|
902
|
+
return False
|
|
903
|
+
return bool(facts.changed_files or facts.vault_changed_file_count or facts.written_count)
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def _has_unresolved_work_before_link(facts: FixWikiRuntimeFacts) -> bool:
|
|
907
|
+
"""Keep link execution behind higher-priority blockers and human choices."""
|
|
908
|
+
|
|
909
|
+
return bool(
|
|
910
|
+
facts.failed
|
|
911
|
+
or facts.human_decision_required
|
|
912
|
+
or facts.decision is not None
|
|
913
|
+
or facts.human_decision_packet is not None
|
|
914
|
+
or facts.external_wait_reason_code
|
|
915
|
+
or _related_notes_waiting_external(facts)
|
|
916
|
+
or facts.vocabulary_semantic_ingestion_pending
|
|
917
|
+
or facts.vocabulary_eval_needs_review
|
|
918
|
+
or facts.atomicity_split_required
|
|
919
|
+
or facts.merge_review_required
|
|
920
|
+
or facts.graph_review_required
|
|
921
|
+
or facts.graph_blocker_count
|
|
922
|
+
or facts.graph_error_count
|
|
923
|
+
or facts.related_notes_blocked
|
|
924
|
+
or facts.linker_blocked
|
|
925
|
+
or facts.taxonomy_action_required
|
|
926
|
+
or _specialist_model_waiting_agent(facts)
|
|
927
|
+
or facts.requires_llm_rewrite_count
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def _link_effect_input_from_runtime(facts: FixWikiRuntimeFacts) -> WorkflowEffect | None:
|
|
932
|
+
"""Build the private link effect payload consumed by the StateChart action."""
|
|
933
|
+
|
|
934
|
+
if not _link_subworkflow_required(facts):
|
|
935
|
+
return None
|
|
936
|
+
link_artifacts = facts.artifacts
|
|
937
|
+
diagnosis_path = _artifact_text_field(link_artifacts, "linker_diagnosis_path")
|
|
938
|
+
receipt_path = _artifact_text_field(link_artifacts, "linker_receipt_path") or _link_receipt_path(diagnosis_path)
|
|
939
|
+
trigger_context_path = _artifact_text_field(link_artifacts, "link_trigger_context_path")
|
|
940
|
+
return WorkflowEffect(
|
|
941
|
+
workflow=FIX_WIKI_WORKFLOW,
|
|
942
|
+
run_id=facts.run_id,
|
|
943
|
+
effect_id="fix-wiki-link-run",
|
|
944
|
+
origin_state=FixWikiState.LINK_RUN_REQUESTED.value,
|
|
945
|
+
kind=WorkflowEffectKind.RUN_SUBWORKFLOW,
|
|
946
|
+
target="/mednotes:link",
|
|
947
|
+
payload=LinkWorkflowRunEffectPayload(
|
|
948
|
+
kind="link_run",
|
|
949
|
+
diagnose=False,
|
|
950
|
+
apply=True,
|
|
951
|
+
diagnosis_path=diagnosis_path,
|
|
952
|
+
receipt_path=receipt_path,
|
|
953
|
+
trigger_context_path=trigger_context_path,
|
|
954
|
+
no_related_notes=False,
|
|
955
|
+
version_control_safety=facts.version_control_safety,
|
|
956
|
+
).to_payload(),
|
|
957
|
+
mutates_resources=True,
|
|
958
|
+
rollback_declared=True,
|
|
959
|
+
requires_receipt=False,
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _artifact_text_field(artifacts: JsonObject, key: str) -> str:
|
|
964
|
+
if key not in artifacts:
|
|
965
|
+
return ""
|
|
966
|
+
value = artifacts[key]
|
|
967
|
+
return value if isinstance(value, str) else ""
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def _link_receipt_path(diagnosis_path: str) -> str:
|
|
971
|
+
if not diagnosis_path.strip():
|
|
972
|
+
return ""
|
|
973
|
+
return str(Path(diagnosis_path).with_name("link-run-receipt.json"))
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _fix_wiki_model_after_event(initial_state: FixWikiState, event: FixWikiBoundaryEvent) -> WorkflowModel:
|
|
977
|
+
event = FixWikiBoundaryEventAdapter.validate_python(event.to_payload())
|
|
978
|
+
model = WorkflowModel.start(
|
|
979
|
+
workflow=FIX_WIKI_WORKFLOW,
|
|
980
|
+
run_id=event.run_id,
|
|
981
|
+
initial_state=initial_state.value,
|
|
982
|
+
)
|
|
983
|
+
send_workflow_event(
|
|
984
|
+
FixWikiMachine(model=model, state_field=WorkflowModel.STATECHART_STATE_FIELD),
|
|
985
|
+
event,
|
|
986
|
+
)
|
|
987
|
+
return model
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _state_view_from_model(facts: FixWikiFsmFacts, model: WorkflowModel) -> _FixWikiStateView:
|
|
991
|
+
"""Derive public presentation from the canonical StateChart result."""
|
|
992
|
+
|
|
993
|
+
state = FixWikiState(model.state)
|
|
994
|
+
category = category_for_state(state)
|
|
995
|
+
status = WorkflowProgressStatus(category.value)
|
|
996
|
+
reason = _state_reason_from_model(model, state)
|
|
997
|
+
trigger = model.last_transition.trigger if model.last_transition is not None else reason.value
|
|
998
|
+
if category == WorkflowStateCategory.WAITING_HUMAN:
|
|
999
|
+
has_runtime_decision = facts.decision is not None
|
|
1000
|
+
decision = facts.decision or (model.last_transition.decision if model.last_transition is not None else None)
|
|
1001
|
+
if decision is None:
|
|
1002
|
+
raise ValueError("waiting_human state requires decision")
|
|
1003
|
+
if has_runtime_decision and decision.reason_code != reason.value:
|
|
1004
|
+
decision = _decision_with_leaf_reason(decision, reason_code=reason.value, phase=state.value)
|
|
1005
|
+
next_action = _default_next_action(facts, reason) if not has_runtime_decision else ""
|
|
1006
|
+
if not has_runtime_decision and next_action and next_action != decision.next_action:
|
|
1007
|
+
decision = _decision_with_recovery_action(decision, next_action=next_action)
|
|
1008
|
+
return _state_view(
|
|
1009
|
+
reason=reason,
|
|
1010
|
+
state=state,
|
|
1011
|
+
category=category,
|
|
1012
|
+
status=status,
|
|
1013
|
+
event_type=WorkflowProgressEventType.DECISION_EMITTED,
|
|
1014
|
+
decision=decision,
|
|
1015
|
+
next_action=decision.next_action,
|
|
1016
|
+
resume_action=decision.resume_action,
|
|
1017
|
+
resume_supported=bool(decision.resume_action),
|
|
1018
|
+
can_continue_now=False,
|
|
1019
|
+
message="Fix-wiki aguardando decisao humana antes de continuar.",
|
|
1020
|
+
trigger=decision.reason_code or trigger,
|
|
1021
|
+
)
|
|
1022
|
+
match reason:
|
|
1023
|
+
case FixWikiReason.COMPLETED:
|
|
1024
|
+
return _state_view(
|
|
1025
|
+
reason=reason,
|
|
1026
|
+
state=state,
|
|
1027
|
+
category=category,
|
|
1028
|
+
status=status,
|
|
1029
|
+
event_type=WorkflowProgressEventType.WORKFLOW_COMPLETED,
|
|
1030
|
+
message="Wiki corrigida e conferida.",
|
|
1031
|
+
trigger=trigger,
|
|
1032
|
+
)
|
|
1033
|
+
case FixWikiReason.COMPLETED_WITH_WARNINGS:
|
|
1034
|
+
return _state_view(
|
|
1035
|
+
reason=reason,
|
|
1036
|
+
state=state,
|
|
1037
|
+
category=category,
|
|
1038
|
+
status=status,
|
|
1039
|
+
event_type=WorkflowProgressEventType.WORKFLOW_COMPLETED,
|
|
1040
|
+
next_action=_default_next_action(facts, reason),
|
|
1041
|
+
message="Wiki corrigida com avisos pendentes.",
|
|
1042
|
+
trigger=trigger,
|
|
1043
|
+
)
|
|
1044
|
+
case FixWikiReason.PREVIEW_READY:
|
|
1045
|
+
return _state_view(
|
|
1046
|
+
reason=reason,
|
|
1047
|
+
state=state,
|
|
1048
|
+
category=category,
|
|
1049
|
+
status=status,
|
|
1050
|
+
event_type=WorkflowProgressEventType.VALIDATION_COMPLETED,
|
|
1051
|
+
message="Previa do fix-wiki pronta.",
|
|
1052
|
+
trigger=trigger,
|
|
1053
|
+
)
|
|
1054
|
+
case (
|
|
1055
|
+
FixWikiReason.ENVIRONMENT_PATHS_MISSING
|
|
1056
|
+
| FixWikiReason.ENVIRONMENT_WIKI_DIR_MISSING
|
|
1057
|
+
| FixWikiReason.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED
|
|
1058
|
+
):
|
|
1059
|
+
next_action = _default_next_action(facts, reason)
|
|
1060
|
+
return _blocked_state_view(
|
|
1061
|
+
facts=facts,
|
|
1062
|
+
state=state,
|
|
1063
|
+
reason=reason,
|
|
1064
|
+
phase="environment",
|
|
1065
|
+
reason_code=reason.value,
|
|
1066
|
+
public_summary="O ambiente precisa ser preparado antes de continuar o fix-wiki.",
|
|
1067
|
+
developer_summary=(
|
|
1068
|
+
"Fix-wiki entered a recoverable environment leaf and emitted the typed "
|
|
1069
|
+
"/mednotes:setup recovery effect."
|
|
1070
|
+
),
|
|
1071
|
+
message="Fix-wiki bloqueado por preparacao de ambiente.",
|
|
1072
|
+
trigger=trigger,
|
|
1073
|
+
next_action=next_action,
|
|
1074
|
+
resume_action=next_action,
|
|
1075
|
+
resume_supported=True,
|
|
1076
|
+
)
|
|
1077
|
+
case FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
|
|
1078
|
+
next_action = _default_next_action(facts, reason)
|
|
1079
|
+
return _state_view(
|
|
1080
|
+
reason=reason,
|
|
1081
|
+
state=state,
|
|
1082
|
+
category=category,
|
|
1083
|
+
status=status,
|
|
1084
|
+
event_type=WorkflowProgressEventType.EXTERNAL_WAIT_STARTED,
|
|
1085
|
+
next_action=next_action,
|
|
1086
|
+
resume_action=facts.resume_action or next_action,
|
|
1087
|
+
resume_supported=facts.related_notes_recovery_state.resume_supported,
|
|
1088
|
+
can_continue_now=False,
|
|
1089
|
+
message=_related_notes_external_wait_message(facts),
|
|
1090
|
+
trigger=trigger,
|
|
1091
|
+
)
|
|
1092
|
+
case FixWikiReason.WAITING_EXTERNAL:
|
|
1093
|
+
next_action = facts.next_action or facts.external_wait_resume_action
|
|
1094
|
+
return _state_view(
|
|
1095
|
+
reason=reason,
|
|
1096
|
+
state=state,
|
|
1097
|
+
category=category,
|
|
1098
|
+
status=status,
|
|
1099
|
+
event_type=WorkflowProgressEventType.EXTERNAL_WAIT_STARTED,
|
|
1100
|
+
next_action=next_action,
|
|
1101
|
+
resume_action=next_action,
|
|
1102
|
+
resume_supported=True,
|
|
1103
|
+
can_continue_now=False,
|
|
1104
|
+
message="Workflow aguardando condicao externa para retomar pela rota oficial.",
|
|
1105
|
+
trigger=trigger,
|
|
1106
|
+
)
|
|
1107
|
+
case FixWikiReason.STYLE_REWRITE_READY:
|
|
1108
|
+
next_action = _default_next_action(facts, reason)
|
|
1109
|
+
decision = WorkflowDecision(
|
|
1110
|
+
kind="auto_plan",
|
|
1111
|
+
phase="style_rewrite",
|
|
1112
|
+
reason_code=reason.value,
|
|
1113
|
+
public_summary="A reescrita especializada esta pronta para continuacao assistida.",
|
|
1114
|
+
developer_summary=(
|
|
1115
|
+
"Fix-wiki generated a typed call_specialist_model effect and expects the agent "
|
|
1116
|
+
"to continue with the official agent_directive effect route."
|
|
1117
|
+
),
|
|
1118
|
+
evidence=[
|
|
1119
|
+
DecisionEvidence(
|
|
1120
|
+
summary="O workflow emitiu efeito tipado para modelo especialista.",
|
|
1121
|
+
technical_code="call_specialist_model",
|
|
1122
|
+
source="fix_wiki_fsm",
|
|
1123
|
+
)
|
|
1124
|
+
],
|
|
1125
|
+
next_action=next_action,
|
|
1126
|
+
)
|
|
1127
|
+
return _state_view(
|
|
1128
|
+
reason=reason,
|
|
1129
|
+
state=state,
|
|
1130
|
+
category=category,
|
|
1131
|
+
status=status,
|
|
1132
|
+
event_type=WorkflowProgressEventType.STATE_ENTERED,
|
|
1133
|
+
decision=decision,
|
|
1134
|
+
next_action=next_action,
|
|
1135
|
+
resume_action=next_action,
|
|
1136
|
+
resume_supported=False,
|
|
1137
|
+
can_continue_now=True,
|
|
1138
|
+
message="Fix-wiki pronto para continuar com reescrita especializada.",
|
|
1139
|
+
trigger=trigger,
|
|
1140
|
+
)
|
|
1141
|
+
case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
|
|
1142
|
+
next_action = _default_next_action(facts, reason)
|
|
1143
|
+
decision = WorkflowDecision(
|
|
1144
|
+
kind="auto_plan",
|
|
1145
|
+
phase="vocabulary",
|
|
1146
|
+
reason_code=reason.value,
|
|
1147
|
+
public_summary="A curadoria semantica do vocabulario esta pronta para continuacao assistida.",
|
|
1148
|
+
developer_summary=(
|
|
1149
|
+
"Fix-wiki generated a typed run_subworkflow effect and expects the agent "
|
|
1150
|
+
"to continue vocabulary semantic ingestion through agent_directive.control.effects."
|
|
1151
|
+
),
|
|
1152
|
+
evidence=[
|
|
1153
|
+
DecisionEvidence(
|
|
1154
|
+
summary="O workflow emitiu efeito tipado para curadoria semantica do vocabulario.",
|
|
1155
|
+
technical_code=WorkflowEffectKind.RUN_SUBWORKFLOW.value,
|
|
1156
|
+
source="fix_wiki_fsm",
|
|
1157
|
+
)
|
|
1158
|
+
],
|
|
1159
|
+
next_action=next_action,
|
|
1160
|
+
)
|
|
1161
|
+
return _state_view(
|
|
1162
|
+
reason=reason,
|
|
1163
|
+
state=state,
|
|
1164
|
+
category=category,
|
|
1165
|
+
status=status,
|
|
1166
|
+
event_type=WorkflowProgressEventType.STATE_ENTERED,
|
|
1167
|
+
decision=decision,
|
|
1168
|
+
next_action=next_action,
|
|
1169
|
+
resume_action=next_action,
|
|
1170
|
+
resume_supported=False,
|
|
1171
|
+
can_continue_now=True,
|
|
1172
|
+
message="Fix-wiki pronto para continuar com curadoria semantica do vocabulario.",
|
|
1173
|
+
trigger=trigger,
|
|
1174
|
+
)
|
|
1175
|
+
case FixWikiReason.GRAPH_BLOCKED:
|
|
1176
|
+
return _blocked_state_view(
|
|
1177
|
+
facts=facts,
|
|
1178
|
+
state=state,
|
|
1179
|
+
reason=reason,
|
|
1180
|
+
phase="graph_validation",
|
|
1181
|
+
reason_code="graph_blockers",
|
|
1182
|
+
public_summary="A Wiki ainda tem bloqueios de grafo antes de concluir.",
|
|
1183
|
+
developer_summary="Graph validation found blockers after fix-wiki StateChart.",
|
|
1184
|
+
message="Fix-wiki bloqueado por problemas de grafo.",
|
|
1185
|
+
trigger=trigger,
|
|
1186
|
+
)
|
|
1187
|
+
case FixWikiReason.ATOMICITY_SPLIT_REQUIRED:
|
|
1188
|
+
return _blocked_state_view(
|
|
1189
|
+
facts=facts,
|
|
1190
|
+
state=state,
|
|
1191
|
+
reason=reason,
|
|
1192
|
+
phase="atomicity_split",
|
|
1193
|
+
reason_code="atomicity_split_required",
|
|
1194
|
+
public_summary="Ha split de atomicidade pendente antes de concluir.",
|
|
1195
|
+
developer_summary="Fix-wiki found pending atomicity split work that must run by the official route.",
|
|
1196
|
+
message="Fix-wiki bloqueado por split de atomicidade pendente.",
|
|
1197
|
+
trigger=trigger,
|
|
1198
|
+
)
|
|
1199
|
+
case FixWikiReason.RELATED_NOTES_BLOCKED:
|
|
1200
|
+
return _blocked_state_view(
|
|
1201
|
+
facts=facts,
|
|
1202
|
+
state=state,
|
|
1203
|
+
reason=reason,
|
|
1204
|
+
phase="related_notes",
|
|
1205
|
+
reason_code="related_notes_blocked",
|
|
1206
|
+
public_summary="As Notas Relacionadas ainda precisam ser atualizadas antes de concluir.",
|
|
1207
|
+
developer_summary="Fix-wiki could not close because Related Notes sync/export is still blocked.",
|
|
1208
|
+
message="Fix-wiki bloqueado por Notas Relacionadas pendentes.",
|
|
1209
|
+
trigger=trigger,
|
|
1210
|
+
)
|
|
1211
|
+
case FixWikiReason.LINKER_BLOCKED:
|
|
1212
|
+
return _blocked_state_view(
|
|
1213
|
+
facts=facts,
|
|
1214
|
+
state=state,
|
|
1215
|
+
reason=reason,
|
|
1216
|
+
phase="linker",
|
|
1217
|
+
reason_code="linker_blocked",
|
|
1218
|
+
public_summary="O pacote de links ainda esta bloqueado.",
|
|
1219
|
+
developer_summary="Fix-wiki could not complete because the linker package reported a blocker.",
|
|
1220
|
+
message="Fix-wiki bloqueado pelo pacote de links.",
|
|
1221
|
+
trigger=trigger,
|
|
1222
|
+
)
|
|
1223
|
+
case FixWikiReason.TAXONOMY_BLOCKED:
|
|
1224
|
+
return _blocked_state_view(
|
|
1225
|
+
facts=facts,
|
|
1226
|
+
state=state,
|
|
1227
|
+
reason=reason,
|
|
1228
|
+
phase="taxonomy",
|
|
1229
|
+
reason_code="taxonomy_blocked",
|
|
1230
|
+
public_summary="A taxonomia exige acao antes de concluir.",
|
|
1231
|
+
developer_summary="Fix-wiki found a taxonomy action/block before final health could close.",
|
|
1232
|
+
message="Fix-wiki bloqueado por acao de taxonomia.",
|
|
1233
|
+
trigger=trigger,
|
|
1234
|
+
)
|
|
1235
|
+
case FixWikiReason.VAULT_GUARD_REQUIRED:
|
|
1236
|
+
return _blocked_state_view(
|
|
1237
|
+
facts=facts,
|
|
1238
|
+
state=state,
|
|
1239
|
+
reason=reason,
|
|
1240
|
+
phase="vault_guard",
|
|
1241
|
+
reason_code="vault_guard_required",
|
|
1242
|
+
public_summary="A protecao do vault precisa ser aberta antes de alterar a Wiki.",
|
|
1243
|
+
developer_summary="Fix-wiki apply was blocked because the official vault guard was not active.",
|
|
1244
|
+
message="Fix-wiki bloqueado pela protecao do vault.",
|
|
1245
|
+
trigger=trigger,
|
|
1246
|
+
)
|
|
1247
|
+
case FixWikiReason.STYLE_REWRITE_REQUIRED:
|
|
1248
|
+
return _blocked_state_view(
|
|
1249
|
+
facts=facts,
|
|
1250
|
+
state=state,
|
|
1251
|
+
reason=reason,
|
|
1252
|
+
phase="style_rewrite",
|
|
1253
|
+
reason_code="style_rewrite_required",
|
|
1254
|
+
public_summary="Ha reescrita semantica pendente antes de concluir.",
|
|
1255
|
+
developer_summary="Fix-wiki found notes that require the official semantic rewrite route.",
|
|
1256
|
+
message="Fix-wiki bloqueado por reescrita semantica pendente.",
|
|
1257
|
+
trigger=trigger,
|
|
1258
|
+
)
|
|
1259
|
+
case FixWikiReason.FAILED:
|
|
1260
|
+
next_action = _default_next_action(facts, reason)
|
|
1261
|
+
reason_code = facts.failed_reason_code or (
|
|
1262
|
+
state.value if state != FixWikiState.FAILED else "fix_wiki_failed"
|
|
1263
|
+
)
|
|
1264
|
+
decision = WorkflowDecision(
|
|
1265
|
+
kind="failed",
|
|
1266
|
+
phase="failure",
|
|
1267
|
+
reason_code=reason_code,
|
|
1268
|
+
public_summary="O fix-wiki falhou antes de concluir a conferencia.",
|
|
1269
|
+
developer_summary="Fix-wiki emitted a failed StateChart state.",
|
|
1270
|
+
evidence=[
|
|
1271
|
+
DecisionEvidence(
|
|
1272
|
+
summary="A execucao informou falha operacional.",
|
|
1273
|
+
technical_code=reason_code,
|
|
1274
|
+
source="fix_wiki_fsm",
|
|
1275
|
+
)
|
|
1276
|
+
],
|
|
1277
|
+
next_action=next_action,
|
|
1278
|
+
)
|
|
1279
|
+
return _state_view(
|
|
1280
|
+
reason=reason,
|
|
1281
|
+
state=state,
|
|
1282
|
+
category=category,
|
|
1283
|
+
status=status,
|
|
1284
|
+
event_type=WorkflowProgressEventType.WORKFLOW_FAILED,
|
|
1285
|
+
decision=decision,
|
|
1286
|
+
next_action=next_action,
|
|
1287
|
+
message="Fix-wiki falhou antes de concluir.",
|
|
1288
|
+
trigger=trigger,
|
|
1289
|
+
)
|
|
1290
|
+
case _:
|
|
1291
|
+
return _state_view(
|
|
1292
|
+
reason=reason,
|
|
1293
|
+
state=state,
|
|
1294
|
+
category=category,
|
|
1295
|
+
status=status,
|
|
1296
|
+
event_type=_default_event_type_for_status(status),
|
|
1297
|
+
decision=model.last_transition.decision if model.last_transition is not None else None,
|
|
1298
|
+
next_action=_default_next_action(facts, reason),
|
|
1299
|
+
resume_action=model.last_transition.resume_action if model.last_transition is not None else "",
|
|
1300
|
+
resume_supported=bool(model.last_transition and model.last_transition.resume_action),
|
|
1301
|
+
can_continue_now=status == WorkflowProgressStatus.WAITING_AGENT,
|
|
1302
|
+
message=_default_message_for_state(state),
|
|
1303
|
+
trigger=trigger,
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def _state_reason_from_model(model: WorkflowModel, state: FixWikiState) -> FixWikiReason:
|
|
1308
|
+
"""Derive public reason from the canonical leaf state, not transition metadata."""
|
|
1309
|
+
|
|
1310
|
+
return reason_for_state(state)
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
def _default_event_type_for_status(status: WorkflowProgressStatus) -> WorkflowProgressEventType:
|
|
1314
|
+
match status:
|
|
1315
|
+
case WorkflowProgressStatus.RUNNING | WorkflowProgressStatus.WAITING_AGENT:
|
|
1316
|
+
return WorkflowProgressEventType.STATE_ENTERED
|
|
1317
|
+
case WorkflowProgressStatus.WAITING_EXTERNAL:
|
|
1318
|
+
return WorkflowProgressEventType.EXTERNAL_WAIT_STARTED
|
|
1319
|
+
case WorkflowProgressStatus.WAITING_HUMAN | WorkflowProgressStatus.BLOCKED:
|
|
1320
|
+
return WorkflowProgressEventType.DECISION_EMITTED
|
|
1321
|
+
case WorkflowProgressStatus.FAILED:
|
|
1322
|
+
return WorkflowProgressEventType.WORKFLOW_FAILED
|
|
1323
|
+
case WorkflowProgressStatus.COMPLETED | WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
|
|
1324
|
+
return WorkflowProgressEventType.WORKFLOW_COMPLETED
|
|
1325
|
+
raise AssertionError(f"unsupported workflow progress status: {status}")
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def _default_message_for_state(state: FixWikiState) -> str:
|
|
1329
|
+
phase = _PHASE_BY_STATE[state.value]
|
|
1330
|
+
return f"Fix-wiki em {phase}."
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
def _state_view(
|
|
1334
|
+
*,
|
|
1335
|
+
reason: FixWikiReason,
|
|
1336
|
+
state: FixWikiState,
|
|
1337
|
+
category: WorkflowStateCategory,
|
|
1338
|
+
status: WorkflowProgressStatus,
|
|
1339
|
+
event_type: WorkflowProgressEventType,
|
|
1340
|
+
message: str,
|
|
1341
|
+
trigger: str,
|
|
1342
|
+
decision: WorkflowDecision | None = None,
|
|
1343
|
+
next_action: str = "",
|
|
1344
|
+
resume_action: str = "",
|
|
1345
|
+
resume_supported: bool = False,
|
|
1346
|
+
can_continue_now: bool = False,
|
|
1347
|
+
) -> _FixWikiStateView:
|
|
1348
|
+
return _FixWikiStateView(
|
|
1349
|
+
reason=reason,
|
|
1350
|
+
state=state,
|
|
1351
|
+
category=category,
|
|
1352
|
+
status=status,
|
|
1353
|
+
event_type=event_type,
|
|
1354
|
+
decision=decision,
|
|
1355
|
+
next_action=next_action,
|
|
1356
|
+
resume_action=resume_action,
|
|
1357
|
+
resume_supported=resume_supported,
|
|
1358
|
+
can_continue_now=can_continue_now,
|
|
1359
|
+
message=message,
|
|
1360
|
+
trigger=trigger,
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
def _blocked_state_view(
|
|
1365
|
+
*,
|
|
1366
|
+
facts: FixWikiFsmFacts,
|
|
1367
|
+
state: FixWikiState,
|
|
1368
|
+
reason: FixWikiReason,
|
|
1369
|
+
phase: str,
|
|
1370
|
+
reason_code: str,
|
|
1371
|
+
public_summary: str,
|
|
1372
|
+
developer_summary: str,
|
|
1373
|
+
message: str,
|
|
1374
|
+
trigger: str,
|
|
1375
|
+
next_action: str | None = None,
|
|
1376
|
+
resume_action: str = "",
|
|
1377
|
+
resume_supported: bool = False,
|
|
1378
|
+
) -> _FixWikiStateView:
|
|
1379
|
+
next_action = next_action if next_action is not None else _default_next_action(facts, reason)
|
|
1380
|
+
effective_resume_action = resume_action or next_action
|
|
1381
|
+
category = category_for_state(state)
|
|
1382
|
+
status = WorkflowProgressStatus(category.value)
|
|
1383
|
+
if category == WorkflowStateCategory.WAITING_HUMAN:
|
|
1384
|
+
decision = _ask_human_decision(
|
|
1385
|
+
phase=phase,
|
|
1386
|
+
reason_code=reason_code,
|
|
1387
|
+
public_summary=public_summary,
|
|
1388
|
+
developer_summary=developer_summary,
|
|
1389
|
+
next_action=next_action,
|
|
1390
|
+
required_inputs=_required_inputs_for_block(facts, reason),
|
|
1391
|
+
)
|
|
1392
|
+
else:
|
|
1393
|
+
decision = _hard_block_decision(
|
|
1394
|
+
phase=phase,
|
|
1395
|
+
reason_code=reason_code,
|
|
1396
|
+
public_summary=public_summary,
|
|
1397
|
+
developer_summary=developer_summary,
|
|
1398
|
+
next_action=next_action,
|
|
1399
|
+
required_inputs=_required_inputs_for_block(facts, reason),
|
|
1400
|
+
)
|
|
1401
|
+
return _state_view(
|
|
1402
|
+
reason=reason,
|
|
1403
|
+
state=state,
|
|
1404
|
+
category=category,
|
|
1405
|
+
status=status,
|
|
1406
|
+
event_type=WorkflowProgressEventType.DECISION_EMITTED,
|
|
1407
|
+
decision=decision,
|
|
1408
|
+
next_action=next_action,
|
|
1409
|
+
resume_action=effective_resume_action,
|
|
1410
|
+
resume_supported=resume_supported,
|
|
1411
|
+
message=message,
|
|
1412
|
+
trigger=trigger,
|
|
1413
|
+
)
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
def _external_wait_state(facts: FixWikiFsmFacts) -> FixWikiState:
|
|
1417
|
+
"""Select the concrete waiting leaf from the canonical external-wait envelope."""
|
|
1418
|
+
|
|
1419
|
+
return _external_wait_state_from_payload(facts.external_wait_payload)
|
|
1420
|
+
|
|
1421
|
+
|
|
1422
|
+
def _external_wait_state_from_payload(payload: JsonObject) -> FixWikiState:
|
|
1423
|
+
"""Map a waiting-external effect envelope to the concrete fix-wiki leaf."""
|
|
1424
|
+
|
|
1425
|
+
child_payload = _optional_json_object_field(payload, "payload") or payload
|
|
1426
|
+
child = _FixWikiExternalWaitPayloadFields.model_validate(child_payload)
|
|
1427
|
+
child_state = child.state_machine_snapshot.current_state or child.progress_view_model.state
|
|
1428
|
+
if child.progress_view_model.status == "waiting_external" and child_state == "waiting_external_related_notes_quota":
|
|
1429
|
+
return FixWikiState.RELATED_NOTES_QUOTA_WAIT
|
|
1430
|
+
recovery = child.diagnostic_context.related_notes_recovery_state
|
|
1431
|
+
if _related_notes_recovery_waiting_external(recovery):
|
|
1432
|
+
return FixWikiState.RELATED_NOTES_QUOTA_WAIT
|
|
1433
|
+
effect = _FixWikiExternalWaitEffectFields.model_validate(
|
|
1434
|
+
_optional_json_object_subset(payload, "effect", ("origin_state",))
|
|
1435
|
+
)
|
|
1436
|
+
if effect.origin_state:
|
|
1437
|
+
try:
|
|
1438
|
+
state = FixWikiState(effect.origin_state)
|
|
1439
|
+
except ValueError:
|
|
1440
|
+
state = FixWikiState.STYLE_REWRITE_CAPACITY_WAIT
|
|
1441
|
+
if category_for_state(state) == WorkflowStateCategory.WAITING_EXTERNAL:
|
|
1442
|
+
return state
|
|
1443
|
+
return FixWikiState.STYLE_REWRITE_CAPACITY_WAIT
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
def _related_notes_recovery_waiting_external(recovery: JsonObject) -> bool:
|
|
1447
|
+
typed = RelatedNotesRecoveryState.from_payload(recovery)
|
|
1448
|
+
return (
|
|
1449
|
+
typed.status == "waiting_for_retry"
|
|
1450
|
+
and typed.resume_supported
|
|
1451
|
+
and typed.blocked_reason
|
|
1452
|
+
in {
|
|
1453
|
+
"related_notes_headless_quota_exhausted",
|
|
1454
|
+
"related_notes_headless_time_budget_exhausted",
|
|
1455
|
+
}
|
|
1456
|
+
)
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
def _hard_block_decision(
|
|
1460
|
+
*,
|
|
1461
|
+
phase: str,
|
|
1462
|
+
reason_code: str,
|
|
1463
|
+
public_summary: str,
|
|
1464
|
+
developer_summary: str,
|
|
1465
|
+
next_action: str,
|
|
1466
|
+
required_inputs: list[str] | None = None,
|
|
1467
|
+
) -> WorkflowDecision:
|
|
1468
|
+
return WorkflowDecision(
|
|
1469
|
+
kind="hard_block",
|
|
1470
|
+
phase=phase,
|
|
1471
|
+
reason_code=reason_code,
|
|
1472
|
+
public_summary=public_summary,
|
|
1473
|
+
developer_summary=developer_summary,
|
|
1474
|
+
evidence=[
|
|
1475
|
+
DecisionEvidence(
|
|
1476
|
+
summary="O FSM classificou o resultado como bloqueado.",
|
|
1477
|
+
technical_code=reason_code,
|
|
1478
|
+
source="fix_wiki_fsm",
|
|
1479
|
+
)
|
|
1480
|
+
],
|
|
1481
|
+
next_action=next_action,
|
|
1482
|
+
required_inputs=list(required_inputs or []),
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
def _ask_human_decision(
|
|
1487
|
+
*,
|
|
1488
|
+
phase: str,
|
|
1489
|
+
reason_code: str,
|
|
1490
|
+
public_summary: str,
|
|
1491
|
+
developer_summary: str,
|
|
1492
|
+
next_action: str,
|
|
1493
|
+
required_inputs: list[str] | None = None,
|
|
1494
|
+
) -> WorkflowDecision:
|
|
1495
|
+
return WorkflowDecision(
|
|
1496
|
+
kind="ask_human",
|
|
1497
|
+
phase=phase,
|
|
1498
|
+
reason_code=reason_code,
|
|
1499
|
+
public_summary=public_summary,
|
|
1500
|
+
developer_summary=developer_summary,
|
|
1501
|
+
evidence=[
|
|
1502
|
+
DecisionEvidence(
|
|
1503
|
+
summary="O FSM entrou em uma folha que exige escolha ou revisão humana.",
|
|
1504
|
+
technical_code=reason_code,
|
|
1505
|
+
source="fix_wiki_fsm",
|
|
1506
|
+
)
|
|
1507
|
+
],
|
|
1508
|
+
next_action=next_action,
|
|
1509
|
+
required_inputs=list(required_inputs or []),
|
|
1510
|
+
resume_action=next_action,
|
|
1511
|
+
recommended_option_id="continue_official_route",
|
|
1512
|
+
human_decision_kind=reason_code,
|
|
1513
|
+
options=[
|
|
1514
|
+
HumanDecisionOption(
|
|
1515
|
+
id="continue_official_route",
|
|
1516
|
+
label="Continuar",
|
|
1517
|
+
description=next_action,
|
|
1518
|
+
),
|
|
1519
|
+
HumanDecisionOption(
|
|
1520
|
+
id="stop_here",
|
|
1521
|
+
label="Parar",
|
|
1522
|
+
description="Encerrar este workflow sem aplicar a próxima etapa agora.",
|
|
1523
|
+
),
|
|
1524
|
+
],
|
|
1525
|
+
rejected_automations=[
|
|
1526
|
+
RejectedAutomation(
|
|
1527
|
+
kind="auto_fix",
|
|
1528
|
+
reason_code=reason_code,
|
|
1529
|
+
reason="A próxima ação depende de revisão ou escolha humana antes de mutar a Wiki.",
|
|
1530
|
+
),
|
|
1531
|
+
RejectedAutomation(
|
|
1532
|
+
kind="auto_defer",
|
|
1533
|
+
reason_code=reason_code,
|
|
1534
|
+
reason="Adiar manteria a folha waiting_human aberta sem decisão registrada.",
|
|
1535
|
+
),
|
|
1536
|
+
RejectedAutomation(
|
|
1537
|
+
kind="auto_plan",
|
|
1538
|
+
reason_code=reason_code,
|
|
1539
|
+
reason="O plano precisa da decisão humana para escolher a próxima rota segura.",
|
|
1540
|
+
),
|
|
1541
|
+
],
|
|
1542
|
+
)
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
def _projection_human_decision_packet(projection: _FixWikiStateView) -> JsonObject | None:
|
|
1546
|
+
if projection.status != WorkflowProgressStatus.WAITING_HUMAN:
|
|
1547
|
+
return None
|
|
1548
|
+
if projection.decision is None:
|
|
1549
|
+
return None
|
|
1550
|
+
return projection.decision.to_human_decision_packet()
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
def _decision_with_recovery_action(decision: WorkflowDecision, *, next_action: str) -> WorkflowDecision:
|
|
1554
|
+
"""Revalidate a StateChart decision after adding artifact-specific recovery text."""
|
|
1555
|
+
|
|
1556
|
+
updated = decision.model_copy(update={"next_action": next_action, "resume_action": next_action})
|
|
1557
|
+
return WorkflowDecision.model_validate(updated.to_payload())
|
|
1558
|
+
|
|
1559
|
+
|
|
1560
|
+
def _decision_with_leaf_reason(decision: WorkflowDecision, *, reason_code: str, phase: str) -> WorkflowDecision:
|
|
1561
|
+
"""Make the public decision reason follow the reached leaf, not runtime metadata."""
|
|
1562
|
+
|
|
1563
|
+
human_kind = decision.human_decision_kind or decision.reason_code
|
|
1564
|
+
updated = decision.model_copy(update={"reason_code": reason_code, "phase": phase, "human_decision_kind": human_kind})
|
|
1565
|
+
return WorkflowDecision.model_validate(updated.to_payload())
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
def _required_inputs_for_block(facts: FixWikiFsmFacts, reason: FixWikiReason) -> list[str]:
|
|
1569
|
+
match reason:
|
|
1570
|
+
case (
|
|
1571
|
+
FixWikiReason.GRAPH_BLOCKED
|
|
1572
|
+
| FixWikiReason.LINKER_BLOCKED
|
|
1573
|
+
| FixWikiReason.RELATED_NOTES_BLOCKED
|
|
1574
|
+
| FixWikiReason.TAXONOMY_BLOCKED
|
|
1575
|
+
| FixWikiReason.VAULT_GUARD_REQUIRED
|
|
1576
|
+
):
|
|
1577
|
+
return _clean_required_inputs(facts.required_inputs)
|
|
1578
|
+
case FixWikiReason.FAILED:
|
|
1579
|
+
error_fields = _FixWikiErrorRequiredInputs.model_validate(facts.error_context)
|
|
1580
|
+
return _clean_required_inputs(error_fields.required_inputs)
|
|
1581
|
+
case _:
|
|
1582
|
+
return []
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
def _clean_required_inputs(value: object) -> list[str]:
|
|
1586
|
+
if value is None:
|
|
1587
|
+
return []
|
|
1588
|
+
raw_items = JsonObjectAdapter.validate_python({"items": value})["items"]
|
|
1589
|
+
if not isinstance(raw_items, list):
|
|
1590
|
+
raise ValueError("required_inputs must be a list of strings")
|
|
1591
|
+
cleaned: list[str] = []
|
|
1592
|
+
for item in raw_items:
|
|
1593
|
+
if not isinstance(item, str):
|
|
1594
|
+
raise ValueError("required_inputs must contain only strings")
|
|
1595
|
+
text = item.strip()
|
|
1596
|
+
if text:
|
|
1597
|
+
cleaned.append(text)
|
|
1598
|
+
return cleaned
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
def _default_next_action(facts: FixWikiFsmFacts, reason: FixWikiReason) -> str:
|
|
1602
|
+
candidate = facts.next_action.strip()
|
|
1603
|
+
match reason:
|
|
1604
|
+
case FixWikiReason.ENVIRONMENT_PATHS_MISSING | FixWikiReason.ENVIRONMENT_WIKI_DIR_MISSING:
|
|
1605
|
+
return candidate or "setup:set-paths"
|
|
1606
|
+
case FixWikiReason.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED:
|
|
1607
|
+
return candidate or "setup:bootstrap-python"
|
|
1608
|
+
case FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
|
|
1609
|
+
return candidate or "Aguardar a cota externa e retomar pela rota oficial."
|
|
1610
|
+
case FixWikiReason.WAITING_EXTERNAL:
|
|
1611
|
+
return facts.external_wait_resume_action or "Aguardar a condicao externa e retomar pela rota oficial."
|
|
1612
|
+
case FixWikiReason.WAITING_HUMAN:
|
|
1613
|
+
return candidate or "Responder a decisao solicitada para continuar."
|
|
1614
|
+
case FixWikiReason.STYLE_REWRITE_REVIEW_REQUIRED:
|
|
1615
|
+
return "Executar a reescrita semantica oficial antes de concluir."
|
|
1616
|
+
case FixWikiReason.TAXONOMY_DECISION_REQUIRED:
|
|
1617
|
+
return "Resolver a acao de taxonomia pela rota oficial antes de concluir."
|
|
1618
|
+
case FixWikiReason.VOCABULARY_EVAL_NEEDS_REVIEW:
|
|
1619
|
+
return "Revisar a avaliacao do vocabulario e retomar pela rota oficial."
|
|
1620
|
+
case FixWikiReason.GRAPH_REVIEW_REQUIRED:
|
|
1621
|
+
return candidate or "Revisar os bloqueios de grafo e retomar pela rota oficial."
|
|
1622
|
+
case FixWikiReason.ATOMICITY_SPLIT_REVIEW_REQUIRED:
|
|
1623
|
+
return _atomicity_split_recovery_action(facts)
|
|
1624
|
+
case FixWikiReason.MERGE_REVIEW_REQUIRED:
|
|
1625
|
+
return "Revisar o merge de notas e retomar pela rota oficial."
|
|
1626
|
+
case FixWikiReason.SUBAGENT_PLAN_ATTESTATION_REQUIRED | FixWikiReason.SUBAGENT_PLAN_ATTESTATION_INVALID:
|
|
1627
|
+
return "Reemitir o plano de subagente pela rota oficial com atestacao valida."
|
|
1628
|
+
case FixWikiReason.STYLE_REWRITE_READY:
|
|
1629
|
+
return "Continuar pela reescrita especializada, aplicar a versao validada e repetir a conferencia da Wiki."
|
|
1630
|
+
case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
|
|
1631
|
+
return candidate or "Executar a curadoria semantica do vocabulario e repetir a conferencia da Wiki."
|
|
1632
|
+
case FixWikiReason.GRAPH_BLOCKED:
|
|
1633
|
+
return candidate or "Executar /mednotes:link para reparar WikiLinks e grafo pela rota oficial."
|
|
1634
|
+
case FixWikiReason.LINK_RUN_REQUESTED:
|
|
1635
|
+
return candidate or "Executar /mednotes:link pela rota oficial antes de concluir o fix-wiki."
|
|
1636
|
+
case FixWikiReason.ATOMICITY_SPLIT_REQUIRED:
|
|
1637
|
+
return candidate or _atomicity_split_recovery_action(facts)
|
|
1638
|
+
case FixWikiReason.RELATED_NOTES_BLOCKED:
|
|
1639
|
+
return candidate or (
|
|
1640
|
+
"Conferir o export do Related Notes: o workflow não conseguiu provar que o export cobre esta Wiki; "
|
|
1641
|
+
"atualize as Notas Relacionadas e repita a conferência."
|
|
1642
|
+
)
|
|
1643
|
+
case FixWikiReason.LINKER_BLOCKED:
|
|
1644
|
+
return candidate or "Retomar o pacote de links pela rota oficial antes de concluir."
|
|
1645
|
+
case FixWikiReason.TAXONOMY_BLOCKED:
|
|
1646
|
+
return candidate or "Resolver a acao de taxonomia pela rota oficial antes de concluir."
|
|
1647
|
+
case FixWikiReason.VAULT_GUARD_REQUIRED:
|
|
1648
|
+
return candidate or "Abrir a protecao do vault pela rota oficial e repetir o apply."
|
|
1649
|
+
case FixWikiReason.STYLE_REWRITE_REQUIRED:
|
|
1650
|
+
return "Executar a reescrita semantica oficial antes de concluir."
|
|
1651
|
+
case FixWikiReason.FAILED:
|
|
1652
|
+
return candidate or "Revisar o erro e retomar pela rota oficial indicada."
|
|
1653
|
+
case FixWikiReason.COMPLETED_WITH_WARNINGS:
|
|
1654
|
+
return candidate or "Revisar os avisos pendentes quando possivel."
|
|
1655
|
+
case FixWikiReason.PREVIEW_READY | FixWikiReason.COMPLETED:
|
|
1656
|
+
return ""
|
|
1657
|
+
|
|
1658
|
+
|
|
1659
|
+
def _atomicity_split_recovery_action(facts: FixWikiFsmFacts) -> str:
|
|
1660
|
+
"""Render the official recovery route from the FSM artifact snapshot."""
|
|
1661
|
+
|
|
1662
|
+
plan_path = _FixWikiArtifactPathFields.model_validate(facts.artifacts).atomicity_split_plan_path.strip()
|
|
1663
|
+
command = "apply-atomicity-split"
|
|
1664
|
+
if plan_path:
|
|
1665
|
+
return (
|
|
1666
|
+
f"Revisar {plan_path}, executar {command} para os bundles aprovados "
|
|
1667
|
+
"e repetir /mednotes:fix-wiki."
|
|
1668
|
+
)
|
|
1669
|
+
return f"Executar {command} para os bundles aprovados e repetir /mednotes:fix-wiki."
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
def _error_context(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> JsonObject:
|
|
1673
|
+
existing = JsonObjectAdapter.validate_python(facts.error_context or {})
|
|
1674
|
+
if projection.status not in {
|
|
1675
|
+
WorkflowProgressStatus.BLOCKED,
|
|
1676
|
+
WorkflowProgressStatus.FAILED,
|
|
1677
|
+
WorkflowProgressStatus.WAITING_HUMAN,
|
|
1678
|
+
}:
|
|
1679
|
+
return existing
|
|
1680
|
+
expected_reason = projection.decision.reason_code if projection.decision is not None else projection.reason.value
|
|
1681
|
+
if _existing_error_context_matches(existing, expected_reason, projection.next_action):
|
|
1682
|
+
return existing
|
|
1683
|
+
return build_error_context(
|
|
1684
|
+
phase=projection.decision.phase if projection.decision is not None else _progress_phase(facts, projection),
|
|
1685
|
+
blocked_reason=expected_reason,
|
|
1686
|
+
root_cause=expected_reason,
|
|
1687
|
+
affected_artifact=_affected_artifact_for_reason(projection.reason),
|
|
1688
|
+
error_summary=projection.message,
|
|
1689
|
+
suggested_fix=projection.next_action,
|
|
1690
|
+
next_action=projection.next_action,
|
|
1691
|
+
retry_scope=_retry_scope_for_reason(projection.reason),
|
|
1692
|
+
human_decision_required=projection.status == WorkflowProgressStatus.WAITING_HUMAN,
|
|
1693
|
+
)
|
|
1694
|
+
|
|
1695
|
+
|
|
1696
|
+
def _existing_error_context_matches(context: JsonObject, expected_reason: str, expected_next_action: str) -> bool:
|
|
1697
|
+
if not context:
|
|
1698
|
+
return False
|
|
1699
|
+
fields = _FixWikiExistingErrorContextFields.model_validate(context)
|
|
1700
|
+
return expected_reason in {fields.blocked_reason, fields.root_cause} and fields.next_action == expected_next_action
|
|
1701
|
+
|
|
1702
|
+
|
|
1703
|
+
def _affected_artifact_for_reason(reason: FixWikiReason) -> str:
|
|
1704
|
+
match reason:
|
|
1705
|
+
case FixWikiReason.ENVIRONMENT_PATHS_MISSING | FixWikiReason.ENVIRONMENT_WIKI_DIR_MISSING:
|
|
1706
|
+
return "workbench_paths_config"
|
|
1707
|
+
case FixWikiReason.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED:
|
|
1708
|
+
return "python_environment"
|
|
1709
|
+
case (
|
|
1710
|
+
FixWikiReason.STYLE_REWRITE_REQUIRED
|
|
1711
|
+
| FixWikiReason.STYLE_REWRITE_READY
|
|
1712
|
+
| FixWikiReason.STYLE_REWRITE_REVIEW_REQUIRED
|
|
1713
|
+
):
|
|
1714
|
+
return "style_rewrite_plan"
|
|
1715
|
+
case FixWikiReason.SUBAGENT_PLAN_ATTESTATION_REQUIRED | FixWikiReason.SUBAGENT_PLAN_ATTESTATION_INVALID:
|
|
1716
|
+
return "subagent_plan"
|
|
1717
|
+
case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
|
|
1718
|
+
return "vocabulary_semantic_repair"
|
|
1719
|
+
case FixWikiReason.VOCABULARY_EVAL_NEEDS_REVIEW:
|
|
1720
|
+
return "vocabulary_eval_report"
|
|
1721
|
+
case FixWikiReason.RELATED_NOTES_BLOCKED | FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
|
|
1722
|
+
return "related_notes_export"
|
|
1723
|
+
case FixWikiReason.LINKER_BLOCKED | FixWikiReason.GRAPH_BLOCKED | FixWikiReason.GRAPH_REVIEW_REQUIRED:
|
|
1724
|
+
return "linker_diagnosis"
|
|
1725
|
+
case FixWikiReason.TAXONOMY_BLOCKED | FixWikiReason.TAXONOMY_DECISION_REQUIRED:
|
|
1726
|
+
return "taxonomy_plan"
|
|
1727
|
+
case FixWikiReason.ATOMICITY_SPLIT_REQUIRED | FixWikiReason.ATOMICITY_SPLIT_REVIEW_REQUIRED:
|
|
1728
|
+
return "atomicity_split_plan"
|
|
1729
|
+
case FixWikiReason.MERGE_REVIEW_REQUIRED:
|
|
1730
|
+
return "note_merge_plan"
|
|
1731
|
+
case _:
|
|
1732
|
+
return "fix_wiki_plan"
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
def _retry_scope_for_reason(reason: FixWikiReason) -> str:
|
|
1736
|
+
match reason:
|
|
1737
|
+
case (
|
|
1738
|
+
FixWikiReason.ENVIRONMENT_PATHS_MISSING
|
|
1739
|
+
| FixWikiReason.ENVIRONMENT_WIKI_DIR_MISSING
|
|
1740
|
+
| FixWikiReason.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED
|
|
1741
|
+
):
|
|
1742
|
+
return "setup_then_rerun_fix_wiki"
|
|
1743
|
+
case (
|
|
1744
|
+
FixWikiReason.STYLE_REWRITE_REQUIRED
|
|
1745
|
+
| FixWikiReason.STYLE_REWRITE_READY
|
|
1746
|
+
| FixWikiReason.STYLE_REWRITE_REVIEW_REQUIRED
|
|
1747
|
+
):
|
|
1748
|
+
return "style_rewrite_official_route"
|
|
1749
|
+
case FixWikiReason.SUBAGENT_PLAN_ATTESTATION_REQUIRED | FixWikiReason.SUBAGENT_PLAN_ATTESTATION_INVALID:
|
|
1750
|
+
return "subagent_plan_attestation"
|
|
1751
|
+
case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
|
|
1752
|
+
return "vocabulary_semantic_ingestion_then_rerun_fix_wiki"
|
|
1753
|
+
case FixWikiReason.VOCABULARY_EVAL_NEEDS_REVIEW:
|
|
1754
|
+
return "vocabulary_eval_review"
|
|
1755
|
+
case FixWikiReason.GRAPH_REVIEW_REQUIRED:
|
|
1756
|
+
return "link_review_then_rerun_fix_wiki"
|
|
1757
|
+
case FixWikiReason.ATOMICITY_SPLIT_REQUIRED | FixWikiReason.ATOMICITY_SPLIT_REVIEW_REQUIRED:
|
|
1758
|
+
return "atomicity_split_then_rerun_fix_wiki"
|
|
1759
|
+
case FixWikiReason.MERGE_REVIEW_REQUIRED:
|
|
1760
|
+
return "note_merge_review"
|
|
1761
|
+
case FixWikiReason.RELATED_NOTES_BLOCKED | FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
|
|
1762
|
+
return "related_notes_then_rerun_fix_wiki"
|
|
1763
|
+
case FixWikiReason.LINKER_BLOCKED | FixWikiReason.GRAPH_BLOCKED:
|
|
1764
|
+
return "link_then_rerun_fix_wiki"
|
|
1765
|
+
case FixWikiReason.TAXONOMY_DECISION_REQUIRED:
|
|
1766
|
+
return "taxonomy_official_route"
|
|
1767
|
+
case _:
|
|
1768
|
+
return "fix_wiki_official_route"
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
def _related_notes_waiting_external(facts: FixWikiRuntimeFacts) -> bool:
|
|
1772
|
+
state = facts.related_notes_recovery_state
|
|
1773
|
+
return (
|
|
1774
|
+
facts.related_notes_blocked
|
|
1775
|
+
and state.status == "waiting_for_retry"
|
|
1776
|
+
and state.resume_supported
|
|
1777
|
+
and state.blocked_reason
|
|
1778
|
+
in {
|
|
1779
|
+
"related_notes_headless_quota_exhausted",
|
|
1780
|
+
"related_notes_headless_time_budget_exhausted",
|
|
1781
|
+
}
|
|
1782
|
+
)
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
def _specialist_model_waiting_agent(facts: FixWikiRuntimeFacts) -> bool:
|
|
1786
|
+
if facts.requires_llm_rewrite_count <= 0:
|
|
1787
|
+
return False
|
|
1788
|
+
for effect in facts.pending_effects:
|
|
1789
|
+
if effect.kind == WorkflowEffectKind.CALL_SPECIALIST_MODEL:
|
|
1790
|
+
return True
|
|
1791
|
+
return False
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
def _related_notes_external_wait_message(facts: FixWikiFsmFacts) -> str:
|
|
1795
|
+
blocked_reason = facts.related_notes_recovery_state.blocked_reason
|
|
1796
|
+
if blocked_reason == "related_notes_headless_time_budget_exhausted":
|
|
1797
|
+
return "Related Notes pausou a indexação para evitar uma execução longa; a próxima tentativa retoma do índice parcial."
|
|
1798
|
+
if blocked_reason == "related_notes_headless_quota_exhausted":
|
|
1799
|
+
return "Related Notes aguardando cota externa para retomar pela rota oficial."
|
|
1800
|
+
return "Related Notes aguardando condição externa para retomar pela rota oficial."
|
|
1801
|
+
|
|
1802
|
+
|
|
1803
|
+
def _progress_user_action(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> str:
|
|
1804
|
+
"""Domain-owned user action text; kernel progress stays domain-agnostic."""
|
|
1805
|
+
|
|
1806
|
+
if projection.status != WorkflowProgressStatus.WAITING_EXTERNAL:
|
|
1807
|
+
return ""
|
|
1808
|
+
if projection.reason == FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
|
|
1809
|
+
blocked_reason = facts.related_notes_recovery_state.blocked_reason
|
|
1810
|
+
if blocked_reason == "related_notes_headless_time_budget_exhausted":
|
|
1811
|
+
return (
|
|
1812
|
+
"A indexacao pausou para evitar uma execucao longa; "
|
|
1813
|
+
"o progresso foi preservado para retomar pela acao oficial."
|
|
1814
|
+
)
|
|
1815
|
+
if blocked_reason == "related_notes_headless_quota_exhausted":
|
|
1816
|
+
return "Aguarde a cota externa; o progresso foi preservado para retomar pela acao oficial."
|
|
1817
|
+
if (
|
|
1818
|
+
projection.reason == FixWikiReason.WAITING_EXTERNAL
|
|
1819
|
+
and facts.external_wait_reason_code == "specialist_model_capacity_unavailable"
|
|
1820
|
+
):
|
|
1821
|
+
return "Aguarde o modelo especializado antes de retomar pela rota oficial."
|
|
1822
|
+
return ""
|
|
1823
|
+
|
|
1824
|
+
|
|
1825
|
+
def _progress_state(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> WorkflowProgressState:
|
|
1826
|
+
current = _related_current(facts.related_notes_recovery_state)
|
|
1827
|
+
total = _related_total(facts.related_notes_recovery_state)
|
|
1828
|
+
remaining = _related_remaining(facts.related_notes_recovery_state, current=current, total=total)
|
|
1829
|
+
if total and current > total:
|
|
1830
|
+
current = max(0, total - remaining) if remaining else total
|
|
1831
|
+
if total == 0:
|
|
1832
|
+
current = max(0, facts.total_changed_count)
|
|
1833
|
+
total = max(current, facts.total_changed_count)
|
|
1834
|
+
remaining = max(0, total - current)
|
|
1835
|
+
counts = WorkflowProgressCounts(
|
|
1836
|
+
planned_items=total,
|
|
1837
|
+
processed_items=current,
|
|
1838
|
+
warnings=facts.warning_count,
|
|
1839
|
+
mutated_files=_applied_mutation_file_count(facts),
|
|
1840
|
+
written_files=_applied_written_file_count(facts),
|
|
1841
|
+
remaining_items=remaining,
|
|
1842
|
+
blocked_items=_blocked_item_count(facts),
|
|
1843
|
+
)
|
|
1844
|
+
return WorkflowProgressState(
|
|
1845
|
+
workflow=FIX_WIKI_WORKFLOW,
|
|
1846
|
+
run_id=facts.run_id,
|
|
1847
|
+
state=projection.state.value,
|
|
1848
|
+
phase=_progress_phase(facts, projection),
|
|
1849
|
+
event_type=projection.event_type,
|
|
1850
|
+
message=projection.message,
|
|
1851
|
+
status=projection.status,
|
|
1852
|
+
current=current,
|
|
1853
|
+
total=total,
|
|
1854
|
+
counts=counts,
|
|
1855
|
+
resume_action=projection.resume_action,
|
|
1856
|
+
resume_supported=projection.resume_supported,
|
|
1857
|
+
can_continue_now=projection.can_continue_now,
|
|
1858
|
+
user_action=_progress_user_action(facts, projection),
|
|
1859
|
+
decision=projection.decision.decision_summary() if projection.decision is not None else None,
|
|
1860
|
+
technical_context={
|
|
1861
|
+
"reason": projection.reason.value,
|
|
1862
|
+
"trigger": projection.trigger,
|
|
1863
|
+
"related_notes_blocked_reason": facts.related_notes_recovery_state.blocked_reason,
|
|
1864
|
+
},
|
|
1865
|
+
)
|
|
1866
|
+
|
|
1867
|
+
|
|
1868
|
+
def _fsm_directive_instructions(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> list[str]:
|
|
1869
|
+
common = [
|
|
1870
|
+
(
|
|
1871
|
+
"agent_instruction: use progress_view_model, state_machine_snapshot, receipt, "
|
|
1872
|
+
"reports.public_report.lines and reports.details.primary_objective_summary as source of truth."
|
|
1873
|
+
),
|
|
1874
|
+
"agent_instruction: ignore diagnostic-only fields when they contradict FSM fields.",
|
|
1875
|
+
]
|
|
1876
|
+
match projection.reason:
|
|
1877
|
+
case FixWikiReason.PREVIEW_READY:
|
|
1878
|
+
specific = [
|
|
1879
|
+
"agent_instruction: report that this was a preview and no vault files were changed.",
|
|
1880
|
+
"agent_instruction: do not run extra probes or repeat fix-wiki before the user requests apply.",
|
|
1881
|
+
]
|
|
1882
|
+
case FixWikiReason.COMPLETED:
|
|
1883
|
+
specific = [
|
|
1884
|
+
"agent_instruction: write the final report now using reports.public_report.lines and reports.details.primary_objective_summary.",
|
|
1885
|
+
"agent_instruction: do not run additional diagnostics after completed status.",
|
|
1886
|
+
]
|
|
1887
|
+
case FixWikiReason.COMPLETED_WITH_WARNINGS:
|
|
1888
|
+
specific = [
|
|
1889
|
+
"agent_instruction: write the final report now and explicitly include the remaining warnings.",
|
|
1890
|
+
"agent_instruction: do not describe completed_with_warnings as a clean completion.",
|
|
1891
|
+
]
|
|
1892
|
+
case FixWikiReason.STYLE_REWRITE_READY:
|
|
1893
|
+
specific = [
|
|
1894
|
+
"agent_instruction: do not write the final report yet.",
|
|
1895
|
+
(
|
|
1896
|
+
"agent_instruction: execute agent_directive.control.effects through the official "
|
|
1897
|
+
"harness route before final report."
|
|
1898
|
+
),
|
|
1899
|
+
(
|
|
1900
|
+
"agent_instruction: do not repeat /mednotes:fix-wiki preview before the executable "
|
|
1901
|
+
"effects are applied or blocked."
|
|
1902
|
+
),
|
|
1903
|
+
"agent_instruction: do not invent specialist prompts or substitute packaged specialists.",
|
|
1904
|
+
"agent_instruction: do not edit receipts, manifests, attestations or specialist outputs by hand.",
|
|
1905
|
+
(
|
|
1906
|
+
"agent_instruction: follow agent_directive.control.effects returned by each official apply; "
|
|
1907
|
+
"only rerun /mednotes:fix-wiki when the style queue is empty."
|
|
1908
|
+
),
|
|
1909
|
+
]
|
|
1910
|
+
case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
|
|
1911
|
+
specific = [
|
|
1912
|
+
"agent_instruction: do not write the final report yet.",
|
|
1913
|
+
(
|
|
1914
|
+
"agent_instruction: execute agent_directive.control.effects through the official "
|
|
1915
|
+
"harness route before final report."
|
|
1916
|
+
),
|
|
1917
|
+
(
|
|
1918
|
+
"agent_instruction: do not classify vocabulary semantic ingestion as linker failure; "
|
|
1919
|
+
"this is an executable waiting_agent state."
|
|
1920
|
+
),
|
|
1921
|
+
"agent_instruction: rerun /mednotes:fix-wiki only after the vocabulary effect completes or blocks.",
|
|
1922
|
+
]
|
|
1923
|
+
case FixWikiReason.WAITING_EXTERNAL | FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
|
|
1924
|
+
specific = [
|
|
1925
|
+
"agent_instruction: report the external wait and preserved progress; do not claim the Wiki is fixed.",
|
|
1926
|
+
"agent_instruction: do not manually call external APIs or regenerate external indexes outside the official route.",
|
|
1927
|
+
]
|
|
1928
|
+
if projection.resume_action:
|
|
1929
|
+
specific.append(f"agent_instruction: resume only through resume_action: {projection.resume_action}.")
|
|
1930
|
+
case (
|
|
1931
|
+
FixWikiReason.WAITING_HUMAN
|
|
1932
|
+
| FixWikiReason.SUBAGENT_PLAN_ATTESTATION_REQUIRED
|
|
1933
|
+
| FixWikiReason.SUBAGENT_PLAN_ATTESTATION_INVALID
|
|
1934
|
+
| FixWikiReason.STYLE_REWRITE_REVIEW_REQUIRED
|
|
1935
|
+
| FixWikiReason.TAXONOMY_DECISION_REQUIRED
|
|
1936
|
+
| FixWikiReason.VOCABULARY_EVAL_NEEDS_REVIEW
|
|
1937
|
+
| FixWikiReason.GRAPH_REVIEW_REQUIRED
|
|
1938
|
+
| FixWikiReason.ATOMICITY_SPLIT_REVIEW_REQUIRED
|
|
1939
|
+
| FixWikiReason.MERGE_REVIEW_REQUIRED
|
|
1940
|
+
):
|
|
1941
|
+
specific = [
|
|
1942
|
+
"agent_instruction: present human_decision_packet options; do not choose on behalf of the user.",
|
|
1943
|
+
"agent_instruction: do not mutate the vault until the human decision is provided.",
|
|
1944
|
+
]
|
|
1945
|
+
case (
|
|
1946
|
+
FixWikiReason.ENVIRONMENT_PATHS_MISSING
|
|
1947
|
+
| FixWikiReason.ENVIRONMENT_WIKI_DIR_MISSING
|
|
1948
|
+
| FixWikiReason.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED
|
|
1949
|
+
):
|
|
1950
|
+
specific = [
|
|
1951
|
+
"agent_instruction: do not claim fix-wiki failed; this is a recoverable setup blocker.",
|
|
1952
|
+
"agent_instruction: execute only the typed /mednotes:setup effect before rerunning fix-wiki.",
|
|
1953
|
+
"agent_instruction: do not patch scripts or prompts as a workaround for environment setup.",
|
|
1954
|
+
]
|
|
1955
|
+
case (
|
|
1956
|
+
FixWikiReason.GRAPH_BLOCKED
|
|
1957
|
+
| FixWikiReason.RELATED_NOTES_BLOCKED
|
|
1958
|
+
| FixWikiReason.LINKER_BLOCKED
|
|
1959
|
+
| FixWikiReason.TAXONOMY_BLOCKED
|
|
1960
|
+
| FixWikiReason.STYLE_REWRITE_REQUIRED
|
|
1961
|
+
| FixWikiReason.ATOMICITY_SPLIT_REQUIRED
|
|
1962
|
+
):
|
|
1963
|
+
specific = [
|
|
1964
|
+
"agent_instruction: report the blocker and next action; do not treat tool success as workflow success.",
|
|
1965
|
+
"agent_instruction: do not mutate the vault or launch alternate commands outside the FSM next action.",
|
|
1966
|
+
]
|
|
1967
|
+
case FixWikiReason.FAILED:
|
|
1968
|
+
specific = [
|
|
1969
|
+
"agent_instruction: report the failure root cause and next action; do not claim success.",
|
|
1970
|
+
"agent_instruction: do not retry with ad hoc commands outside the official route.",
|
|
1971
|
+
]
|
|
1972
|
+
case _:
|
|
1973
|
+
specific = [
|
|
1974
|
+
"agent_instruction: follow the StateChart status and report only after the FSM reaches a terminal state.",
|
|
1975
|
+
]
|
|
1976
|
+
if facts.pending_effects and projection.reason != FixWikiReason.STYLE_REWRITE_READY:
|
|
1977
|
+
specific.append("agent_instruction: pending_effects exist; do not ignore them in the final report.")
|
|
1978
|
+
return [*common, *specific]
|
|
1979
|
+
|
|
1980
|
+
|
|
1981
|
+
def _progress_phase(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> str:
|
|
1982
|
+
if projection.reason == FixWikiReason.WAITING_EXTERNAL:
|
|
1983
|
+
if projection.state == FixWikiState.RELATED_NOTES_QUOTA_WAIT:
|
|
1984
|
+
return _PHASE_BY_STATE[projection.state.value]
|
|
1985
|
+
payload = facts.external_wait_payload
|
|
1986
|
+
effect = _FixWikiExternalWaitEffectFields.model_validate(
|
|
1987
|
+
_optional_json_object_subset(payload, "effect", ("origin_state",))
|
|
1988
|
+
)
|
|
1989
|
+
phase = effect.origin_state.strip()
|
|
1990
|
+
if phase:
|
|
1991
|
+
return _PHASE_BY_STATE.get(phase, phase)
|
|
1992
|
+
if facts.external_wait_reason_code == "specialist_model_capacity_unavailable":
|
|
1993
|
+
return "style_rewrite"
|
|
1994
|
+
if projection.reason == FixWikiReason.STYLE_REWRITE_READY:
|
|
1995
|
+
return "style_rewrite"
|
|
1996
|
+
return _PHASE_BY_STATE[projection.state.value]
|
|
1997
|
+
|
|
1998
|
+
|
|
1999
|
+
def _snapshot_from_model(
|
|
2000
|
+
model: WorkflowModel,
|
|
2001
|
+
projection: _FixWikiStateView,
|
|
2002
|
+
progress_state: WorkflowProgressState,
|
|
2003
|
+
) -> WorkflowStateMachineSnapshot:
|
|
2004
|
+
if model.state != projection.state.value:
|
|
2005
|
+
raise ValueError("FixWikiMachine state must match public projection state")
|
|
2006
|
+
progress_event = WorkflowProgressEvent(
|
|
2007
|
+
workflow=FIX_WIKI_WORKFLOW,
|
|
2008
|
+
run_id=model.run_id,
|
|
2009
|
+
state=projection.state.value,
|
|
2010
|
+
phase=progress_state.phase,
|
|
2011
|
+
event_type=projection.event_type,
|
|
2012
|
+
message=projection.message,
|
|
2013
|
+
status=projection.status,
|
|
2014
|
+
current=progress_state.current,
|
|
2015
|
+
total=progress_state.total,
|
|
2016
|
+
counts=progress_state.counts,
|
|
2017
|
+
resume_action=projection.resume_action,
|
|
2018
|
+
resume_supported=projection.resume_supported,
|
|
2019
|
+
can_continue_now=projection.can_continue_now,
|
|
2020
|
+
decision=progress_state.decision,
|
|
2021
|
+
technical_context=progress_state.technical_context,
|
|
2022
|
+
)
|
|
2023
|
+
transitions: list[WorkflowTransition] = []
|
|
2024
|
+
for index, transition in enumerate(model.transition_log):
|
|
2025
|
+
progress_events = [progress_event] if index == len(model.transition_log) - 1 else []
|
|
2026
|
+
transitions.append(
|
|
2027
|
+
WorkflowTransition(
|
|
2028
|
+
workflow=transition.workflow,
|
|
2029
|
+
from_state=transition.from_state,
|
|
2030
|
+
to_state=transition.to_state,
|
|
2031
|
+
to_category=category_for_state(transition.to_state),
|
|
2032
|
+
trigger=transition.trigger,
|
|
2033
|
+
effects=list(transition.effects),
|
|
2034
|
+
progress_events=progress_events,
|
|
2035
|
+
decision=transition.decision,
|
|
2036
|
+
resume_action=transition.resume_action,
|
|
2037
|
+
)
|
|
2038
|
+
)
|
|
2039
|
+
return WorkflowStateMachineSnapshot(
|
|
2040
|
+
workflow=FIX_WIKI_WORKFLOW,
|
|
2041
|
+
run_id=model.run_id,
|
|
2042
|
+
current_state=model.state,
|
|
2043
|
+
current_category=category_for_state(model.state),
|
|
2044
|
+
transitions=transitions,
|
|
2045
|
+
metadata={"reason": projection.reason.value, "source": "FixWikiMachine"},
|
|
2046
|
+
)
|
|
2047
|
+
|
|
2048
|
+
|
|
2049
|
+
def _transition_effects(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> list[WorkflowEffect]:
|
|
2050
|
+
allowed_kinds = _allowed_effect_kinds_for_fix_wiki_state(
|
|
2051
|
+
category=projection.category,
|
|
2052
|
+
current_state=projection.state.value,
|
|
2053
|
+
)
|
|
2054
|
+
if not allowed_kinds:
|
|
2055
|
+
return []
|
|
2056
|
+
return [effect for effect in facts.machine_effects if effect.kind in allowed_kinds]
|
|
2057
|
+
|
|
2058
|
+
|
|
2059
|
+
def _allowed_effect_kinds_for_fix_wiki_state(
|
|
2060
|
+
*,
|
|
2061
|
+
category: WorkflowStateCategory,
|
|
2062
|
+
current_state: str,
|
|
2063
|
+
) -> set[WorkflowEffectKind]:
|
|
2064
|
+
"""Return effect kinds executable from a concrete fix-wiki StateChart leaf."""
|
|
2065
|
+
|
|
2066
|
+
allowed_kinds = _allowed_effect_kinds_for_category(category)
|
|
2067
|
+
if current_state in {
|
|
2068
|
+
FixWikiState.ENVIRONMENT_PATHS_MISSING.value,
|
|
2069
|
+
FixWikiState.ENVIRONMENT_WIKI_DIR_MISSING.value,
|
|
2070
|
+
FixWikiState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED.value,
|
|
2071
|
+
FixWikiState.LINK_RUN_REQUESTED.value,
|
|
2072
|
+
}:
|
|
2073
|
+
allowed_kinds = allowed_kinds | {WorkflowEffectKind.RUN_SUBWORKFLOW}
|
|
2074
|
+
return allowed_kinds
|
|
2075
|
+
|
|
2076
|
+
|
|
2077
|
+
def _allowed_effect_kinds_for_category(category: WorkflowStateCategory) -> set[WorkflowEffectKind]:
|
|
2078
|
+
match category:
|
|
2079
|
+
case WorkflowStateCategory.WAITING_AGENT:
|
|
2080
|
+
return {WorkflowEffectKind.CALL_SPECIALIST_MODEL, WorkflowEffectKind.RUN_SUBWORKFLOW}
|
|
2081
|
+
case WorkflowStateCategory.WAITING_EXTERNAL:
|
|
2082
|
+
return {WorkflowEffectKind.WAIT_EXTERNAL}
|
|
2083
|
+
case WorkflowStateCategory.WAITING_HUMAN:
|
|
2084
|
+
return {WorkflowEffectKind.ASK_HUMAN}
|
|
2085
|
+
case _:
|
|
2086
|
+
return set()
|
|
2087
|
+
|
|
2088
|
+
|
|
2089
|
+
def _receipt(
|
|
2090
|
+
facts: FixWikiFsmFacts,
|
|
2091
|
+
projection: _FixWikiStateView,
|
|
2092
|
+
progress_state: WorkflowProgressState,
|
|
2093
|
+
snapshot: WorkflowStateMachineSnapshot,
|
|
2094
|
+
*,
|
|
2095
|
+
human_decision_packet: JsonObject | HumanDecisionPacket | None = None,
|
|
2096
|
+
) -> WorkflowReceiptPayload:
|
|
2097
|
+
view_model = build_progress_view_model(progress_state)
|
|
2098
|
+
return WorkflowReceiptPayload(
|
|
2099
|
+
schema=FIX_WIKI_RECEIPT_SCHEMA,
|
|
2100
|
+
workflow=FIX_WIKI_WORKFLOW,
|
|
2101
|
+
run_id=facts.run_id,
|
|
2102
|
+
status=_receipt_status(projection),
|
|
2103
|
+
mutated=_applied_mutation_file_count(facts) > 0,
|
|
2104
|
+
next_action=_receipt_next_action(projection),
|
|
2105
|
+
human_decision_required=projection.status == WorkflowProgressStatus.WAITING_HUMAN,
|
|
2106
|
+
human_decision_packet=human_decision_packet,
|
|
2107
|
+
phase_outcomes=_receipt_phase_outcomes(projection, human_decision_packet=human_decision_packet),
|
|
2108
|
+
artifacts=_receipt_artifacts(facts.artifacts),
|
|
2109
|
+
changed_files=list(facts.changed_files),
|
|
2110
|
+
version_control_safety=facts.version_control_safety,
|
|
2111
|
+
progress_state=progress_state,
|
|
2112
|
+
progress_view_model=view_model,
|
|
2113
|
+
state_machine_snapshot=snapshot,
|
|
2114
|
+
)
|
|
2115
|
+
|
|
2116
|
+
|
|
2117
|
+
def _receipt_phase_outcomes(
|
|
2118
|
+
projection: _FixWikiStateView,
|
|
2119
|
+
*,
|
|
2120
|
+
human_decision_packet: JsonObject | HumanDecisionPacket | None,
|
|
2121
|
+
) -> list[WorkflowPhaseOutcome]:
|
|
2122
|
+
"""Embed the FSM decision in the receipt instead of duplicating blocker fields."""
|
|
2123
|
+
|
|
2124
|
+
if projection.decision is None:
|
|
2125
|
+
return []
|
|
2126
|
+
packet = None
|
|
2127
|
+
if human_decision_packet is not None:
|
|
2128
|
+
packet = HumanDecisionPacket.model_validate(human_decision_packet)
|
|
2129
|
+
return [
|
|
2130
|
+
WorkflowPhaseOutcome(
|
|
2131
|
+
phase=projection.decision.phase,
|
|
2132
|
+
decision_summary=projection.decision.decision_summary(),
|
|
2133
|
+
human_decision_packet=packet,
|
|
2134
|
+
)
|
|
2135
|
+
]
|
|
2136
|
+
|
|
2137
|
+
|
|
2138
|
+
def _applied_mutation_file_count(facts: FixWikiFsmFacts) -> int:
|
|
2139
|
+
if not facts.effective_apply:
|
|
2140
|
+
return 0
|
|
2141
|
+
if facts.vault_changed_file_count > 0:
|
|
2142
|
+
return facts.vault_changed_file_count
|
|
2143
|
+
if facts.total_changed_count > 0:
|
|
2144
|
+
return facts.total_changed_count
|
|
2145
|
+
if facts.written_count > 0:
|
|
2146
|
+
return facts.written_count
|
|
2147
|
+
return len(_non_backup_changed_files(facts.changed_files))
|
|
2148
|
+
|
|
2149
|
+
|
|
2150
|
+
def _applied_written_file_count(facts: FixWikiFsmFacts) -> int:
|
|
2151
|
+
if not facts.effective_apply:
|
|
2152
|
+
return 0
|
|
2153
|
+
if facts.written_count > 0:
|
|
2154
|
+
return facts.written_count
|
|
2155
|
+
if facts.total_changed_count > 0:
|
|
2156
|
+
return facts.total_changed_count
|
|
2157
|
+
if facts.vault_changed_file_count > 0:
|
|
2158
|
+
return facts.vault_changed_file_count
|
|
2159
|
+
return len(_non_backup_changed_files(facts.changed_files))
|
|
2160
|
+
|
|
2161
|
+
|
|
2162
|
+
def _non_backup_changed_files(paths: list[str]) -> list[str]:
|
|
2163
|
+
return [path for path in paths if path.strip() and not path.endswith(".bak")]
|
|
2164
|
+
|
|
2165
|
+
|
|
2166
|
+
def _receipt_status(projection: _FixWikiStateView) -> ReceiptStatus:
|
|
2167
|
+
match projection.status:
|
|
2168
|
+
case WorkflowProgressStatus.COMPLETED:
|
|
2169
|
+
return "completed"
|
|
2170
|
+
case WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
|
|
2171
|
+
return "completed_with_warnings"
|
|
2172
|
+
case WorkflowProgressStatus.WAITING_AGENT:
|
|
2173
|
+
return "waiting_agent"
|
|
2174
|
+
case WorkflowProgressStatus.WAITING_EXTERNAL:
|
|
2175
|
+
return "waiting_external"
|
|
2176
|
+
case WorkflowProgressStatus.WAITING_HUMAN:
|
|
2177
|
+
return "waiting_human"
|
|
2178
|
+
case WorkflowProgressStatus.RUNNING:
|
|
2179
|
+
return "running"
|
|
2180
|
+
case WorkflowProgressStatus.BLOCKED:
|
|
2181
|
+
return "blocked"
|
|
2182
|
+
case WorkflowProgressStatus.FAILED:
|
|
2183
|
+
return "failed"
|
|
2184
|
+
case _:
|
|
2185
|
+
return "blocked"
|
|
2186
|
+
|
|
2187
|
+
|
|
2188
|
+
def _receipt_next_action(projection: _FixWikiStateView) -> str:
|
|
2189
|
+
if projection.status == WorkflowProgressStatus.COMPLETED:
|
|
2190
|
+
return ""
|
|
2191
|
+
return projection.next_action
|
|
2192
|
+
|
|
2193
|
+
|
|
2194
|
+
def _receipt_artifacts(artifacts: JsonObject) -> list[dict[str, str]]:
|
|
2195
|
+
receipt_artifacts: list[dict[str, str]] = []
|
|
2196
|
+
for key, value in artifacts.items():
|
|
2197
|
+
if value is None:
|
|
2198
|
+
continue
|
|
2199
|
+
if isinstance(value, dict):
|
|
2200
|
+
for nested_key, nested_value in value.items():
|
|
2201
|
+
if nested_value is None:
|
|
2202
|
+
continue
|
|
2203
|
+
nested_path = str(nested_value).strip()
|
|
2204
|
+
if nested_path:
|
|
2205
|
+
receipt_artifacts.append({"kind": f"{key}.{nested_key}", "path": nested_path})
|
|
2206
|
+
continue
|
|
2207
|
+
receipt_artifacts.append({"kind": str(key), "path": str(value)})
|
|
2208
|
+
return receipt_artifacts
|
|
2209
|
+
|
|
2210
|
+
|
|
2211
|
+
def _reports(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> WorkflowReports:
|
|
2212
|
+
match projection.reason:
|
|
2213
|
+
case FixWikiReason.COMPLETED:
|
|
2214
|
+
summary = "Corrigi e conferi a Wiki."
|
|
2215
|
+
case FixWikiReason.COMPLETED_WITH_WARNINGS:
|
|
2216
|
+
summary = "Corrigi a Wiki com avisos pendentes."
|
|
2217
|
+
case FixWikiReason.PREVIEW_READY:
|
|
2218
|
+
summary = "Conferi a Wiki; nada foi alterado."
|
|
2219
|
+
case FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
|
|
2220
|
+
summary = "Pausei a etapa do Related Notes porque ela depende de cota externa."
|
|
2221
|
+
case FixWikiReason.WAITING_EXTERNAL:
|
|
2222
|
+
if (
|
|
2223
|
+
facts.external_wait_reason_code == "specialist_model_capacity_unavailable"
|
|
2224
|
+
and facts.requires_llm_rewrite_count > 0
|
|
2225
|
+
):
|
|
2226
|
+
rewrite_label = (
|
|
2227
|
+
"reescrita especializada"
|
|
2228
|
+
if facts.requires_llm_rewrite_count == 1
|
|
2229
|
+
else "reescritas especializadas"
|
|
2230
|
+
)
|
|
2231
|
+
summary = (
|
|
2232
|
+
"Apliquei os reparos seguros e pausei porque "
|
|
2233
|
+
f"{facts.requires_llm_rewrite_count} {rewrite_label} dependem do modelo especializado."
|
|
2234
|
+
)
|
|
2235
|
+
else:
|
|
2236
|
+
summary = "Pausei o workflow porque ele depende de uma condicao externa."
|
|
2237
|
+
case FixWikiReason.STYLE_REWRITE_READY:
|
|
2238
|
+
summary = "Apliquei os reparos seguros e vou continuar pela reescrita especializada."
|
|
2239
|
+
case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
|
|
2240
|
+
summary = "Apliquei os reparos seguros e vou continuar pela curadoria semantica do vocabulario."
|
|
2241
|
+
case (
|
|
2242
|
+
FixWikiReason.WAITING_HUMAN
|
|
2243
|
+
| FixWikiReason.SUBAGENT_PLAN_ATTESTATION_REQUIRED
|
|
2244
|
+
| FixWikiReason.SUBAGENT_PLAN_ATTESTATION_INVALID
|
|
2245
|
+
| FixWikiReason.STYLE_REWRITE_REVIEW_REQUIRED
|
|
2246
|
+
| FixWikiReason.TAXONOMY_DECISION_REQUIRED
|
|
2247
|
+
| FixWikiReason.VOCABULARY_EVAL_NEEDS_REVIEW
|
|
2248
|
+
| FixWikiReason.ATOMICITY_SPLIT_REVIEW_REQUIRED
|
|
2249
|
+
| FixWikiReason.MERGE_REVIEW_REQUIRED
|
|
2250
|
+
):
|
|
2251
|
+
summary = "Preciso de uma escolha sua antes de continuar."
|
|
2252
|
+
case FixWikiReason.ATOMICITY_SPLIT_REQUIRED:
|
|
2253
|
+
summary = "O fix-wiki parou porque ha split de atomicidade pendente."
|
|
2254
|
+
case FixWikiReason.GRAPH_BLOCKED:
|
|
2255
|
+
summary = "A Wiki ainda precisa de reparo de grafo pela rota oficial."
|
|
2256
|
+
case FixWikiReason.RELATED_NOTES_BLOCKED:
|
|
2257
|
+
summary = "O fix-wiki parou porque as Notas Relacionadas ainda precisam ser atualizadas."
|
|
2258
|
+
case FixWikiReason.LINKER_BLOCKED:
|
|
2259
|
+
summary = "O pacote de links ainda esta bloqueado."
|
|
2260
|
+
case FixWikiReason.TAXONOMY_BLOCKED:
|
|
2261
|
+
summary = "A taxonomia exige acao antes de concluir."
|
|
2262
|
+
case FixWikiReason.STYLE_REWRITE_REQUIRED:
|
|
2263
|
+
summary = "Ha reescrita semantica pendente antes de concluir."
|
|
2264
|
+
case FixWikiReason.FAILED:
|
|
2265
|
+
summary = "O fix-wiki falhou antes de concluir."
|
|
2266
|
+
case _:
|
|
2267
|
+
summary = projection.message
|
|
2268
|
+
return WorkflowReports(
|
|
2269
|
+
summary=summary,
|
|
2270
|
+
public_report=_public_report(facts, projection, summary),
|
|
2271
|
+
)
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
def _reports_with_primary_objective_summary(
|
|
2275
|
+
reports: WorkflowReports,
|
|
2276
|
+
*,
|
|
2277
|
+
run_id: str,
|
|
2278
|
+
progress_view_model: WorkflowProgressViewModel,
|
|
2279
|
+
receipt: WorkflowReceiptPayload,
|
|
2280
|
+
diagnostic_context: JsonObject,
|
|
2281
|
+
) -> WorkflowReports:
|
|
2282
|
+
"""Attach the typed validator summary under the shared reports envelope."""
|
|
2283
|
+
|
|
2284
|
+
objective = fix_wiki_primary_objective_summary(
|
|
2285
|
+
JsonObjectAdapter.validate_python(
|
|
2286
|
+
{
|
|
2287
|
+
"schema": FIX_WIKI_SCHEMA,
|
|
2288
|
+
"workflow": FIX_WIKI_WORKFLOW,
|
|
2289
|
+
"run_id": run_id,
|
|
2290
|
+
"progress_view_model": progress_view_model.to_payload(),
|
|
2291
|
+
"receipt": receipt.to_payload(),
|
|
2292
|
+
"diagnostic_context": dict(diagnostic_context),
|
|
2293
|
+
}
|
|
2294
|
+
)
|
|
2295
|
+
)
|
|
2296
|
+
if objective is None:
|
|
2297
|
+
return reports
|
|
2298
|
+
details = dict(reports.details)
|
|
2299
|
+
details["primary_objective_summary"] = objective.to_payload()
|
|
2300
|
+
return reports.model_copy(update={"details": JsonObjectAdapter.validate_python(details)})
|
|
2301
|
+
|
|
2302
|
+
|
|
2303
|
+
def _payload_with_primary_objective_summary(payload: JsonObject) -> JsonObject:
|
|
2304
|
+
"""Refresh the structured validator summary on the final serialized payload."""
|
|
2305
|
+
|
|
2306
|
+
objective = fix_wiki_primary_objective_summary(payload)
|
|
2307
|
+
if objective is None:
|
|
2308
|
+
return payload
|
|
2309
|
+
reports = JsonObjectAdapter.validate_python(payload["reports"] if "reports" in payload else {})
|
|
2310
|
+
details = JsonObjectAdapter.validate_python(reports["details"] if "details" in reports else {})
|
|
2311
|
+
details["primary_objective_summary"] = objective.to_payload()
|
|
2312
|
+
reports["details"] = details
|
|
2313
|
+
payload["reports"] = reports
|
|
2314
|
+
return payload
|
|
2315
|
+
|
|
2316
|
+
|
|
2317
|
+
def _public_report(facts: FixWikiFsmFacts, projection: _FixWikiStateView, summary: str) -> WorkflowPublicReport:
|
|
2318
|
+
"""Human UX projection owned by the same FSM result as the machine state."""
|
|
2319
|
+
|
|
2320
|
+
changed_count = _applied_mutation_file_count(facts)
|
|
2321
|
+
vault_has_changes = changed_count > 0
|
|
2322
|
+
human_decision_required = (
|
|
2323
|
+
projection.status == WorkflowProgressStatus.WAITING_HUMAN
|
|
2324
|
+
or (projection.decision is not None and projection.decision.kind == "ask_human")
|
|
2325
|
+
)
|
|
2326
|
+
can_continue_without_human = projection.status == WorkflowProgressStatus.WAITING_AGENT and projection.can_continue_now
|
|
2327
|
+
|
|
2328
|
+
if can_continue_without_human:
|
|
2329
|
+
headline = (
|
|
2330
|
+
"Apliquei reparos iniciais e vou continuar automaticamente."
|
|
2331
|
+
if vault_has_changes
|
|
2332
|
+
else "Preparei a próxima etapa e vou continuar automaticamente."
|
|
2333
|
+
)
|
|
2334
|
+
elif not facts.requested_apply and not vault_has_changes:
|
|
2335
|
+
headline = "Conferi a Wiki; nada foi alterado."
|
|
2336
|
+
elif vault_has_changes and projection.status == WorkflowProgressStatus.COMPLETED:
|
|
2337
|
+
headline = "Corrigi a Wiki."
|
|
2338
|
+
elif vault_has_changes and projection.status == WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
|
|
2339
|
+
headline = "Corrigi a Wiki; restaram avisos não bloqueantes."
|
|
2340
|
+
elif vault_has_changes:
|
|
2341
|
+
headline = "Apliquei reparos na Wiki, mas ainda falta concluir."
|
|
2342
|
+
else:
|
|
2343
|
+
headline = "Não alterei a Wiki."
|
|
2344
|
+
|
|
2345
|
+
lines: list[str] = [headline]
|
|
2346
|
+
if vault_has_changes:
|
|
2347
|
+
lines.append(f"Alterei {changed_count} arquivo(s) da Wiki nesta etapa.")
|
|
2348
|
+
elif not facts.requested_apply:
|
|
2349
|
+
lines.append("Esta foi uma conferência: nenhum arquivo da Wiki foi alterado.")
|
|
2350
|
+
else:
|
|
2351
|
+
lines.append("Nenhum arquivo da Wiki foi alterado nesta etapa.")
|
|
2352
|
+
|
|
2353
|
+
if can_continue_without_human:
|
|
2354
|
+
lines.append(_public_followup_line(facts, projection))
|
|
2355
|
+
|
|
2356
|
+
blockers = _public_blockers(facts, projection, human_decision_required=human_decision_required)
|
|
2357
|
+
if blockers:
|
|
2358
|
+
lines.append("Ainda falta concluir; " + "; ".join(blockers) + ".")
|
|
2359
|
+
elif projection.status == WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
|
|
2360
|
+
lines.append("A Wiki ficou sem bloqueios técnicos, mas ainda há avisos para revisar.")
|
|
2361
|
+
elif projection.status == WorkflowProgressStatus.COMPLETED:
|
|
2362
|
+
lines.append("Não encontrei bloqueios técnicos restantes.")
|
|
2363
|
+
|
|
2364
|
+
if human_decision_required:
|
|
2365
|
+
question = ""
|
|
2366
|
+
if projection.decision is not None and projection.decision.public_summary:
|
|
2367
|
+
question = projection.decision.public_summary.strip()
|
|
2368
|
+
if question:
|
|
2369
|
+
lines.append(f"Preciso da sua decisão: {question}")
|
|
2370
|
+
else:
|
|
2371
|
+
lines.append(_public_followup_line(facts, projection))
|
|
2372
|
+
elif projection.status == WorkflowProgressStatus.WAITING_EXTERNAL:
|
|
2373
|
+
lines.append(_waiting_external_public_line(facts, projection))
|
|
2374
|
+
elif not can_continue_without_human and projection.status not in {
|
|
2375
|
+
WorkflowProgressStatus.COMPLETED,
|
|
2376
|
+
WorkflowProgressStatus.COMPLETED_WITH_WARNINGS,
|
|
2377
|
+
}:
|
|
2378
|
+
followup_line = _public_followup_line(facts, projection)
|
|
2379
|
+
if followup_line:
|
|
2380
|
+
lines.append(followup_line)
|
|
2381
|
+
|
|
2382
|
+
return WorkflowPublicReport(
|
|
2383
|
+
workflow=FIX_WIKI_WORKFLOW,
|
|
2384
|
+
run_id=facts.run_id,
|
|
2385
|
+
headline=headline,
|
|
2386
|
+
lines=lines,
|
|
2387
|
+
)
|
|
2388
|
+
|
|
2389
|
+
|
|
2390
|
+
def _public_blockers(
|
|
2391
|
+
facts: FixWikiFsmFacts,
|
|
2392
|
+
projection: _FixWikiStateView,
|
|
2393
|
+
*,
|
|
2394
|
+
human_decision_required: bool,
|
|
2395
|
+
) -> list[str]:
|
|
2396
|
+
blockers: list[str] = []
|
|
2397
|
+
match projection.reason:
|
|
2398
|
+
case (
|
|
2399
|
+
FixWikiReason.STYLE_REWRITE_READY
|
|
2400
|
+
| FixWikiReason.STYLE_REWRITE_REQUIRED
|
|
2401
|
+
| FixWikiReason.STYLE_REWRITE_REVIEW_REQUIRED
|
|
2402
|
+
):
|
|
2403
|
+
blockers.append("há nota(s) que precisam de reescrita assistida antes de concluir")
|
|
2404
|
+
case FixWikiReason.VOCABULARY_SEMANTIC_INGESTION_PENDING:
|
|
2405
|
+
blockers.append("a curadoria semântica do vocabulário ainda precisa ser aplicada")
|
|
2406
|
+
case FixWikiReason.GRAPH_BLOCKED | FixWikiReason.GRAPH_REVIEW_REQUIRED:
|
|
2407
|
+
blockers.append("o grafo de links ainda tem referência(s) que precisam ser corrigidas")
|
|
2408
|
+
case FixWikiReason.RELATED_NOTES_BLOCKED | FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES:
|
|
2409
|
+
blockers.append("as Notas Relacionadas ainda precisam ser atualizadas")
|
|
2410
|
+
case FixWikiReason.LINKER_BLOCKED:
|
|
2411
|
+
blockers.append("a atualização de links ainda não pôde ser concluída")
|
|
2412
|
+
case FixWikiReason.TAXONOMY_BLOCKED | FixWikiReason.TAXONOMY_DECISION_REQUIRED:
|
|
2413
|
+
blockers.append("a organização por pastas ainda precisa de revisão")
|
|
2414
|
+
case FixWikiReason.ATOMICITY_SPLIT_REQUIRED | FixWikiReason.ATOMICITY_SPLIT_REVIEW_REQUIRED:
|
|
2415
|
+
blockers.append("há nota(s) que precisam ser divididas antes de concluir")
|
|
2416
|
+
case _:
|
|
2417
|
+
pass
|
|
2418
|
+
if human_decision_required:
|
|
2419
|
+
blockers.append("há uma decisão humana pendente antes de continuar")
|
|
2420
|
+
return blockers
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
def _waiting_external_public_line(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> str:
|
|
2424
|
+
if projection.reason == FixWikiReason.WAITING_EXTERNAL_RELATED_NOTES or facts.related_notes_recovery_state.status:
|
|
2425
|
+
return "As Notas Relacionadas dependem de quota externa antes de retomar com segurança."
|
|
2426
|
+
return _public_followup_line(facts, projection)
|
|
2427
|
+
|
|
2428
|
+
|
|
2429
|
+
def _public_followup_line(facts: FixWikiFsmFacts, projection: _FixWikiStateView) -> str:
|
|
2430
|
+
"""Render a safe next-step sentence without copying technical next_action text."""
|
|
2431
|
+
|
|
2432
|
+
if projection.reason in {
|
|
2433
|
+
FixWikiReason.GRAPH_BLOCKED,
|
|
2434
|
+
FixWikiReason.GRAPH_REVIEW_REQUIRED,
|
|
2435
|
+
FixWikiReason.LINKER_BLOCKED,
|
|
2436
|
+
} and _graph_curator_followup(facts.next_action):
|
|
2437
|
+
return "Retomar a curadoria do grafo pela rota oficial antes de concluir."
|
|
2438
|
+
return public_progress_followup_line(_progress_state(facts, projection))
|
|
2439
|
+
|
|
2440
|
+
|
|
2441
|
+
def _graph_curator_followup(next_action: str) -> bool:
|
|
2442
|
+
"""Recognize graph-curator workflow hints only to choose closed public wording."""
|
|
2443
|
+
|
|
2444
|
+
normalized = next_action.casefold()
|
|
2445
|
+
return any(marker in normalized for marker in ("med-link-graph-curator", "collect-curator-outputs", "curator-batch"))
|
|
2446
|
+
|
|
2447
|
+
|
|
2448
|
+
def _public_report_summary_text(public_report: WorkflowPublicReport) -> str:
|
|
2449
|
+
"""Use the public report lines as the single human-visible summary channel."""
|
|
2450
|
+
|
|
2451
|
+
return public_report.summary_text()
|
|
2452
|
+
|
|
2453
|
+
|
|
2454
|
+
def _diagnostic_context(
|
|
2455
|
+
facts: FixWikiFsmFacts,
|
|
2456
|
+
projection: _FixWikiStateView,
|
|
2457
|
+
) -> JsonObject:
|
|
2458
|
+
"""Build explanatory diagnostics without carrying executable control."""
|
|
2459
|
+
|
|
2460
|
+
context: JsonObject = dict(facts.diagnostic_context)
|
|
2461
|
+
for key in FIX_WIKI_DIAGNOSTIC_PARALLEL_TRUTH_KEYS:
|
|
2462
|
+
if key in context:
|
|
2463
|
+
context.pop(key)
|
|
2464
|
+
apply_context = _optional_json_object_field(context, "apply")
|
|
2465
|
+
context.update(
|
|
2466
|
+
{
|
|
2467
|
+
"schema": "medical-notes-workbench.fix-wiki-fsm-diagnostic-context.v1",
|
|
2468
|
+
"reason": projection.reason.value,
|
|
2469
|
+
"state": projection.state.value,
|
|
2470
|
+
"apply": {
|
|
2471
|
+
**apply_context,
|
|
2472
|
+
"requested_apply": facts.requested_apply,
|
|
2473
|
+
"effective_apply": facts.effective_apply,
|
|
2474
|
+
},
|
|
2475
|
+
"counts": {
|
|
2476
|
+
"total_changed_count": facts.total_changed_count,
|
|
2477
|
+
"vault_changed_file_count": facts.vault_changed_file_count,
|
|
2478
|
+
"written_count": facts.written_count,
|
|
2479
|
+
"warning_count": facts.warning_count,
|
|
2480
|
+
"requires_llm_rewrite_count": facts.requires_llm_rewrite_count,
|
|
2481
|
+
"graph_error_count": facts.graph_error_count,
|
|
2482
|
+
"graph_blocker_count": facts.graph_blocker_count,
|
|
2483
|
+
},
|
|
2484
|
+
"final_validation": dict(facts.final_validation),
|
|
2485
|
+
"linker_blocked": facts.linker_blocked,
|
|
2486
|
+
"related_notes_blocked": facts.related_notes_blocked,
|
|
2487
|
+
"atomicity_split_required": facts.atomicity_split_required,
|
|
2488
|
+
"graph_review_required": facts.graph_review_required,
|
|
2489
|
+
"taxonomy_action_required": facts.taxonomy_action_required,
|
|
2490
|
+
}
|
|
2491
|
+
)
|
|
2492
|
+
if facts.failed_reason_code:
|
|
2493
|
+
context["root_cause"] = facts.failed_reason_code
|
|
2494
|
+
if facts.related_notes_recovery_state:
|
|
2495
|
+
context["related_notes_recovery_state"] = facts.related_notes_recovery_state.operation_payload
|
|
2496
|
+
if facts.external_wait_payload:
|
|
2497
|
+
context["external_wait_payload"] = dict(facts.external_wait_payload)
|
|
2498
|
+
return diagnostic_context_evidence_only(context)
|
|
2499
|
+
|
|
2500
|
+
|
|
2501
|
+
def _agent_directive(
|
|
2502
|
+
facts: FixWikiFsmFacts,
|
|
2503
|
+
projection: _FixWikiStateView,
|
|
2504
|
+
*,
|
|
2505
|
+
progress_view_model: WorkflowProgressViewModel,
|
|
2506
|
+
user_visible_summary: str,
|
|
2507
|
+
) -> JsonObject:
|
|
2508
|
+
"""Build the root executable agent contract directly from FSM state."""
|
|
2509
|
+
|
|
2510
|
+
typed = agent_directive_from_progress_view_model(
|
|
2511
|
+
progress_view_model,
|
|
2512
|
+
schema=MEDNOTES_AGENT_DIRECTIVE_SCHEMA,
|
|
2513
|
+
reason=projection.reason.value,
|
|
2514
|
+
effects=_transition_effects(facts, projection),
|
|
2515
|
+
blockers=_blocked_by_for_guidance(projection),
|
|
2516
|
+
resume=projection.resume_action,
|
|
2517
|
+
report_requires=["primary_objective", "mutations", "graph", "related_notes"],
|
|
2518
|
+
summary=user_visible_summary,
|
|
2519
|
+
instructions=_plain_agent_directive_instructions(_fsm_directive_instructions(facts, projection)),
|
|
2520
|
+
)
|
|
2521
|
+
return JsonObjectAdapter.validate_python(typed.to_payload())
|
|
2522
|
+
|
|
2523
|
+
|
|
2524
|
+
def _problem_diagnostic_context(context: JsonObject, projection: _FixWikiStateView) -> JsonObject:
|
|
2525
|
+
vocabulary_bootstrap = _FixWikiVocabularyBootstrapDiagnostic.model_validate(
|
|
2526
|
+
context["vocabulary_bootstrap"]
|
|
2527
|
+
if "vocabulary_bootstrap" in context and isinstance(context["vocabulary_bootstrap"], dict)
|
|
2528
|
+
else {}
|
|
2529
|
+
)
|
|
2530
|
+
explicit_vocabulary_reset = vocabulary_bootstrap.trigger == "explicit_vocabulary_reset"
|
|
2531
|
+
if projection.status == WorkflowProgressStatus.COMPLETED:
|
|
2532
|
+
if explicit_vocabulary_reset:
|
|
2533
|
+
return JsonObjectAdapter.validate_python(context)
|
|
2534
|
+
return {}
|
|
2535
|
+
return JsonObjectAdapter.validate_python(context)
|
|
2536
|
+
|
|
2537
|
+
|
|
2538
|
+
def _blocked_by_for_guidance(projection: _FixWikiStateView) -> list[str]:
|
|
2539
|
+
if projection.status not in {
|
|
2540
|
+
WorkflowProgressStatus.BLOCKED,
|
|
2541
|
+
WorkflowProgressStatus.FAILED,
|
|
2542
|
+
WorkflowProgressStatus.WAITING_EXTERNAL,
|
|
2543
|
+
WorkflowProgressStatus.WAITING_HUMAN,
|
|
2544
|
+
}:
|
|
2545
|
+
return []
|
|
2546
|
+
if projection.decision is not None:
|
|
2547
|
+
return [projection.decision.reason_code]
|
|
2548
|
+
return [projection.trigger or projection.reason.value]
|
|
2549
|
+
|
|
2550
|
+
|
|
2551
|
+
def _plain_agent_directive_instructions(lines: list[str]) -> list[str]:
|
|
2552
|
+
cleaned: list[str] = []
|
|
2553
|
+
for line in lines:
|
|
2554
|
+
text = line.strip()
|
|
2555
|
+
prefix = "agent_instruction:"
|
|
2556
|
+
if text.casefold().startswith(prefix):
|
|
2557
|
+
text = text[len(prefix):].strip()
|
|
2558
|
+
if text:
|
|
2559
|
+
cleaned.append(text)
|
|
2560
|
+
return cleaned
|
|
2561
|
+
|
|
2562
|
+
|
|
2563
|
+
def fix_wiki_cli_exit_code(payload: JsonObject) -> int:
|
|
2564
|
+
progress = _FixWikiPayloadProgressView.model_validate(
|
|
2565
|
+
_json_object_subset(payload, "progress_view_model", ("status",))
|
|
2566
|
+
)
|
|
2567
|
+
status = progress.status
|
|
2568
|
+
match status:
|
|
2569
|
+
case "completed" | "completed_with_warnings":
|
|
2570
|
+
return 0
|
|
2571
|
+
case "waiting_agent" | "waiting_external" | "waiting_human" | "blocked":
|
|
2572
|
+
return 3
|
|
2573
|
+
case "failed":
|
|
2574
|
+
return 5
|
|
2575
|
+
case _:
|
|
2576
|
+
return 1
|
|
2577
|
+
|
|
2578
|
+
|
|
2579
|
+
def assert_fix_wiki_fsm_payload(payload: JsonObject) -> None:
|
|
2580
|
+
forbidden_root_keys = set(payload) & FIX_WIKI_FORBIDDEN_ROOT_KEYS
|
|
2581
|
+
if forbidden_root_keys:
|
|
2582
|
+
raise ValueError(f"fix-wiki FSM payload contains noncanonical root fields: {sorted(forbidden_root_keys)}")
|
|
2583
|
+
required_root_keys = FIX_WIKI_ALLOWED_ROOT_KEYS - {"diagnostic_context"}
|
|
2584
|
+
missing_keys = required_root_keys - set(payload)
|
|
2585
|
+
if missing_keys:
|
|
2586
|
+
raise ValueError(f"fix-wiki FSM payload missing canonical root fields: {sorted(missing_keys)}")
|
|
2587
|
+
unexpected_keys = set(payload) - FIX_WIKI_ALLOWED_ROOT_KEYS
|
|
2588
|
+
if unexpected_keys:
|
|
2589
|
+
raise ValueError(f"fix-wiki FSM payload contains unexpected root fields: {sorted(unexpected_keys)}")
|
|
2590
|
+
diagnostic_context = payload["diagnostic_context"] if "diagnostic_context" in payload else {}
|
|
2591
|
+
assert_diagnostic_context_evidence_only(diagnostic_context)
|
|
2592
|
+
fields = _fix_wiki_payload_fields(payload)
|
|
2593
|
+
reports_model = WorkflowReports.model_validate(payload["reports"])
|
|
2594
|
+
if fields.progress_view_model.status != fields.state_machine_snapshot.current_category:
|
|
2595
|
+
raise ValueError("fix-wiki FSM status must match state_machine_snapshot category")
|
|
2596
|
+
if fields.receipt.status != fields.progress_view_model.status:
|
|
2597
|
+
raise ValueError("fix-wiki FSM receipt status must match progress view status")
|
|
2598
|
+
snapshot = WorkflowStateMachineSnapshot.model_validate(payload["state_machine_snapshot"])
|
|
2599
|
+
progress_view_model = WorkflowProgressViewModel.model_validate(payload["progress_view_model"])
|
|
2600
|
+
assert_public_report_matches_progress(
|
|
2601
|
+
reports_model.public_report,
|
|
2602
|
+
workflow=FIX_WIKI_WORKFLOW,
|
|
2603
|
+
run_id=str(payload["run_id"]),
|
|
2604
|
+
progress_view_model=progress_view_model,
|
|
2605
|
+
label="fix-wiki FSM",
|
|
2606
|
+
)
|
|
2607
|
+
_assert_fix_wiki_machine_snapshot(snapshot)
|
|
2608
|
+
_assert_fix_wiki_agent_directive_matches_snapshot(payload, snapshot, progress_view_model)
|
|
2609
|
+
diagnostic_context = payload["diagnostic_context"] if "diagnostic_context" in payload else None
|
|
2610
|
+
if isinstance(diagnostic_context, dict) and "agent_directive" in diagnostic_context:
|
|
2611
|
+
raise ValueError("fix-wiki FSM diagnostic_context must not contain agent_directive")
|
|
2612
|
+
if isinstance(diagnostic_context, dict):
|
|
2613
|
+
_assert_fix_wiki_diagnostic_context_is_non_operational(diagnostic_context)
|
|
2614
|
+
|
|
2615
|
+
|
|
2616
|
+
def _assert_fix_wiki_diagnostic_context_is_non_operational(diagnostic_context: JsonObject) -> None:
|
|
2617
|
+
"""Keep diagnostics as evidence only; executable work belongs in agent_directive."""
|
|
2618
|
+
|
|
2619
|
+
for key in FIX_WIKI_DIAGNOSTIC_PARALLEL_TRUTH_KEYS:
|
|
2620
|
+
if key in diagnostic_context:
|
|
2621
|
+
raise ValueError(f"fix-wiki FSM diagnostic_context contains parallel truth field: {key}")
|
|
2622
|
+
for key in diagnostic_context:
|
|
2623
|
+
if str(key).startswith("human_decision"):
|
|
2624
|
+
raise ValueError(f"fix-wiki FSM diagnostic_context contains parallel truth field: {key}")
|
|
2625
|
+
for plan_key in ("orchestration_plan", "continuation_plan"):
|
|
2626
|
+
plan = diagnostic_context[plan_key] if plan_key in diagnostic_context else None
|
|
2627
|
+
if not isinstance(plan, dict):
|
|
2628
|
+
continue
|
|
2629
|
+
plan_payload = JsonObjectAdapter.validate_python(plan)
|
|
2630
|
+
operational_keys = sorted(set(plan_payload) & FIX_WIKI_DIAGNOSTIC_OPERATIONAL_PLAN_KEYS)
|
|
2631
|
+
if operational_keys:
|
|
2632
|
+
raise ValueError(
|
|
2633
|
+
f"fix-wiki FSM diagnostic_context.{plan_key} contains operational fields: {operational_keys}"
|
|
2634
|
+
)
|
|
2635
|
+
|
|
2636
|
+
|
|
2637
|
+
def _assert_fix_wiki_machine_snapshot(snapshot: WorkflowStateMachineSnapshot) -> None:
|
|
2638
|
+
if snapshot.workflow != FIX_WIKI_WORKFLOW:
|
|
2639
|
+
raise ValueError("fix-wiki FSM snapshot has invalid workflow")
|
|
2640
|
+
if snapshot.current_category != category_for_state(snapshot.current_state):
|
|
2641
|
+
raise ValueError("fix-wiki FSM snapshot category does not match StateChart state")
|
|
2642
|
+
edges = _fix_wiki_machine_edges()
|
|
2643
|
+
for transition in snapshot.transitions:
|
|
2644
|
+
if transition.to_category != category_for_state(transition.to_state):
|
|
2645
|
+
raise ValueError("fix-wiki FSM transition category does not match StateChart state")
|
|
2646
|
+
for effect in transition.effects:
|
|
2647
|
+
if effect.origin_state != transition.to_state:
|
|
2648
|
+
raise ValueError("fix-wiki FSM transition effect origin_state must match transition target")
|
|
2649
|
+
if effect.kind == WorkflowEffectKind.ASK_HUMAN and transition.to_category != WorkflowStateCategory.WAITING_HUMAN:
|
|
2650
|
+
raise ValueError("fix-wiki ask_human effects are only allowed for waiting_human states")
|
|
2651
|
+
if effect.kind == WorkflowEffectKind.WAIT_EXTERNAL and transition.to_category != WorkflowStateCategory.WAITING_EXTERNAL:
|
|
2652
|
+
raise ValueError("fix-wiki wait_external effects are only allowed for waiting_external states")
|
|
2653
|
+
if (
|
|
2654
|
+
effect.kind == WorkflowEffectKind.CALL_SPECIALIST_MODEL
|
|
2655
|
+
and transition.to_category != WorkflowStateCategory.WAITING_AGENT
|
|
2656
|
+
):
|
|
2657
|
+
raise ValueError("fix-wiki specialist effects are only allowed for waiting_agent states")
|
|
2658
|
+
edge = (transition.trigger, transition.from_state, transition.to_state)
|
|
2659
|
+
if edge not in edges:
|
|
2660
|
+
raise ValueError(f"unauthorized FSM transition: {edge}")
|
|
2661
|
+
|
|
2662
|
+
|
|
2663
|
+
def _assert_fix_wiki_agent_directive_matches_snapshot(
|
|
2664
|
+
payload: JsonObject,
|
|
2665
|
+
snapshot: WorkflowStateMachineSnapshot,
|
|
2666
|
+
progress_view_model: WorkflowProgressViewModel,
|
|
2667
|
+
) -> None:
|
|
2668
|
+
"""Keep the public agent route anchored to the current StateChart leaf."""
|
|
2669
|
+
|
|
2670
|
+
try:
|
|
2671
|
+
directive = AgentDirective.model_validate(_optional_json_object_field(payload, "agent_directive"))
|
|
2672
|
+
except PydanticValidationError as exc:
|
|
2673
|
+
raise ValueError("fix-wiki FSM payload invalid: agent_directive") from exc
|
|
2674
|
+
if directive.workflow != FIX_WIKI_WORKFLOW:
|
|
2675
|
+
raise ValueError("fix-wiki FSM agent_directive workflow must match workflow")
|
|
2676
|
+
if directive.schema_ != MEDNOTES_AGENT_DIRECTIVE_SCHEMA:
|
|
2677
|
+
raise ValueError("fix-wiki FSM agent_directive schema must match public MedNotes contract")
|
|
2678
|
+
category = WorkflowStateCategory(snapshot.current_category)
|
|
2679
|
+
allowed_effect_kinds = _allowed_effect_kinds_for_fix_wiki_state(
|
|
2680
|
+
category=category,
|
|
2681
|
+
current_state=snapshot.current_state,
|
|
2682
|
+
)
|
|
2683
|
+
assert_agent_directive_matches_progress(
|
|
2684
|
+
directive,
|
|
2685
|
+
workflow=FIX_WIKI_WORKFLOW,
|
|
2686
|
+
run_id=str(payload["run_id"]),
|
|
2687
|
+
progress_view_model=progress_view_model,
|
|
2688
|
+
snapshot=snapshot,
|
|
2689
|
+
allowed_effect_kinds=allowed_effect_kinds,
|
|
2690
|
+
label="fix-wiki FSM",
|
|
2691
|
+
)
|
|
2692
|
+
|
|
2693
|
+
|
|
2694
|
+
def _fix_wiki_machine_edges() -> set[tuple[str, str, str]]:
|
|
2695
|
+
from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_machine import FixWikiMachine
|
|
2696
|
+
|
|
2697
|
+
edges: set[tuple[str, str, str]] = set()
|
|
2698
|
+
for event in FixWikiMachine.events:
|
|
2699
|
+
for transition in event._transitions:
|
|
2700
|
+
for target in transition._targets:
|
|
2701
|
+
edges.add((event.id, str(transition.source.value), str(target.value)))
|
|
2702
|
+
return edges
|
|
2703
|
+
|
|
2704
|
+
|
|
2705
|
+
def _fix_wiki_payload_fields(payload: JsonObject) -> _FixWikiPayloadFields:
|
|
2706
|
+
raw_fields: JsonObject = {
|
|
2707
|
+
"workflow": payload["workflow"],
|
|
2708
|
+
"progress_view_model": _json_object_subset(payload, "progress_view_model", ("status",)),
|
|
2709
|
+
"state_machine_snapshot": _json_object_subset(payload, "state_machine_snapshot", ("current_category",)),
|
|
2710
|
+
"receipt": _json_object_subset(payload, "receipt", ("status",)),
|
|
2711
|
+
}
|
|
2712
|
+
try:
|
|
2713
|
+
return _FixWikiPayloadFields.model_validate(raw_fields)
|
|
2714
|
+
except PydanticValidationError as exc:
|
|
2715
|
+
first = exc.errors()[0] if exc.errors() else {}
|
|
2716
|
+
loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
|
|
2717
|
+
msg = str(first.get("msg") or str(exc))
|
|
2718
|
+
raise ValueError(f"fix-wiki FSM payload invalid: {loc}: {msg}") from exc
|
|
2719
|
+
|
|
2720
|
+
|
|
2721
|
+
def _json_object_subset(payload: JsonObject, field_name: str, keys: tuple[str, ...]) -> JsonObject:
|
|
2722
|
+
try:
|
|
2723
|
+
source = JsonObjectAdapter.validate_python(payload[field_name])
|
|
2724
|
+
except PydanticValidationError as exc:
|
|
2725
|
+
raise ValueError(f"fix-wiki FSM payload invalid: {field_name} must be an object") from exc
|
|
2726
|
+
return {key: source[key] for key in keys if key in source}
|
|
2727
|
+
|
|
2728
|
+
|
|
2729
|
+
def _optional_json_object_field(payload: JsonObject, field_name: str) -> JsonObject:
|
|
2730
|
+
if field_name not in payload:
|
|
2731
|
+
return {}
|
|
2732
|
+
try:
|
|
2733
|
+
return JsonObjectAdapter.validate_python(payload[field_name])
|
|
2734
|
+
except PydanticValidationError as exc:
|
|
2735
|
+
raise ValueError(f"fix-wiki FSM payload invalid: {field_name} must be an object") from exc
|
|
2736
|
+
|
|
2737
|
+
|
|
2738
|
+
def _optional_json_object_subset(payload: JsonObject, field_name: str, keys: tuple[str, ...]) -> JsonObject:
|
|
2739
|
+
if field_name not in payload:
|
|
2740
|
+
return {}
|
|
2741
|
+
return _json_object_subset(payload, field_name, keys)
|
|
2742
|
+
|
|
2743
|
+
|
|
2744
|
+
def _related_current(recovery_state: RelatedNotesRecoveryState) -> int:
|
|
2745
|
+
return recovery_state.fresh_record_count or recovery_state.partial_record_count
|
|
2746
|
+
|
|
2747
|
+
|
|
2748
|
+
def _related_total(recovery_state: RelatedNotesRecoveryState) -> int:
|
|
2749
|
+
return recovery_state.total_note_count
|
|
2750
|
+
|
|
2751
|
+
|
|
2752
|
+
def _related_remaining(recovery_state: RelatedNotesRecoveryState, *, current: int, total: int) -> int:
|
|
2753
|
+
remaining = recovery_state.remaining_count
|
|
2754
|
+
if remaining:
|
|
2755
|
+
return min(remaining, total) if total else remaining
|
|
2756
|
+
return max(0, total - current)
|
|
2757
|
+
|
|
2758
|
+
|
|
2759
|
+
def _blocked_item_count(facts: FixWikiFsmFacts) -> int:
|
|
2760
|
+
return (
|
|
2761
|
+
facts.graph_blocker_count
|
|
2762
|
+
+ int(facts.linker_blocked)
|
|
2763
|
+
+ int(facts.related_notes_blocked)
|
|
2764
|
+
+ int(facts.taxonomy_action_required)
|
|
2765
|
+
+ int(facts.atomicity_split_required)
|
|
2766
|
+
+ int(facts.graph_review_required)
|
|
2767
|
+
+ facts.requires_llm_rewrite_count
|
|
2768
|
+
)
|