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,1592 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, StrictStr, field_validator, model_validator
|
|
8
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
9
|
+
from pydantic.json_schema import SkipJsonSchema
|
|
10
|
+
|
|
11
|
+
from mednotes.domains.wiki.contracts.agent_report import (
|
|
12
|
+
ProcessChatsCoverageStatus,
|
|
13
|
+
ProcessChatsLinkerStatus,
|
|
14
|
+
ProcessChatsNotesStatus,
|
|
15
|
+
ProcessChatsObjectiveStatus,
|
|
16
|
+
ProcessChatsPrimaryObjectiveSummary,
|
|
17
|
+
ProcessChatsRawStatus,
|
|
18
|
+
)
|
|
19
|
+
from mednotes.domains.wiki.contracts.publish import PublishReceipt
|
|
20
|
+
from mednotes.domains.wiki.contracts.workflow_blockers import BlockerRegistryError, blocker_entry
|
|
21
|
+
from mednotes.domains.wiki.contracts.workflow_outcomes import (
|
|
22
|
+
DecisionEvidence,
|
|
23
|
+
RejectedAutomation,
|
|
24
|
+
WorkflowDecision,
|
|
25
|
+
WorkflowDecisionKind,
|
|
26
|
+
)
|
|
27
|
+
from mednotes.domains.wiki.flows.process_chats.process_chats_machine import (
|
|
28
|
+
ProcessChatsBoundaryEvent,
|
|
29
|
+
ProcessChatsMachine,
|
|
30
|
+
ProcessChatsPublishRuntimeObservation,
|
|
31
|
+
)
|
|
32
|
+
from mednotes.domains.wiki.flows.process_chats.process_chats_machine import (
|
|
33
|
+
ProcessChatsState as MachineProcessChatsState,
|
|
34
|
+
)
|
|
35
|
+
from mednotes.kernel.agent_directive import (
|
|
36
|
+
AgentDirective,
|
|
37
|
+
agent_directive_from_progress_view_model,
|
|
38
|
+
assert_agent_directive_matches_progress,
|
|
39
|
+
)
|
|
40
|
+
from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
|
|
41
|
+
from mednotes.kernel.effects import WorkflowEffect, WorkflowEffectKind, WorkflowEffectResult
|
|
42
|
+
from mednotes.kernel.fsm_event import WorkflowEventLike
|
|
43
|
+
from mednotes.kernel.fsm_model import WorkflowModel
|
|
44
|
+
from mednotes.kernel.fsm_transition_result import WorkflowTransitionResult
|
|
45
|
+
from mednotes.kernel.progress import (
|
|
46
|
+
WorkflowProgressCounts,
|
|
47
|
+
WorkflowProgressEvent,
|
|
48
|
+
WorkflowProgressEventType,
|
|
49
|
+
WorkflowProgressState,
|
|
50
|
+
WorkflowProgressStatus,
|
|
51
|
+
WorkflowProgressViewModel,
|
|
52
|
+
build_progress_view_model,
|
|
53
|
+
progress_state_from_view_model,
|
|
54
|
+
)
|
|
55
|
+
from mednotes.kernel.public_report import (
|
|
56
|
+
WorkflowPublicReport,
|
|
57
|
+
WorkflowReports,
|
|
58
|
+
assert_public_report_matches_progress,
|
|
59
|
+
public_progress_followup_line,
|
|
60
|
+
)
|
|
61
|
+
from mednotes.kernel.state_machine import (
|
|
62
|
+
WorkflowStateCategory,
|
|
63
|
+
WorkflowStateMachineSnapshot,
|
|
64
|
+
WorkflowTransition,
|
|
65
|
+
send_workflow_event,
|
|
66
|
+
)
|
|
67
|
+
from mednotes.kernel.workflow import (
|
|
68
|
+
HumanDecisionPacket,
|
|
69
|
+
ReceiptStatus,
|
|
70
|
+
VersionControlSafety,
|
|
71
|
+
WorkflowReceiptPayload,
|
|
72
|
+
assert_diagnostic_context_evidence_only,
|
|
73
|
+
diagnostic_context_evidence_only,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
PROCESS_CHATS_WORKFLOW: Literal["/mednotes:process-chats"] = "/mednotes:process-chats"
|
|
77
|
+
PROCESS_CHATS_SCHEMA = "medical-notes-workbench.process-chats-fsm-result.v1"
|
|
78
|
+
PROCESS_CHATS_RECEIPT_SCHEMA = "medical-notes-workbench.process-chats-receipt.v1"
|
|
79
|
+
MEDNOTES_AGENT_DIRECTIVE_SCHEMA = "medical-notes-workbench.agent-directive.v1"
|
|
80
|
+
PROCESS_CHATS_AGENT_DIRECTIVE_FIELD = "agent_directive"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
PROCESS_CHATS_ALLOWED_ROOT_KEYS = frozenset(
|
|
84
|
+
{
|
|
85
|
+
"schema",
|
|
86
|
+
"workflow",
|
|
87
|
+
"run_id",
|
|
88
|
+
"state_machine_snapshot",
|
|
89
|
+
"progress_view_model",
|
|
90
|
+
"decision",
|
|
91
|
+
"human_decision_packet",
|
|
92
|
+
"receipt",
|
|
93
|
+
"reports",
|
|
94
|
+
"agent_directive",
|
|
95
|
+
"artifacts",
|
|
96
|
+
"version_control_safety",
|
|
97
|
+
"diagnostic_context",
|
|
98
|
+
"error_context",
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
PROCESS_CHATS_FORBIDDEN_ROOT_KEYS = frozenset(
|
|
102
|
+
{
|
|
103
|
+
"status",
|
|
104
|
+
"phase",
|
|
105
|
+
"blocked_reason",
|
|
106
|
+
"next_action",
|
|
107
|
+
"required_inputs",
|
|
108
|
+
"human_decision_required",
|
|
109
|
+
"dry_run",
|
|
110
|
+
"dry_run_receipt",
|
|
111
|
+
"created",
|
|
112
|
+
"created_count",
|
|
113
|
+
"raw_updates",
|
|
114
|
+
"processed_raw_count",
|
|
115
|
+
"publish_receipt",
|
|
116
|
+
"planned_batches",
|
|
117
|
+
"linker",
|
|
118
|
+
"linker_applied",
|
|
119
|
+
"linker_skipped_reason",
|
|
120
|
+
"link_trigger_context_path",
|
|
121
|
+
"linker_trigger_context_path",
|
|
122
|
+
"linker_diagnosis_path",
|
|
123
|
+
"linker_receipt_path",
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
_PHASE_BY_STATE = {
|
|
128
|
+
MachineProcessChatsState.ENVIRONMENT_CHECKING.value: "environment",
|
|
129
|
+
MachineProcessChatsState.ENVIRONMENT_PATHS_MISSING.value: "environment",
|
|
130
|
+
MachineProcessChatsState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED.value: "environment",
|
|
131
|
+
MachineProcessChatsState.BACKLOG_NO_PENDING_RAW_CHATS.value: "backlog",
|
|
132
|
+
MachineProcessChatsState.BACKLOG_NO_TRIAGED_RAW_CHATS.value: "backlog",
|
|
133
|
+
MachineProcessChatsState.BACKLOG_TRIAGED_RAW_CHATS_READY.value: "backlog",
|
|
134
|
+
MachineProcessChatsState.VAULT_GUARD_DECISION_REQUIRED.value: "vault_guard",
|
|
135
|
+
MachineProcessChatsState.VAULT_GUARD_REJECTED.value: "vault_guard",
|
|
136
|
+
MachineProcessChatsState.TRIAGE_PLANNING.value: "triage",
|
|
137
|
+
MachineProcessChatsState.ARCHITECT_WORK_REQUESTED.value: "architect",
|
|
138
|
+
MachineProcessChatsState.ARCHITECT_AWAITING_SPECIALIST_CAPACITY.value: "architect",
|
|
139
|
+
MachineProcessChatsState.ARCHITECT_REVIEWING_OUTPUT.value: "architect",
|
|
140
|
+
MachineProcessChatsState.SUBAGENT_PLAN_ATTESTATION_REQUIRED.value: "subagent_plan_attestation",
|
|
141
|
+
MachineProcessChatsState.SUBAGENT_PLAN_ATTESTATION_INVALID.value: "subagent_plan_attestation",
|
|
142
|
+
MachineProcessChatsState.NOTE_VALIDATION_RUNNING.value: "note_validation",
|
|
143
|
+
MachineProcessChatsState.NOTE_VALIDATION_COVERAGE_GAP.value: "note_validation",
|
|
144
|
+
MachineProcessChatsState.NOTE_VALIDATION_MANIFEST_MISMATCH.value: "note_validation",
|
|
145
|
+
MachineProcessChatsState.NOTE_VALIDATION_CONTENT_INVALID.value: "note_validation",
|
|
146
|
+
MachineProcessChatsState.STAGING_MANIFEST_READY.value: "staging",
|
|
147
|
+
MachineProcessChatsState.PUBLISH_AWAITING_CONFIRMATION.value: "publish_preview",
|
|
148
|
+
MachineProcessChatsState.PUBLISH_CANCELLED_BY_HUMAN.value: "publish_preview",
|
|
149
|
+
MachineProcessChatsState.PUBLISH_APPLY_REQUESTED.value: "publish_apply",
|
|
150
|
+
MachineProcessChatsState.PUBLISH_PAUSED_FOR_QUOTA.value: "publish_apply",
|
|
151
|
+
MachineProcessChatsState.PUBLISH_DRY_RUN_RECEIPT_REQUIRED.value: "publish_apply",
|
|
152
|
+
MachineProcessChatsState.PUBLISH_STALE_RECEIPT.value: "publish_apply",
|
|
153
|
+
MachineProcessChatsState.PUBLISH_DUPLICATE_TARGET.value: "publish_apply",
|
|
154
|
+
MachineProcessChatsState.PUBLISH_PROVENANCE_GAP.value: "publish_apply",
|
|
155
|
+
MachineProcessChatsState.PUBLISH_RECEIPT_INVALID.value: "publish_apply",
|
|
156
|
+
MachineProcessChatsState.LINK_RUN_REQUESTED.value: "link_package",
|
|
157
|
+
MachineProcessChatsState.CONTRACT_GAP_MISSING_NEXT_ACTION.value: "contract_gap",
|
|
158
|
+
MachineProcessChatsState.CONTRACT_GAP_MISSING_ERROR_CONTEXT.value: "contract_gap",
|
|
159
|
+
MachineProcessChatsState.AGENT_TOOL_CONTRACT_VIOLATION.value: "agent_tool_contract",
|
|
160
|
+
MachineProcessChatsState.ROLLBACK_RECORDED.value: "rollback",
|
|
161
|
+
MachineProcessChatsState.PUBLISHED.value: "publish_apply",
|
|
162
|
+
MachineProcessChatsState.COMPLETED_WITH_LINK_BLOCKERS.value: "link_package",
|
|
163
|
+
MachineProcessChatsState.TERMINAL_FAILURE_RECORDED.value: "publish_failed",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ProcessChatsOutcomeReason(StrEnum):
|
|
168
|
+
NO_PENDING = "no_pending"
|
|
169
|
+
TRIAGED_READY = "triaged_raw_chats_ready"
|
|
170
|
+
READY_TO_PUBLISH = "ready_to_publish"
|
|
171
|
+
PUBLISHED = "published"
|
|
172
|
+
LINKER_BLOCKED = "process_chats_linker_blocked"
|
|
173
|
+
RECOVERABLE_BLOCKED = "recoverable_blocked"
|
|
174
|
+
WAITING_HUMAN = "waiting_human"
|
|
175
|
+
BLOCKED = "blocked"
|
|
176
|
+
FAILED = "failed"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class ProcessChatsBatchState(ContractModel):
|
|
180
|
+
batch_id: str = ""
|
|
181
|
+
run_id: str = ""
|
|
182
|
+
note_plan_hash: str = ""
|
|
183
|
+
coverage_hash: str = ""
|
|
184
|
+
source_artifact_hash: str = ""
|
|
185
|
+
raw_file: str = ""
|
|
186
|
+
raw_files: str = ""
|
|
187
|
+
coverage_path: str = ""
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class ProcessChatsDryRunReceipt(ContractModel):
|
|
191
|
+
path: str = ""
|
|
192
|
+
expires_at: int = Field(default=0, ge=0, strict=True)
|
|
193
|
+
manifest_hash: str = ""
|
|
194
|
+
dry_run_options_hash: str = ""
|
|
195
|
+
batch_state: list[ProcessChatsBatchState] = Field(default_factory=list)
|
|
196
|
+
|
|
197
|
+
@field_validator("expires_at", mode="before")
|
|
198
|
+
@classmethod
|
|
199
|
+
def _normalize_expires_at(cls, value: object) -> int:
|
|
200
|
+
if value in (None, ""):
|
|
201
|
+
return 0
|
|
202
|
+
if isinstance(value, bool):
|
|
203
|
+
raise ValueError("expires_at must be an epoch timestamp")
|
|
204
|
+
if isinstance(value, int):
|
|
205
|
+
return value
|
|
206
|
+
if isinstance(value, str):
|
|
207
|
+
text = value.strip()
|
|
208
|
+
if text.isdigit():
|
|
209
|
+
return int(text)
|
|
210
|
+
try:
|
|
211
|
+
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
212
|
+
except ValueError as exc:
|
|
213
|
+
raise ValueError("expires_at must be an epoch timestamp or ISO datetime") from exc
|
|
214
|
+
if parsed.tzinfo is None:
|
|
215
|
+
parsed = parsed.replace(tzinfo=UTC)
|
|
216
|
+
return int(parsed.timestamp())
|
|
217
|
+
raise ValueError("expires_at must be an epoch timestamp")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TaxonomyCanonicalizationStep(ContractModel):
|
|
221
|
+
from_taxonomy: str = Field(default="", alias="from")
|
|
222
|
+
to: str = ""
|
|
223
|
+
under: str = ""
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class ProcessChatsDecisionSummary(ContractModel):
|
|
227
|
+
kind: WorkflowDecisionKind
|
|
228
|
+
phase: str = ""
|
|
229
|
+
reason_code: str = ""
|
|
230
|
+
public_summary: str = ""
|
|
231
|
+
developer_summary: str = ""
|
|
232
|
+
rejected_automations: list[RejectedAutomation] = Field(default_factory=list)
|
|
233
|
+
evidence: list[DecisionEvidence] = Field(default_factory=list)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class ProcessChatsArtifactReport(ContractModel):
|
|
237
|
+
schema_id: str = Field(default="", alias="schema")
|
|
238
|
+
scope: str = ""
|
|
239
|
+
required: bool = Field(default=False, strict=True)
|
|
240
|
+
manifest_count: int = Field(default=0, ge=0, strict=True)
|
|
241
|
+
artifact_count: int = Field(default=0, ge=0, strict=True)
|
|
242
|
+
included_artifact_count: int = Field(default=0, ge=0, strict=True)
|
|
243
|
+
covered_artifact_count: int = Field(default=0, ge=0, strict=True)
|
|
244
|
+
missing_artifact_count: int = Field(default=0, ge=0, strict=True)
|
|
245
|
+
errors: list[str] = Field(default_factory=list)
|
|
246
|
+
note: str = ""
|
|
247
|
+
manifests: list[JsonObject] = Field(default_factory=list)
|
|
248
|
+
artifacts: list[JsonObject] = Field(default_factory=list)
|
|
249
|
+
included_artifacts: list[JsonObject] = Field(default_factory=list)
|
|
250
|
+
missing_artifacts: list[JsonObject] = Field(default_factory=list)
|
|
251
|
+
partial_artifacts: list[JsonObject] = Field(default_factory=list)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class ProcessChatsArtifactValidation(ProcessChatsArtifactReport):
|
|
255
|
+
notes: list[ProcessChatsArtifactReport] = Field(default_factory=list)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class ProcessChatsCoverageSource(ContractModel):
|
|
259
|
+
raw_file: str = Field(min_length=1)
|
|
260
|
+
status: Literal["covered", "already_covered", "not_relevant"]
|
|
261
|
+
target_title: str = ""
|
|
262
|
+
target_section: str = ""
|
|
263
|
+
new_information_summary: str = ""
|
|
264
|
+
reference_added: str = ""
|
|
265
|
+
reason: str = ""
|
|
266
|
+
existing_title: str = ""
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class ProcessChatsCoverageSummary(ContractModel):
|
|
270
|
+
"""Coverage summary accepted at the typed publish-to-FSM boundary."""
|
|
271
|
+
|
|
272
|
+
schema_id: str = Field(default="", alias="schema")
|
|
273
|
+
coverage_path: str = ""
|
|
274
|
+
coverage_hash: str = ""
|
|
275
|
+
coverage_hashes: list[str] = Field(default_factory=list)
|
|
276
|
+
raw_file: str = ""
|
|
277
|
+
raw_files: list[str] = Field(default_factory=list)
|
|
278
|
+
multi_source: bool = Field(default=False, strict=True)
|
|
279
|
+
source_count: int = Field(default=0, ge=0, strict=True)
|
|
280
|
+
exhaustive: bool = Field(default=False, strict=True)
|
|
281
|
+
status: str = ""
|
|
282
|
+
item_count: int = Field(default=0, ge=0, strict=True)
|
|
283
|
+
planned_meaning_count: int = Field(default=0, ge=0, strict=True)
|
|
284
|
+
not_a_note_count: int = Field(default=0, ge=0, strict=True)
|
|
285
|
+
raw_file_count: int = Field(default=0, ge=0, strict=True)
|
|
286
|
+
covered_count: int = Field(default=0, ge=0, strict=True)
|
|
287
|
+
sources: list[ProcessChatsCoverageSource] = Field(default_factory=list)
|
|
288
|
+
source_status_counts: dict[str, int] = Field(default_factory=dict)
|
|
289
|
+
staged_note_count: int = Field(default=0, ge=0, strict=True)
|
|
290
|
+
note_plan_hash: str = ""
|
|
291
|
+
batch_id: str = ""
|
|
292
|
+
run_id: str = ""
|
|
293
|
+
source_artifact_hash: str = ""
|
|
294
|
+
note_plan_source_count: int = Field(default=0, ge=0, strict=True)
|
|
295
|
+
note_plan_hashes: dict[str, str] = Field(default_factory=dict)
|
|
296
|
+
note_plan_item_count: int = Field(default=0, ge=0, strict=True)
|
|
297
|
+
note_plan_planned_meaning_count: int = Field(default=0, ge=0, strict=True)
|
|
298
|
+
note_plan_attach_count: int = Field(default=0, ge=0, strict=True)
|
|
299
|
+
note_plan_not_a_note_count: int = Field(default=0, ge=0, strict=True)
|
|
300
|
+
note_plan_needs_context_count: int = Field(default=0, ge=0, strict=True)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class ProcessChatsRawUpdate(ContractModel):
|
|
304
|
+
raw_file: str = Field(min_length=1)
|
|
305
|
+
backup: str | None = None
|
|
306
|
+
updated: bool = Field(strict=True)
|
|
307
|
+
updates: dict[str, str] = Field(default_factory=dict)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class ProcessChatsNewTaxonomyLeafAuthorizationNote(ContractModel):
|
|
311
|
+
target_path: str = Field(min_length=1)
|
|
312
|
+
taxonomy: str = ""
|
|
313
|
+
taxonomy_requested: str = ""
|
|
314
|
+
taxonomy_new_dirs: list[str] = Field(default_factory=list)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class ProcessChatsNewTaxonomyLeafAuthorization(ContractModel):
|
|
318
|
+
required: bool = Field(default=False, strict=True)
|
|
319
|
+
authorized_by_dry_run_receipt: bool = Field(default=False, strict=True)
|
|
320
|
+
note_count: int = Field(default=0, ge=0, strict=True)
|
|
321
|
+
notes: list[ProcessChatsNewTaxonomyLeafAuthorizationNote] = Field(default_factory=list)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class ProcessChatsPlannedNoteSummary(ContractModel):
|
|
325
|
+
title: str = ""
|
|
326
|
+
taxonomy: str = ""
|
|
327
|
+
taxonomy_requested: str = ""
|
|
328
|
+
taxonomy_canonicalized: list[TaxonomyCanonicalizationStep] = Field(default_factory=list)
|
|
329
|
+
taxonomy_new_dirs: list[str] = Field(default_factory=list)
|
|
330
|
+
content_path: str = ""
|
|
331
|
+
target_path: str = ""
|
|
332
|
+
artifact_validation: ProcessChatsArtifactValidation | None = None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class ProcessChatsPlannedBatchSummary(ContractModel):
|
|
336
|
+
raw_file: str = ""
|
|
337
|
+
raw_files: list[str] = Field(default_factory=list)
|
|
338
|
+
notes: list[ProcessChatsPlannedNoteSummary] = Field(default_factory=list)
|
|
339
|
+
coverage_path: str = ""
|
|
340
|
+
coverage: ProcessChatsCoverageSummary | None = None
|
|
341
|
+
artifact_validation: ProcessChatsArtifactValidation | None = None
|
|
342
|
+
batch_state: ProcessChatsBatchState | None = None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class ProcessChatsLinkerRun(ContractModel):
|
|
346
|
+
"""Typed evidence packet for a parent workflow delegating to `/mednotes:link`."""
|
|
347
|
+
|
|
348
|
+
schema_id: str | None = Field(default=None, alias="schema")
|
|
349
|
+
phase: str = ""
|
|
350
|
+
status: str = ""
|
|
351
|
+
next_action: str = ""
|
|
352
|
+
trigger_context_path: str = ""
|
|
353
|
+
diagnosis_path: str = ""
|
|
354
|
+
receipt_path: str = ""
|
|
355
|
+
diagnosis_status: str = ""
|
|
356
|
+
diagnosis_blocked_reason: str = ""
|
|
357
|
+
blocker_count: int = Field(default=0, ge=0, strict=True)
|
|
358
|
+
linker_applied: bool = Field(default=False, strict=True)
|
|
359
|
+
linker_skipped_reason: str = ""
|
|
360
|
+
apply_status: str = ""
|
|
361
|
+
apply_blocked_reason: str = ""
|
|
362
|
+
changed_files: list[str] = Field(default_factory=list)
|
|
363
|
+
files_changed: int = Field(default=0, ge=0, strict=True)
|
|
364
|
+
workflow_effect_results: list[WorkflowEffectResult] = Field(default_factory=list)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class ProcessChatsContinuationEffect(ContractModel):
|
|
368
|
+
kind: str = Field(min_length=1)
|
|
369
|
+
workflow: Literal["/mednotes:process-chats"] = PROCESS_CHATS_WORKFLOW
|
|
370
|
+
blocked_reason: str = Field(min_length=1)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class ProcessChatsContinuationPlan(ContractModel):
|
|
374
|
+
schema_id: Literal["medical-notes-workbench.process-chats-continuation-plan.v1"] = Field(
|
|
375
|
+
default="medical-notes-workbench.process-chats-continuation-plan.v1",
|
|
376
|
+
alias="schema",
|
|
377
|
+
)
|
|
378
|
+
status: Literal["ready"] = "ready"
|
|
379
|
+
workflow: Literal["/mednotes:process-chats"] = PROCESS_CHATS_WORKFLOW
|
|
380
|
+
lane: str = Field(min_length=1)
|
|
381
|
+
blocked_reason: str = Field(min_length=1)
|
|
382
|
+
next_effect: ProcessChatsContinuationEffect
|
|
383
|
+
retry_budget: int = Field(default=1, ge=1, strict=True)
|
|
384
|
+
summary: str = Field(min_length=1)
|
|
385
|
+
directive_instructions: list[str] = Field(default_factory=list, exclude=True)
|
|
386
|
+
|
|
387
|
+
def to_payload(self) -> JsonObject:
|
|
388
|
+
return JsonObjectAdapter.validate_python(self.model_dump(by_alias=True))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class ProcessChatsPublishOperationResult(ContractModel):
|
|
392
|
+
"""Closed operation payload consumed by the FSM boundary.
|
|
393
|
+
|
|
394
|
+
`runtime_observation` is the sole state-driving publish/link fact packet.
|
|
395
|
+
Historical status fields remain diagnostic UX fields and must not select
|
|
396
|
+
FSM entry states or leaf states at this boundary.
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
schema_id: str | None = Field(default=None, alias="schema")
|
|
400
|
+
workflow: str | None = None
|
|
401
|
+
phase: str = ""
|
|
402
|
+
status: Literal[
|
|
403
|
+
"ready_to_publish",
|
|
404
|
+
"published",
|
|
405
|
+
"completed_with_link_blockers",
|
|
406
|
+
"completed",
|
|
407
|
+
"blocked",
|
|
408
|
+
"failed",
|
|
409
|
+
]
|
|
410
|
+
blocked_reason: str = ""
|
|
411
|
+
next_action: str = ""
|
|
412
|
+
required_inputs: list[str] = Field(default_factory=list)
|
|
413
|
+
human_decision_required: bool = Field(default=False, strict=True)
|
|
414
|
+
human_decision_packet: HumanDecisionPacket | None = None
|
|
415
|
+
decision_summary: ProcessChatsDecisionSummary | None = None
|
|
416
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
417
|
+
diagnostic_context: JsonObject = Field(default_factory=dict)
|
|
418
|
+
error: str | None = None
|
|
419
|
+
parse_error: str | None = None
|
|
420
|
+
dry_run: bool = Field(default=False, strict=True)
|
|
421
|
+
backup: bool = Field(default=False, strict=True)
|
|
422
|
+
manifest: str = ""
|
|
423
|
+
manifest_hash: str = ""
|
|
424
|
+
allow_new_taxonomy_leaf: bool = Field(default=True, strict=True)
|
|
425
|
+
require_coverage: bool = Field(default=True, strict=True)
|
|
426
|
+
batch_state: list[ProcessChatsBatchState] = Field(default_factory=list)
|
|
427
|
+
new_taxonomy_leaf_authorization: ProcessChatsNewTaxonomyLeafAuthorization | None = None
|
|
428
|
+
planned_batches: list[ProcessChatsPlannedBatchSummary] = Field(default_factory=list)
|
|
429
|
+
coverage_summary: ProcessChatsCoverageSummary | None = None
|
|
430
|
+
coverage: ProcessChatsCoverageSummary | None = None
|
|
431
|
+
created: list[str] = Field(default_factory=list)
|
|
432
|
+
raw_updates: list[ProcessChatsRawUpdate] = Field(default_factory=list)
|
|
433
|
+
created_count: int = Field(default=0, ge=0, strict=True)
|
|
434
|
+
processed_raw_count: int = Field(default=0, ge=0, strict=True)
|
|
435
|
+
publish_receipt: PublishReceipt | None = None
|
|
436
|
+
dry_run_receipt: ProcessChatsDryRunReceipt | None = None
|
|
437
|
+
linker: ProcessChatsLinkerRun | None = None
|
|
438
|
+
linker_applied: bool = Field(default=False, strict=True)
|
|
439
|
+
linker_skipped_reason: str = ""
|
|
440
|
+
link_trigger_context_path: str = ""
|
|
441
|
+
linker_trigger_context_path: str = ""
|
|
442
|
+
linker_diagnosis_path: str = ""
|
|
443
|
+
linker_receipt_path: str = ""
|
|
444
|
+
runtime_observation: ProcessChatsPublishRuntimeObservation
|
|
445
|
+
|
|
446
|
+
@model_validator(mode="after")
|
|
447
|
+
def _workflow_must_match_process_chats(self) -> ProcessChatsPublishOperationResult:
|
|
448
|
+
if self.workflow is not None and self.workflow != PROCESS_CHATS_WORKFLOW:
|
|
449
|
+
raise ValueError("process-chats publish operation result has invalid workflow")
|
|
450
|
+
return self
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class ProcessChatsPublishDiagnostic(ContractModel):
|
|
454
|
+
"""Non-authoritative publish details rendered after the StateChart decides state."""
|
|
455
|
+
|
|
456
|
+
status: str = ""
|
|
457
|
+
receipt_status: str = ""
|
|
458
|
+
dry_run: bool = Field(default=False, strict=True)
|
|
459
|
+
manifest: str = ""
|
|
460
|
+
dry_run_receipt: ProcessChatsDryRunReceipt | None = None
|
|
461
|
+
new_taxonomy_leaf_authorization: ProcessChatsNewTaxonomyLeafAuthorization | None = None
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class ProcessChatsLinkerDiagnostic(ContractModel):
|
|
465
|
+
"""Non-authoritative linker details rendered after the StateChart decides state."""
|
|
466
|
+
|
|
467
|
+
status: str = ""
|
|
468
|
+
next_action: str = ""
|
|
469
|
+
diagnosis_status: str = ""
|
|
470
|
+
applied: bool = Field(default=False, strict=True)
|
|
471
|
+
skipped_reason: str = ""
|
|
472
|
+
blocker_count: int = Field(default=0, ge=0, strict=True)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class ProcessChatsOperationalSummary(ContractModel):
|
|
476
|
+
"""Operational counts and artifacts that cannot choose the FSM transition."""
|
|
477
|
+
|
|
478
|
+
note_count: int = Field(default=0, ge=0, strict=True)
|
|
479
|
+
raw_count: int = Field(default=0, ge=0, strict=True)
|
|
480
|
+
coverage_raw_count: int = Field(default=0, ge=0, strict=True)
|
|
481
|
+
planned_note_count: int = Field(default=0, ge=0, strict=True)
|
|
482
|
+
mutated: bool = Field(default=False, strict=True)
|
|
483
|
+
changed_files: list[str] = Field(default_factory=list)
|
|
484
|
+
blocked_item_count: int = Field(default=0, ge=0, strict=True)
|
|
485
|
+
next_action: str = ""
|
|
486
|
+
publish: ProcessChatsPublishDiagnostic = Field(default_factory=ProcessChatsPublishDiagnostic)
|
|
487
|
+
linker: ProcessChatsLinkerDiagnostic = Field(default_factory=ProcessChatsLinkerDiagnostic)
|
|
488
|
+
artifacts: JsonObject = Field(default_factory=dict)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
class ProcessChatsFsmFacts(ContractModel):
|
|
492
|
+
run_id: str = Field(min_length=1)
|
|
493
|
+
initial_state: MachineProcessChatsState
|
|
494
|
+
event: ProcessChatsBoundaryEvent
|
|
495
|
+
operational_summary: ProcessChatsOperationalSummary = Field(default_factory=ProcessChatsOperationalSummary)
|
|
496
|
+
version_control_safety: VersionControlSafety
|
|
497
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
498
|
+
|
|
499
|
+
@model_validator(mode="after")
|
|
500
|
+
def _event_must_match_process_chats_entry(self) -> ProcessChatsFsmFacts:
|
|
501
|
+
if self.event.workflow != PROCESS_CHATS_WORKFLOW:
|
|
502
|
+
raise ValueError(f"process-chats event workflow must be {PROCESS_CHATS_WORKFLOW}")
|
|
503
|
+
if self.event.run_id != self.run_id:
|
|
504
|
+
raise ValueError("process-chats event run_id must match ProcessChatsFsmFacts.run_id")
|
|
505
|
+
if self.event.current_state != self.initial_state.value:
|
|
506
|
+
raise ValueError("process-chats event current_state must match initial_state")
|
|
507
|
+
return self
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class _ProcessChatsMachineProjection(ContractModel):
|
|
511
|
+
"""Public payload lens derived only from `ProcessChatsMachine`.
|
|
512
|
+
|
|
513
|
+
This is not a second workflow state. Effects are emitted by the StateChart
|
|
514
|
+
transition and then projected outward; agent-facing control must not infer
|
|
515
|
+
or rebuild them from status strings or adapter payloads.
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
reason: ProcessChatsOutcomeReason
|
|
519
|
+
reason_code: str = ""
|
|
520
|
+
state: MachineProcessChatsState
|
|
521
|
+
category: WorkflowStateCategory
|
|
522
|
+
status: WorkflowProgressStatus
|
|
523
|
+
event_type: WorkflowProgressEventType
|
|
524
|
+
message: str
|
|
525
|
+
trigger: str
|
|
526
|
+
decision: WorkflowDecision | None = None
|
|
527
|
+
human_decision_packet: HumanDecisionPacket | None = None
|
|
528
|
+
next_action: str = ""
|
|
529
|
+
resume_action: str = ""
|
|
530
|
+
resume_supported: bool = False
|
|
531
|
+
can_continue_now: bool = False
|
|
532
|
+
effects: list[WorkflowEffect] = Field(default_factory=list)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class _ProcessChatsPayloadProgressView(ContractModel):
|
|
536
|
+
status: StrictStr
|
|
537
|
+
state: StrictStr = ""
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class _ProcessChatsPayloadSnapshot(ContractModel):
|
|
541
|
+
current_category: StrictStr
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
class _ProcessChatsPayloadReceipt(ContractModel):
|
|
545
|
+
status: StrictStr
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class _ProcessChatsPayloadFields(ContractModel):
|
|
549
|
+
workflow: Literal["/mednotes:process-chats"]
|
|
550
|
+
progress_view_model: _ProcessChatsPayloadProgressView
|
|
551
|
+
state_machine_snapshot: _ProcessChatsPayloadSnapshot
|
|
552
|
+
receipt: _ProcessChatsPayloadReceipt
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class ProcessChatsFsmResult(ContractModel):
|
|
556
|
+
schema_id: Literal["medical-notes-workbench.process-chats-fsm-result.v1"] = Field(
|
|
557
|
+
default=PROCESS_CHATS_SCHEMA,
|
|
558
|
+
alias="schema",
|
|
559
|
+
)
|
|
560
|
+
workflow: Literal["/mednotes:process-chats"] = PROCESS_CHATS_WORKFLOW
|
|
561
|
+
run_id: str = Field(min_length=1)
|
|
562
|
+
progress_state: SkipJsonSchema[WorkflowProgressState]
|
|
563
|
+
progress_view_model: WorkflowProgressViewModel
|
|
564
|
+
state_machine_snapshot: WorkflowStateMachineSnapshot
|
|
565
|
+
decision: WorkflowDecision | None = None
|
|
566
|
+
human_decision_packet: HumanDecisionPacket | None = None
|
|
567
|
+
receipt: WorkflowReceiptPayload
|
|
568
|
+
reports: WorkflowReports
|
|
569
|
+
agent_directive: JsonObject
|
|
570
|
+
artifacts: JsonObject = Field(default_factory=dict)
|
|
571
|
+
version_control_safety: VersionControlSafety
|
|
572
|
+
diagnostic_context: JsonObject = Field(default_factory=dict)
|
|
573
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
574
|
+
|
|
575
|
+
@model_validator(mode="before")
|
|
576
|
+
@classmethod
|
|
577
|
+
def _hydrate_progress_state_from_public_payload(cls, value: object) -> object:
|
|
578
|
+
"""Accept public payloads where progress_state is intentionally hidden."""
|
|
579
|
+
|
|
580
|
+
if not isinstance(value, dict) or "progress_state" in value or "progress_view_model" not in value:
|
|
581
|
+
return value
|
|
582
|
+
hydrated = dict(value)
|
|
583
|
+
progress_view = WorkflowProgressViewModel.model_validate(value["progress_view_model"])
|
|
584
|
+
hydrated["progress_state"] = progress_state_from_view_model(progress_view).to_payload()
|
|
585
|
+
return hydrated
|
|
586
|
+
|
|
587
|
+
@model_validator(mode="after")
|
|
588
|
+
def _progress_view_model_matches_state(self) -> ProcessChatsFsmResult:
|
|
589
|
+
expected = build_progress_view_model(self.progress_state).to_payload()
|
|
590
|
+
if self.progress_view_model.to_payload() != expected:
|
|
591
|
+
raise ValueError("progress_view_model must match progress_state")
|
|
592
|
+
return self
|
|
593
|
+
|
|
594
|
+
def to_payload(self) -> JsonObject:
|
|
595
|
+
payload: JsonObject = {
|
|
596
|
+
"schema": self.schema_id,
|
|
597
|
+
"workflow": self.workflow,
|
|
598
|
+
"run_id": self.run_id,
|
|
599
|
+
"state_machine_snapshot": self.state_machine_snapshot.to_payload(),
|
|
600
|
+
"progress_view_model": self.progress_view_model.to_payload(),
|
|
601
|
+
"decision": self.decision.to_payload() if self.decision is not None else None,
|
|
602
|
+
"human_decision_packet": self.human_decision_packet.to_payload()
|
|
603
|
+
if self.human_decision_packet is not None
|
|
604
|
+
else None,
|
|
605
|
+
"receipt": self.receipt.to_payload(),
|
|
606
|
+
"reports": self.reports.to_payload(),
|
|
607
|
+
"agent_directive": dict(self.agent_directive),
|
|
608
|
+
"artifacts": dict(self.artifacts),
|
|
609
|
+
"version_control_safety": self.version_control_safety.to_payload(),
|
|
610
|
+
"error_context": dict(self.error_context),
|
|
611
|
+
}
|
|
612
|
+
if self.diagnostic_context:
|
|
613
|
+
payload["diagnostic_context"] = dict(self.diagnostic_context)
|
|
614
|
+
payload = JsonObjectAdapter.validate_python(payload)
|
|
615
|
+
assert_process_chats_fsm_payload(payload)
|
|
616
|
+
return payload
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def build_process_chats_fsm_result(facts: ProcessChatsFsmFacts) -> ProcessChatsFsmResult:
|
|
620
|
+
machine_model = _process_chats_model_after_event(facts.initial_state, facts.event)
|
|
621
|
+
projection = _project_machine_outcome(facts, machine_model)
|
|
622
|
+
progress_state = _progress_state(facts, projection)
|
|
623
|
+
progress_view_model = build_progress_view_model(progress_state)
|
|
624
|
+
snapshot = _transition_from_machine_model(machine_model, projection, progress_state)
|
|
625
|
+
receipt = _receipt(facts, projection, progress_state, snapshot)
|
|
626
|
+
reports = _reports(facts, projection, progress_state)
|
|
627
|
+
public_report = reports.public_report
|
|
628
|
+
diagnostic_context = _diagnostic_context(
|
|
629
|
+
facts,
|
|
630
|
+
projection,
|
|
631
|
+
)
|
|
632
|
+
agent_directive = _agent_directive(
|
|
633
|
+
projection,
|
|
634
|
+
progress_view_model=progress_view_model,
|
|
635
|
+
user_visible_summary=public_report.summary_text(),
|
|
636
|
+
)
|
|
637
|
+
diagnostic_context = _problem_diagnostic_context(diagnostic_context, projection, facts.operational_summary)
|
|
638
|
+
if facts.error_context:
|
|
639
|
+
diagnostic_context = diagnostic_context_evidence_only(
|
|
640
|
+
{**diagnostic_context, "error_context": dict(facts.error_context)}
|
|
641
|
+
)
|
|
642
|
+
return ProcessChatsFsmResult(
|
|
643
|
+
run_id=facts.run_id,
|
|
644
|
+
progress_state=progress_state,
|
|
645
|
+
progress_view_model=progress_view_model,
|
|
646
|
+
state_machine_snapshot=snapshot,
|
|
647
|
+
decision=projection.decision,
|
|
648
|
+
human_decision_packet=projection.human_decision_packet,
|
|
649
|
+
receipt=receipt,
|
|
650
|
+
reports=reports,
|
|
651
|
+
agent_directive=agent_directive,
|
|
652
|
+
artifacts=dict(facts.operational_summary.artifacts),
|
|
653
|
+
version_control_safety=facts.version_control_safety,
|
|
654
|
+
diagnostic_context=diagnostic_context,
|
|
655
|
+
error_context=facts.error_context,
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _process_chats_model_after_event(
|
|
660
|
+
initial_state: MachineProcessChatsState,
|
|
661
|
+
event: ProcessChatsBoundaryEvent,
|
|
662
|
+
) -> WorkflowModel:
|
|
663
|
+
model = WorkflowModel.start(workflow=PROCESS_CHATS_WORKFLOW, run_id=event.run_id, initial_state=initial_state.value)
|
|
664
|
+
_send_machine_event(model, event)
|
|
665
|
+
return model
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _send_machine_event(model: WorkflowModel, event: WorkflowEventLike) -> WorkflowTransitionResult:
|
|
669
|
+
machine = ProcessChatsMachine(model=model, state_field=WorkflowModel.STATECHART_STATE_FIELD)
|
|
670
|
+
return send_workflow_event(machine, event)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _project_machine_outcome(facts: ProcessChatsFsmFacts, model: WorkflowModel) -> _ProcessChatsMachineProjection:
|
|
674
|
+
transition = model.last_transition
|
|
675
|
+
if transition is None:
|
|
676
|
+
raise ValueError("process-chats machine model has no transition to project")
|
|
677
|
+
state = MachineProcessChatsState(model.state)
|
|
678
|
+
category = _machine_category_for_state(state.value)
|
|
679
|
+
status = _progress_status_for_category(category)
|
|
680
|
+
reason = _machine_reason_for_state(state)
|
|
681
|
+
decision = transition.decision
|
|
682
|
+
next_action = _machine_next_action(facts, transition)
|
|
683
|
+
resume_action = transition.resume_action or (decision.resume_action if decision is not None else "")
|
|
684
|
+
return _projection(
|
|
685
|
+
reason=reason,
|
|
686
|
+
reason_code=transition.reason_code,
|
|
687
|
+
state=state,
|
|
688
|
+
category=category,
|
|
689
|
+
status=status,
|
|
690
|
+
event_type=_event_type_for_status(status),
|
|
691
|
+
decision=decision,
|
|
692
|
+
human_decision_packet=transition.human_decision_packet,
|
|
693
|
+
next_action=next_action,
|
|
694
|
+
resume_action=resume_action,
|
|
695
|
+
resume_supported=bool(resume_action),
|
|
696
|
+
can_continue_now=status == WorkflowProgressStatus.WAITING_AGENT,
|
|
697
|
+
effects=list(transition.effects),
|
|
698
|
+
message=_machine_message_for_state(state, reason),
|
|
699
|
+
trigger=transition.trigger,
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def _machine_category_for_state(state: str) -> WorkflowStateCategory:
|
|
704
|
+
model = WorkflowModel.start(workflow=PROCESS_CHATS_WORKFLOW, run_id="category", initial_state=state)
|
|
705
|
+
machine = ProcessChatsMachine(model=model, state_field=WorkflowModel.STATECHART_STATE_FIELD)
|
|
706
|
+
return machine.category_for_state(state)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _progress_status_for_category(category: WorkflowStateCategory) -> WorkflowProgressStatus:
|
|
710
|
+
match category:
|
|
711
|
+
case WorkflowStateCategory.PREPARING | WorkflowStateCategory.RUNNING:
|
|
712
|
+
return WorkflowProgressStatus.RUNNING
|
|
713
|
+
case WorkflowStateCategory.WAITING_AGENT:
|
|
714
|
+
return WorkflowProgressStatus.WAITING_AGENT
|
|
715
|
+
case WorkflowStateCategory.WAITING_EXTERNAL:
|
|
716
|
+
return WorkflowProgressStatus.WAITING_EXTERNAL
|
|
717
|
+
case WorkflowStateCategory.WAITING_HUMAN:
|
|
718
|
+
return WorkflowProgressStatus.WAITING_HUMAN
|
|
719
|
+
case WorkflowStateCategory.BLOCKED:
|
|
720
|
+
return WorkflowProgressStatus.BLOCKED
|
|
721
|
+
case WorkflowStateCategory.FAILED:
|
|
722
|
+
return WorkflowProgressStatus.FAILED
|
|
723
|
+
case WorkflowStateCategory.COMPLETED:
|
|
724
|
+
return WorkflowProgressStatus.COMPLETED
|
|
725
|
+
case WorkflowStateCategory.COMPLETED_WITH_WARNINGS:
|
|
726
|
+
return WorkflowProgressStatus.COMPLETED_WITH_WARNINGS
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _event_type_for_status(status: WorkflowProgressStatus) -> WorkflowProgressEventType:
|
|
730
|
+
match status:
|
|
731
|
+
case WorkflowProgressStatus.COMPLETED | WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
|
|
732
|
+
return WorkflowProgressEventType.WORKFLOW_COMPLETED
|
|
733
|
+
case WorkflowProgressStatus.FAILED:
|
|
734
|
+
return WorkflowProgressEventType.WORKFLOW_FAILED
|
|
735
|
+
case WorkflowProgressStatus.BLOCKED | WorkflowProgressStatus.WAITING_HUMAN | WorkflowProgressStatus.WAITING_AGENT:
|
|
736
|
+
return WorkflowProgressEventType.DECISION_EMITTED
|
|
737
|
+
case WorkflowProgressStatus.WAITING_EXTERNAL:
|
|
738
|
+
return WorkflowProgressEventType.EXTERNAL_WAIT_STARTED
|
|
739
|
+
case _:
|
|
740
|
+
return WorkflowProgressEventType.STATE_ENTERED
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _machine_reason_for_state(state: MachineProcessChatsState) -> ProcessChatsOutcomeReason:
|
|
744
|
+
match state:
|
|
745
|
+
case MachineProcessChatsState.BACKLOG_NO_PENDING_RAW_CHATS | MachineProcessChatsState.BACKLOG_NO_TRIAGED_RAW_CHATS:
|
|
746
|
+
return ProcessChatsOutcomeReason.NO_PENDING
|
|
747
|
+
case MachineProcessChatsState.BACKLOG_TRIAGED_RAW_CHATS_READY:
|
|
748
|
+
return ProcessChatsOutcomeReason.TRIAGED_READY
|
|
749
|
+
case MachineProcessChatsState.PUBLISH_AWAITING_CONFIRMATION:
|
|
750
|
+
return ProcessChatsOutcomeReason.READY_TO_PUBLISH
|
|
751
|
+
case MachineProcessChatsState.PUBLISHED:
|
|
752
|
+
return ProcessChatsOutcomeReason.PUBLISHED
|
|
753
|
+
case MachineProcessChatsState.COMPLETED_WITH_LINK_BLOCKERS:
|
|
754
|
+
return ProcessChatsOutcomeReason.LINKER_BLOCKED
|
|
755
|
+
case MachineProcessChatsState.TERMINAL_FAILURE_RECORDED:
|
|
756
|
+
return ProcessChatsOutcomeReason.FAILED
|
|
757
|
+
case (
|
|
758
|
+
MachineProcessChatsState.ARCHITECT_WORK_REQUESTED
|
|
759
|
+
| MachineProcessChatsState.ENVIRONMENT_PATHS_MISSING
|
|
760
|
+
| MachineProcessChatsState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED
|
|
761
|
+
| MachineProcessChatsState.SUBAGENT_PLAN_ATTESTATION_REQUIRED
|
|
762
|
+
| MachineProcessChatsState.SUBAGENT_PLAN_ATTESTATION_INVALID
|
|
763
|
+
| MachineProcessChatsState.NOTE_VALIDATION_COVERAGE_GAP
|
|
764
|
+
| MachineProcessChatsState.NOTE_VALIDATION_MANIFEST_MISMATCH
|
|
765
|
+
| MachineProcessChatsState.NOTE_VALIDATION_CONTENT_INVALID
|
|
766
|
+
| MachineProcessChatsState.PUBLISH_DRY_RUN_RECEIPT_REQUIRED
|
|
767
|
+
| MachineProcessChatsState.PUBLISH_STALE_RECEIPT
|
|
768
|
+
| MachineProcessChatsState.PUBLISH_DUPLICATE_TARGET
|
|
769
|
+
| MachineProcessChatsState.PUBLISH_PROVENANCE_GAP
|
|
770
|
+
| MachineProcessChatsState.PUBLISH_RECEIPT_INVALID
|
|
771
|
+
):
|
|
772
|
+
return ProcessChatsOutcomeReason.RECOVERABLE_BLOCKED
|
|
773
|
+
case _:
|
|
774
|
+
return ProcessChatsOutcomeReason.BLOCKED
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _machine_next_action(facts: ProcessChatsFsmFacts, transition: WorkflowTransitionResult) -> str:
|
|
778
|
+
if transition.decision is not None and transition.decision.next_action.strip():
|
|
779
|
+
return transition.decision.next_action
|
|
780
|
+
if transition.resume_action.strip():
|
|
781
|
+
return transition.resume_action
|
|
782
|
+
return _default_next_action(facts, _machine_reason_for_state(MachineProcessChatsState(transition.to_state)))
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def _machine_message_for_state(state: MachineProcessChatsState, reason: ProcessChatsOutcomeReason) -> str:
|
|
786
|
+
match state:
|
|
787
|
+
case MachineProcessChatsState.BACKLOG_NO_PENDING_RAW_CHATS:
|
|
788
|
+
return "Nenhum chat novo para processar."
|
|
789
|
+
case MachineProcessChatsState.BACKLOG_NO_TRIAGED_RAW_CHATS:
|
|
790
|
+
return "Nenhum chat triado para publicar."
|
|
791
|
+
case MachineProcessChatsState.BACKLOG_TRIAGED_RAW_CHATS_READY:
|
|
792
|
+
return "Nenhum chat bruto novo está pendente; há chats triados aguardando arquitetura."
|
|
793
|
+
case MachineProcessChatsState.PUBLISH_AWAITING_CONFIRMATION:
|
|
794
|
+
return "Previa pronta; aguardando confirmacao humana para publicar."
|
|
795
|
+
case MachineProcessChatsState.PUBLISHED:
|
|
796
|
+
return "Notas publicadas, raws atualizados e conexões executadas."
|
|
797
|
+
case MachineProcessChatsState.COMPLETED_WITH_LINK_BLOCKERS:
|
|
798
|
+
return "Publicacao concluida com pendencias de conexoes/grafo."
|
|
799
|
+
case MachineProcessChatsState.TERMINAL_FAILURE_RECORDED:
|
|
800
|
+
return "Process-chats falhou antes de concluir."
|
|
801
|
+
case _:
|
|
802
|
+
return f"Process-chats em {state.value} por {reason.value}."
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _transition_from_machine_model(
|
|
806
|
+
model: WorkflowModel,
|
|
807
|
+
projection: _ProcessChatsMachineProjection,
|
|
808
|
+
progress_state: WorkflowProgressState,
|
|
809
|
+
) -> WorkflowStateMachineSnapshot:
|
|
810
|
+
transition = model.last_transition
|
|
811
|
+
if transition is None:
|
|
812
|
+
raise ValueError("process-chats machine model has no last transition")
|
|
813
|
+
event = _progress_event_from_transition(transition, projection, progress_state)
|
|
814
|
+
projected_transition = WorkflowTransition(
|
|
815
|
+
workflow=transition.workflow,
|
|
816
|
+
from_state=transition.from_state,
|
|
817
|
+
to_state=transition.to_state,
|
|
818
|
+
to_category=projection.category,
|
|
819
|
+
trigger=transition.trigger,
|
|
820
|
+
effects=list(transition.effects),
|
|
821
|
+
progress_events=[event],
|
|
822
|
+
decision=projection.decision,
|
|
823
|
+
resume_action=projection.resume_action,
|
|
824
|
+
)
|
|
825
|
+
return WorkflowStateMachineSnapshot(
|
|
826
|
+
workflow=PROCESS_CHATS_WORKFLOW,
|
|
827
|
+
run_id=model.run_id,
|
|
828
|
+
current_state=model.state,
|
|
829
|
+
current_category=projection.category,
|
|
830
|
+
transitions=[projected_transition],
|
|
831
|
+
metadata={"reason": projection.reason.value, "statechart": "process_chats_machine"},
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _progress_event_from_transition(
|
|
836
|
+
transition: WorkflowTransitionResult,
|
|
837
|
+
projection: _ProcessChatsMachineProjection,
|
|
838
|
+
progress_state: WorkflowProgressState,
|
|
839
|
+
) -> WorkflowProgressEvent:
|
|
840
|
+
return WorkflowProgressEvent(
|
|
841
|
+
workflow=PROCESS_CHATS_WORKFLOW,
|
|
842
|
+
run_id=transition.run_id,
|
|
843
|
+
state=transition.to_state,
|
|
844
|
+
phase=progress_state.phase,
|
|
845
|
+
event_type=projection.event_type,
|
|
846
|
+
message=projection.message,
|
|
847
|
+
status=projection.status,
|
|
848
|
+
current=progress_state.current,
|
|
849
|
+
total=progress_state.total,
|
|
850
|
+
counts=progress_state.counts,
|
|
851
|
+
resume_action=projection.resume_action,
|
|
852
|
+
resume_supported=projection.resume_supported,
|
|
853
|
+
can_continue_now=projection.can_continue_now,
|
|
854
|
+
decision=progress_state.decision,
|
|
855
|
+
technical_context=progress_state.technical_context,
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _projection(
|
|
860
|
+
*,
|
|
861
|
+
reason: ProcessChatsOutcomeReason,
|
|
862
|
+
reason_code: str = "",
|
|
863
|
+
state: MachineProcessChatsState,
|
|
864
|
+
category: WorkflowStateCategory,
|
|
865
|
+
status: WorkflowProgressStatus,
|
|
866
|
+
event_type: WorkflowProgressEventType,
|
|
867
|
+
message: str,
|
|
868
|
+
trigger: str,
|
|
869
|
+
decision: WorkflowDecision | None = None,
|
|
870
|
+
human_decision_packet: HumanDecisionPacket | None = None,
|
|
871
|
+
next_action: str = "",
|
|
872
|
+
resume_action: str = "",
|
|
873
|
+
resume_supported: bool = False,
|
|
874
|
+
can_continue_now: bool = False,
|
|
875
|
+
effects: list[WorkflowEffect] | None = None,
|
|
876
|
+
) -> _ProcessChatsMachineProjection:
|
|
877
|
+
return _ProcessChatsMachineProjection(
|
|
878
|
+
reason=reason,
|
|
879
|
+
reason_code=reason_code,
|
|
880
|
+
state=state,
|
|
881
|
+
category=category,
|
|
882
|
+
status=status,
|
|
883
|
+
event_type=event_type,
|
|
884
|
+
decision=decision,
|
|
885
|
+
human_decision_packet=human_decision_packet,
|
|
886
|
+
next_action=next_action,
|
|
887
|
+
resume_action=resume_action,
|
|
888
|
+
resume_supported=resume_supported,
|
|
889
|
+
can_continue_now=can_continue_now,
|
|
890
|
+
effects=list(effects or []),
|
|
891
|
+
message=message,
|
|
892
|
+
trigger=trigger,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def _default_next_action(facts: ProcessChatsFsmFacts, reason: ProcessChatsOutcomeReason) -> str:
|
|
897
|
+
linker_next_action = _linker_next_action_after_link_attempt(
|
|
898
|
+
facts.operational_summary.linker.next_action.strip()
|
|
899
|
+
if reason == ProcessChatsOutcomeReason.LINKER_BLOCKED
|
|
900
|
+
else ""
|
|
901
|
+
)
|
|
902
|
+
if facts.operational_summary.next_action.strip():
|
|
903
|
+
next_action = _linker_next_action_after_link_attempt(facts.operational_summary.next_action.strip())
|
|
904
|
+
if linker_next_action and linker_next_action not in next_action:
|
|
905
|
+
return f"{next_action} Detalhe das conexões/grafo: {linker_next_action}"
|
|
906
|
+
return next_action
|
|
907
|
+
if linker_next_action:
|
|
908
|
+
return f"Resolver pendências de conexões/grafo: {linker_next_action}"
|
|
909
|
+
match reason:
|
|
910
|
+
case ProcessChatsOutcomeReason.NO_PENDING:
|
|
911
|
+
return ""
|
|
912
|
+
case ProcessChatsOutcomeReason.TRIAGED_READY:
|
|
913
|
+
return "Continuar para os chats triados com list-triados e plan-subagents --phase architect."
|
|
914
|
+
case ProcessChatsOutcomeReason.READY_TO_PUBLISH:
|
|
915
|
+
return "Revisar a prévia e confirmar publicação pela rota oficial."
|
|
916
|
+
case ProcessChatsOutcomeReason.LINKER_BLOCKED:
|
|
917
|
+
return "Resolver pendências de conexões/grafo pela rota oficial antes de considerar o lote concluído."
|
|
918
|
+
case ProcessChatsOutcomeReason.WAITING_HUMAN:
|
|
919
|
+
return "Responder a decisão solicitada para continuar."
|
|
920
|
+
case ProcessChatsOutcomeReason.RECOVERABLE_BLOCKED:
|
|
921
|
+
return "Continuar automaticamente pela etapa de recuperacao oficial antes de concluir."
|
|
922
|
+
case ProcessChatsOutcomeReason.BLOCKED:
|
|
923
|
+
return "Corrigir o bloqueio informado e repetir /mednotes:process-chats pela rota oficial."
|
|
924
|
+
case ProcessChatsOutcomeReason.FAILED:
|
|
925
|
+
return "Revisar o erro do workflow e retomar /mednotes:process-chats pela rota oficial."
|
|
926
|
+
case ProcessChatsOutcomeReason.PUBLISHED:
|
|
927
|
+
return ""
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def _linker_next_action_after_link_attempt(value: str) -> str:
|
|
931
|
+
"""Drop stale pre-link diagnosis commands after the linker child already ran."""
|
|
932
|
+
|
|
933
|
+
if "run-linker --diagnose" not in value:
|
|
934
|
+
return value
|
|
935
|
+
detail_marker = "Detalhe das conexões/grafo:"
|
|
936
|
+
if detail_marker in value:
|
|
937
|
+
detail = value.split(detail_marker, maxsplit=1)[1].strip()
|
|
938
|
+
if detail:
|
|
939
|
+
return detail
|
|
940
|
+
return "Resolver pendências de conexões/grafo pela rota oficial antes de considerar o lote concluído."
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _progress_state(facts: ProcessChatsFsmFacts, projection: _ProcessChatsMachineProjection) -> WorkflowProgressState:
|
|
944
|
+
summary = facts.operational_summary
|
|
945
|
+
note_count = summary.note_count
|
|
946
|
+
raw_count = summary.raw_count
|
|
947
|
+
coverage_count = summary.coverage_raw_count
|
|
948
|
+
planned = max(note_count, summary.planned_note_count, coverage_count, raw_count)
|
|
949
|
+
current = planned if projection.status == WorkflowProgressStatus.COMPLETED else note_count
|
|
950
|
+
if projection.reason == ProcessChatsOutcomeReason.READY_TO_PUBLISH:
|
|
951
|
+
current = planned
|
|
952
|
+
counts = WorkflowProgressCounts(
|
|
953
|
+
planned_items=planned,
|
|
954
|
+
processed_items=current,
|
|
955
|
+
mutated_files=note_count if summary.mutated else 0,
|
|
956
|
+
written_files=note_count if summary.mutated else 0,
|
|
957
|
+
blocked_items=_blocked_item_count(facts, projection),
|
|
958
|
+
)
|
|
959
|
+
return WorkflowProgressState(
|
|
960
|
+
workflow=PROCESS_CHATS_WORKFLOW,
|
|
961
|
+
run_id=facts.run_id,
|
|
962
|
+
state=projection.state.value,
|
|
963
|
+
phase=_PHASE_BY_STATE[projection.state.value],
|
|
964
|
+
event_type=projection.event_type,
|
|
965
|
+
message=projection.message,
|
|
966
|
+
status=projection.status,
|
|
967
|
+
current=current,
|
|
968
|
+
total=planned,
|
|
969
|
+
counts=counts,
|
|
970
|
+
resume_action=projection.resume_action,
|
|
971
|
+
resume_supported=projection.resume_supported,
|
|
972
|
+
can_continue_now=projection.can_continue_now,
|
|
973
|
+
decision=projection.decision.decision_summary() if projection.decision is not None else None,
|
|
974
|
+
technical_context={
|
|
975
|
+
"reason": projection.reason.value,
|
|
976
|
+
"trigger": projection.trigger,
|
|
977
|
+
"process_status": projection.state.value,
|
|
978
|
+
"note_count": note_count,
|
|
979
|
+
"raw_count": raw_count,
|
|
980
|
+
},
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _receipt(
|
|
985
|
+
facts: ProcessChatsFsmFacts,
|
|
986
|
+
projection: _ProcessChatsMachineProjection,
|
|
987
|
+
progress_state: WorkflowProgressState,
|
|
988
|
+
snapshot: WorkflowStateMachineSnapshot,
|
|
989
|
+
) -> WorkflowReceiptPayload:
|
|
990
|
+
view_model = build_progress_view_model(progress_state)
|
|
991
|
+
return WorkflowReceiptPayload(
|
|
992
|
+
schema=PROCESS_CHATS_RECEIPT_SCHEMA,
|
|
993
|
+
workflow=PROCESS_CHATS_WORKFLOW,
|
|
994
|
+
run_id=facts.run_id,
|
|
995
|
+
status=_receipt_status(projection),
|
|
996
|
+
mutated=facts.operational_summary.mutated,
|
|
997
|
+
next_action=_receipt_next_action(projection),
|
|
998
|
+
human_decision_required=projection.status == WorkflowProgressStatus.WAITING_HUMAN,
|
|
999
|
+
human_decision_packet=projection.human_decision_packet,
|
|
1000
|
+
changed_files=list(facts.operational_summary.changed_files),
|
|
1001
|
+
version_control_safety=facts.version_control_safety,
|
|
1002
|
+
progress_state=progress_state,
|
|
1003
|
+
progress_view_model=view_model,
|
|
1004
|
+
state_machine_snapshot=snapshot,
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def _receipt_status(projection: _ProcessChatsMachineProjection) -> ReceiptStatus:
|
|
1009
|
+
match projection.status:
|
|
1010
|
+
case WorkflowProgressStatus.COMPLETED:
|
|
1011
|
+
return "completed"
|
|
1012
|
+
case WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
|
|
1013
|
+
return "completed_with_warnings"
|
|
1014
|
+
case WorkflowProgressStatus.WAITING_HUMAN:
|
|
1015
|
+
return "waiting_human"
|
|
1016
|
+
case WorkflowProgressStatus.WAITING_AGENT:
|
|
1017
|
+
return "waiting_agent"
|
|
1018
|
+
case WorkflowProgressStatus.WAITING_EXTERNAL:
|
|
1019
|
+
return "waiting_external"
|
|
1020
|
+
case WorkflowProgressStatus.BLOCKED:
|
|
1021
|
+
return "blocked"
|
|
1022
|
+
case WorkflowProgressStatus.FAILED:
|
|
1023
|
+
return "failed"
|
|
1024
|
+
case _:
|
|
1025
|
+
return "blocked"
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def _receipt_next_action(projection: _ProcessChatsMachineProjection) -> str:
|
|
1029
|
+
if projection.reason == ProcessChatsOutcomeReason.PUBLISHED:
|
|
1030
|
+
return ""
|
|
1031
|
+
return projection.next_action
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _reports(
|
|
1035
|
+
facts: ProcessChatsFsmFacts,
|
|
1036
|
+
projection: _ProcessChatsMachineProjection,
|
|
1037
|
+
progress_state: WorkflowProgressState,
|
|
1038
|
+
) -> WorkflowReports:
|
|
1039
|
+
note_count = facts.operational_summary.note_count
|
|
1040
|
+
raw_count = facts.operational_summary.raw_count
|
|
1041
|
+
match projection.reason:
|
|
1042
|
+
case ProcessChatsOutcomeReason.NO_PENDING:
|
|
1043
|
+
summary = "Nenhum chat novo para processar."
|
|
1044
|
+
case ProcessChatsOutcomeReason.TRIAGED_READY:
|
|
1045
|
+
summary = "Nenhum chat bruto novo está pendente; ainda há chats triados para preparar."
|
|
1046
|
+
case ProcessChatsOutcomeReason.READY_TO_PUBLISH:
|
|
1047
|
+
summary = "Prévia pronta; nenhuma nota foi publicada."
|
|
1048
|
+
case ProcessChatsOutcomeReason.PUBLISHED:
|
|
1049
|
+
summary = f"Publiquei {note_count} nota(s), atualizei {raw_count} raw chat(s) e rodei o pacote de links."
|
|
1050
|
+
case ProcessChatsOutcomeReason.LINKER_BLOCKED:
|
|
1051
|
+
summary = f"Publiquei {note_count} nota(s), mas o pacote de links/grafo ficou pendente."
|
|
1052
|
+
case ProcessChatsOutcomeReason.WAITING_HUMAN:
|
|
1053
|
+
summary = "Preciso de uma escolha sua antes de continuar o process-chats."
|
|
1054
|
+
case ProcessChatsOutcomeReason.RECOVERABLE_BLOCKED:
|
|
1055
|
+
summary = "Encontrei uma pendencia recuperavel e vou continuar pela rota oficial."
|
|
1056
|
+
case ProcessChatsOutcomeReason.BLOCKED:
|
|
1057
|
+
summary = "Process-chats bloqueou antes de publicar o lote."
|
|
1058
|
+
case ProcessChatsOutcomeReason.FAILED:
|
|
1059
|
+
summary = "Process-chats falhou antes de concluir."
|
|
1060
|
+
public_lines = [summary]
|
|
1061
|
+
followup_line = public_progress_followup_line(progress_state)
|
|
1062
|
+
if followup_line:
|
|
1063
|
+
public_lines.append(followup_line)
|
|
1064
|
+
public_report = WorkflowPublicReport(
|
|
1065
|
+
workflow=PROCESS_CHATS_WORKFLOW,
|
|
1066
|
+
run_id=facts.run_id,
|
|
1067
|
+
headline=summary,
|
|
1068
|
+
lines=public_lines,
|
|
1069
|
+
)
|
|
1070
|
+
return WorkflowReports(
|
|
1071
|
+
summary=summary,
|
|
1072
|
+
public_report=public_report,
|
|
1073
|
+
details={"primary_objective_summary": _primary_objective_summary(facts, projection).to_payload()},
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def _primary_objective_summary(
|
|
1078
|
+
facts: ProcessChatsFsmFacts,
|
|
1079
|
+
projection: _ProcessChatsMachineProjection,
|
|
1080
|
+
) -> ProcessChatsPrimaryObjectiveSummary:
|
|
1081
|
+
"""Derive the public objective answer from FSM facts, not legacy fields."""
|
|
1082
|
+
note_count = facts.operational_summary.note_count
|
|
1083
|
+
raw_count = facts.operational_summary.raw_count
|
|
1084
|
+
coverage_count = facts.operational_summary.coverage_raw_count
|
|
1085
|
+
status: ProcessChatsObjectiveStatus = _primary_process_status(projection)
|
|
1086
|
+
linker_status = _primary_linker_status(facts, projection)
|
|
1087
|
+
return ProcessChatsPrimaryObjectiveSummary(
|
|
1088
|
+
process_status=status,
|
|
1089
|
+
process_summary=_primary_process_summary(status=status, note_count=note_count, raw_count=raw_count),
|
|
1090
|
+
notes_status=_primary_notes_status(status),
|
|
1091
|
+
note_count=note_count,
|
|
1092
|
+
wiki_write_summary=_primary_wiki_write_summary(status=status, note_count=note_count),
|
|
1093
|
+
raw_status=_primary_raw_status(status=status, raw_count=raw_count, coverage_count=coverage_count),
|
|
1094
|
+
raw_count=raw_count,
|
|
1095
|
+
raw_summary=_primary_raw_summary(status=status, raw_count=raw_count, coverage_count=coverage_count),
|
|
1096
|
+
coverage_status=_primary_coverage_status(status=status, coverage_count=coverage_count),
|
|
1097
|
+
coverage_summary=_primary_coverage_summary(status=status, coverage_count=coverage_count),
|
|
1098
|
+
linker_status=linker_status,
|
|
1099
|
+
linker_summary=_primary_linker_summary(linker_status=linker_status, facts=facts, status=status),
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def _primary_process_status(projection: _ProcessChatsMachineProjection) -> ProcessChatsObjectiveStatus:
|
|
1104
|
+
match projection.reason:
|
|
1105
|
+
case ProcessChatsOutcomeReason.NO_PENDING:
|
|
1106
|
+
return "no_pending"
|
|
1107
|
+
case ProcessChatsOutcomeReason.TRIAGED_READY:
|
|
1108
|
+
return "ready_to_publish"
|
|
1109
|
+
case ProcessChatsOutcomeReason.READY_TO_PUBLISH | ProcessChatsOutcomeReason.WAITING_HUMAN:
|
|
1110
|
+
return "ready_to_publish"
|
|
1111
|
+
case ProcessChatsOutcomeReason.PUBLISHED:
|
|
1112
|
+
return "published"
|
|
1113
|
+
case ProcessChatsOutcomeReason.LINKER_BLOCKED:
|
|
1114
|
+
return "completed_with_link_blockers"
|
|
1115
|
+
case ProcessChatsOutcomeReason.FAILED:
|
|
1116
|
+
return "failed"
|
|
1117
|
+
case ProcessChatsOutcomeReason.BLOCKED | ProcessChatsOutcomeReason.RECOVERABLE_BLOCKED:
|
|
1118
|
+
return "blocked"
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def _primary_process_summary(*, status: ProcessChatsObjectiveStatus, note_count: int, raw_count: int) -> str:
|
|
1122
|
+
match status:
|
|
1123
|
+
case "ready_to_publish":
|
|
1124
|
+
return "process-chats preparou a prévia; nenhuma nota foi publicada."
|
|
1125
|
+
case "no_pending":
|
|
1126
|
+
return "Não havia raw chat novo ou triado para processar; nada foi publicado."
|
|
1127
|
+
case "published":
|
|
1128
|
+
return f"process-chats publicou {note_count} nota(s) e processou {raw_count} raw chat(s)."
|
|
1129
|
+
case "completed_with_link_blockers":
|
|
1130
|
+
return (
|
|
1131
|
+
f"process-chats publicou {note_count} nota(s) e processou {raw_count} raw chat(s), "
|
|
1132
|
+
"mas as conexões/grafo ficaram pendentes."
|
|
1133
|
+
)
|
|
1134
|
+
case "failed":
|
|
1135
|
+
return "process-chats falhou antes de concluir a publicação."
|
|
1136
|
+
case _:
|
|
1137
|
+
return "process-chats bloqueou antes de concluir a publicação."
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
def _primary_notes_status(status: ProcessChatsObjectiveStatus) -> ProcessChatsNotesStatus:
|
|
1141
|
+
match status:
|
|
1142
|
+
case "no_pending":
|
|
1143
|
+
return "not_written"
|
|
1144
|
+
case "ready_to_publish":
|
|
1145
|
+
return "ready_to_publish"
|
|
1146
|
+
case "published" | "completed_with_link_blockers":
|
|
1147
|
+
return "published"
|
|
1148
|
+
case "blocked" | "failed":
|
|
1149
|
+
return "blocked"
|
|
1150
|
+
case _:
|
|
1151
|
+
return "unknown"
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def _primary_wiki_write_summary(*, status: ProcessChatsObjectiveStatus, note_count: int) -> str:
|
|
1155
|
+
match status:
|
|
1156
|
+
case "no_pending":
|
|
1157
|
+
return "Nenhuma nota foi escrita porque não havia chat novo para processar."
|
|
1158
|
+
case "ready_to_publish":
|
|
1159
|
+
return "Nenhum arquivo da Wiki foi escrito; a publicação ainda está em prévia."
|
|
1160
|
+
case "published" | "completed_with_link_blockers":
|
|
1161
|
+
return f"{note_count} arquivo(s) da Wiki foram escritos."
|
|
1162
|
+
case "blocked" | "failed":
|
|
1163
|
+
return "Nenhum arquivo da Wiki deve ser considerado publicado neste estado."
|
|
1164
|
+
case _:
|
|
1165
|
+
return "O payload FSM não confirmou escrita real na Wiki."
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
def _primary_raw_status(
|
|
1169
|
+
*,
|
|
1170
|
+
status: ProcessChatsObjectiveStatus,
|
|
1171
|
+
raw_count: int,
|
|
1172
|
+
coverage_count: int,
|
|
1173
|
+
) -> ProcessChatsRawStatus:
|
|
1174
|
+
if status == "no_pending":
|
|
1175
|
+
return "not_processed"
|
|
1176
|
+
if status == "ready_to_publish":
|
|
1177
|
+
return "covered" if coverage_count else "not_processed"
|
|
1178
|
+
if status in {"published", "completed_with_link_blockers"}:
|
|
1179
|
+
return "processed" if raw_count else "unknown"
|
|
1180
|
+
if status in {"blocked", "failed"}:
|
|
1181
|
+
return "not_processed"
|
|
1182
|
+
return "unknown"
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def _primary_raw_summary(*, status: ProcessChatsObjectiveStatus, raw_count: int, coverage_count: int) -> str:
|
|
1186
|
+
if status == "no_pending":
|
|
1187
|
+
return "Nenhum raw chat foi processado porque não havia item novo nesta fase."
|
|
1188
|
+
if status == "ready_to_publish":
|
|
1189
|
+
if coverage_count:
|
|
1190
|
+
return f"{coverage_count} raw chat(s) estão cobertos, mas ainda não foram marcados como processados."
|
|
1191
|
+
return "Nenhum raw chat foi marcado como processado nesta prévia."
|
|
1192
|
+
if status in {"published", "completed_with_link_blockers"}:
|
|
1193
|
+
if raw_count:
|
|
1194
|
+
return f"{raw_count} raw chat(s) foram marcados como processados."
|
|
1195
|
+
return "O payload FSM publicou notas, mas não confirmou raws processados."
|
|
1196
|
+
if status in {"blocked", "failed"}:
|
|
1197
|
+
return "Nenhum raw chat deve ser considerado processado neste estado."
|
|
1198
|
+
return "O payload FSM não confirmou cobertura ou processamento dos raw chats."
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def _primary_coverage_status(*, status: ProcessChatsObjectiveStatus, coverage_count: int) -> ProcessChatsCoverageStatus:
|
|
1202
|
+
if status == "no_pending":
|
|
1203
|
+
return "not_applicable"
|
|
1204
|
+
if status == "ready_to_publish" and coverage_count:
|
|
1205
|
+
return "valid"
|
|
1206
|
+
if status in {"published", "completed_with_link_blockers"} and coverage_count:
|
|
1207
|
+
return "valid"
|
|
1208
|
+
if status in {"blocked", "failed"}:
|
|
1209
|
+
return "unknown"
|
|
1210
|
+
return "unknown"
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def _primary_coverage_summary(*, status: ProcessChatsObjectiveStatus, coverage_count: int) -> str:
|
|
1214
|
+
if status == "no_pending":
|
|
1215
|
+
return "Coverage/manifest não se aplicam porque nenhuma publicação foi preparada."
|
|
1216
|
+
if coverage_count:
|
|
1217
|
+
return f"Coverage/manifest coerentes para {coverage_count} raw chat(s)."
|
|
1218
|
+
if status == "ready_to_publish":
|
|
1219
|
+
return "O payload FSM não trouxe confirmação suficiente de coverage/manifest."
|
|
1220
|
+
return "Coverage/manifest não foram confirmados neste estado."
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
def _primary_linker_status(
|
|
1224
|
+
facts: ProcessChatsFsmFacts,
|
|
1225
|
+
projection: _ProcessChatsMachineProjection,
|
|
1226
|
+
) -> ProcessChatsLinkerStatus:
|
|
1227
|
+
if projection.reason == ProcessChatsOutcomeReason.NO_PENDING:
|
|
1228
|
+
return "not_applicable"
|
|
1229
|
+
if projection.reason == ProcessChatsOutcomeReason.TRIAGED_READY:
|
|
1230
|
+
return "not_run"
|
|
1231
|
+
if projection.reason == ProcessChatsOutcomeReason.READY_TO_PUBLISH:
|
|
1232
|
+
return "not_run"
|
|
1233
|
+
if projection.reason == ProcessChatsOutcomeReason.LINKER_BLOCKED:
|
|
1234
|
+
return "blocked"
|
|
1235
|
+
if projection.reason == ProcessChatsOutcomeReason.PUBLISHED:
|
|
1236
|
+
linker = facts.operational_summary.linker
|
|
1237
|
+
if linker.applied or linker.status == "completed" or linker.blocker_count == 0:
|
|
1238
|
+
return "applied"
|
|
1239
|
+
return "unknown"
|
|
1240
|
+
if projection.reason in {ProcessChatsOutcomeReason.BLOCKED, ProcessChatsOutcomeReason.RECOVERABLE_BLOCKED, ProcessChatsOutcomeReason.FAILED}:
|
|
1241
|
+
return "not_run"
|
|
1242
|
+
return "unknown"
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
def _primary_linker_summary(
|
|
1246
|
+
*,
|
|
1247
|
+
linker_status: ProcessChatsLinkerStatus,
|
|
1248
|
+
facts: ProcessChatsFsmFacts,
|
|
1249
|
+
status: ProcessChatsObjectiveStatus,
|
|
1250
|
+
) -> str:
|
|
1251
|
+
linker = facts.operational_summary.linker
|
|
1252
|
+
match linker_status:
|
|
1253
|
+
case "not_applicable":
|
|
1254
|
+
return "Conexões/grafo não se aplicam porque nenhuma nota foi publicada."
|
|
1255
|
+
case "not_run":
|
|
1256
|
+
if status == "ready_to_publish":
|
|
1257
|
+
return "Conexões/grafo ainda não rodaram porque a publicação não foi confirmada."
|
|
1258
|
+
return "Conexões/grafo não rodaram porque a publicação não foi concluída."
|
|
1259
|
+
case "applied":
|
|
1260
|
+
return "Conexões/grafo aplicadas sem bloqueios."
|
|
1261
|
+
case "blocked":
|
|
1262
|
+
reason = linker.skipped_reason or f"{linker.blocker_count} blocker(s)"
|
|
1263
|
+
return f"Conexões/grafo ficaram pendentes: {reason}."
|
|
1264
|
+
case _:
|
|
1265
|
+
return "O payload FSM não confirmou o estado de conexões/grafo."
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def _diagnostic_context(
|
|
1269
|
+
facts: ProcessChatsFsmFacts,
|
|
1270
|
+
projection: _ProcessChatsMachineProjection,
|
|
1271
|
+
) -> JsonObject:
|
|
1272
|
+
"""Build explanatory diagnostics without carrying executable control."""
|
|
1273
|
+
|
|
1274
|
+
summary = facts.operational_summary
|
|
1275
|
+
publish = summary.publish
|
|
1276
|
+
linker = summary.linker
|
|
1277
|
+
publish_context: JsonObject = {
|
|
1278
|
+
"status": publish.status,
|
|
1279
|
+
"receipt_status": publish.receipt_status,
|
|
1280
|
+
"dry_run": publish.dry_run,
|
|
1281
|
+
"manifest": publish.manifest,
|
|
1282
|
+
}
|
|
1283
|
+
if publish.dry_run_receipt is not None:
|
|
1284
|
+
publish_context["dry_run_receipt"] = {
|
|
1285
|
+
"path": publish.dry_run_receipt.path,
|
|
1286
|
+
"expires_at": publish.dry_run_receipt.expires_at,
|
|
1287
|
+
"manifest_hash": publish.dry_run_receipt.manifest_hash,
|
|
1288
|
+
"dry_run_options_hash": publish.dry_run_receipt.dry_run_options_hash,
|
|
1289
|
+
"batch_state": [item.to_payload() for item in publish.dry_run_receipt.batch_state],
|
|
1290
|
+
}
|
|
1291
|
+
if publish.new_taxonomy_leaf_authorization:
|
|
1292
|
+
publish_context["new_taxonomy_leaf_authorization"] = publish.new_taxonomy_leaf_authorization.to_payload()
|
|
1293
|
+
context: JsonObject = {
|
|
1294
|
+
"schema": "medical-notes-workbench.process-chats-fsm-diagnostic-context.v1",
|
|
1295
|
+
"reason": projection.reason.value,
|
|
1296
|
+
"outcome_reason": projection.reason.value,
|
|
1297
|
+
"state": projection.state.value,
|
|
1298
|
+
"publish": publish_context,
|
|
1299
|
+
"counts": {
|
|
1300
|
+
"note_count": summary.note_count,
|
|
1301
|
+
"raw_count": summary.raw_count,
|
|
1302
|
+
"coverage_raw_count": summary.coverage_raw_count,
|
|
1303
|
+
"linker_applied": linker.applied,
|
|
1304
|
+
},
|
|
1305
|
+
"linker": {
|
|
1306
|
+
"status": linker.status,
|
|
1307
|
+
"diagnosis_status": linker.diagnosis_status,
|
|
1308
|
+
"applied": linker.applied,
|
|
1309
|
+
"skipped_reason": linker.skipped_reason,
|
|
1310
|
+
"blocker_count": linker.blocker_count,
|
|
1311
|
+
},
|
|
1312
|
+
}
|
|
1313
|
+
return diagnostic_context_evidence_only(context)
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
def _agent_directive(
|
|
1317
|
+
projection: _ProcessChatsMachineProjection,
|
|
1318
|
+
*,
|
|
1319
|
+
progress_view_model: WorkflowProgressViewModel,
|
|
1320
|
+
user_visible_summary: str,
|
|
1321
|
+
) -> JsonObject:
|
|
1322
|
+
"""Build the root executable agent contract directly from FSM state."""
|
|
1323
|
+
|
|
1324
|
+
directive_instructions: list[str] = []
|
|
1325
|
+
if projection.reason == ProcessChatsOutcomeReason.RECOVERABLE_BLOCKED:
|
|
1326
|
+
plan = _recoverable_blocker_plan(projection.reason_code or projection.trigger)
|
|
1327
|
+
if plan is None:
|
|
1328
|
+
raise ValueError("recoverable process-chats diagnostic context requires a recovery plan")
|
|
1329
|
+
directive_instructions = list(plan.directive_instructions)
|
|
1330
|
+
typed = agent_directive_from_progress_view_model(
|
|
1331
|
+
progress_view_model,
|
|
1332
|
+
schema=MEDNOTES_AGENT_DIRECTIVE_SCHEMA,
|
|
1333
|
+
reason=projection.reason.value,
|
|
1334
|
+
effects=projection.effects,
|
|
1335
|
+
blockers=_blocked_by_for_directive(projection),
|
|
1336
|
+
resume=projection.resume_action,
|
|
1337
|
+
report_requires=["primary_objective", "raw_coverage", "manifest", "linker"],
|
|
1338
|
+
summary=user_visible_summary,
|
|
1339
|
+
instructions=_plain_agent_directive_instructions(directive_instructions),
|
|
1340
|
+
)
|
|
1341
|
+
return JsonObjectAdapter.validate_python(typed.to_payload())
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def _problem_diagnostic_context(
|
|
1345
|
+
context: JsonObject,
|
|
1346
|
+
projection: _ProcessChatsMachineProjection,
|
|
1347
|
+
summary: ProcessChatsOperationalSummary,
|
|
1348
|
+
) -> JsonObject:
|
|
1349
|
+
publish = summary.publish
|
|
1350
|
+
linker = summary.linker
|
|
1351
|
+
if projection.status == WorkflowProgressStatus.COMPLETED and publish.dry_run is True:
|
|
1352
|
+
return diagnostic_context_evidence_only(context)
|
|
1353
|
+
if projection.status == WorkflowProgressStatus.COMPLETED:
|
|
1354
|
+
linker_deviation = linker.applied is not True or bool(linker.skipped_reason.strip())
|
|
1355
|
+
if linker_deviation:
|
|
1356
|
+
return diagnostic_context_evidence_only(context)
|
|
1357
|
+
return {}
|
|
1358
|
+
return diagnostic_context_evidence_only(context)
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
def _blocked_by_for_directive(projection: _ProcessChatsMachineProjection) -> list[str]:
|
|
1362
|
+
if projection.status not in {
|
|
1363
|
+
WorkflowProgressStatus.BLOCKED,
|
|
1364
|
+
WorkflowProgressStatus.FAILED,
|
|
1365
|
+
WorkflowProgressStatus.WAITING_EXTERNAL,
|
|
1366
|
+
WorkflowProgressStatus.WAITING_HUMAN,
|
|
1367
|
+
}:
|
|
1368
|
+
return []
|
|
1369
|
+
if projection.decision is not None:
|
|
1370
|
+
return [projection.decision.reason_code]
|
|
1371
|
+
return [projection.trigger or projection.reason.value]
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
def _plain_agent_directive_instructions(lines: list[str]) -> list[str]:
|
|
1375
|
+
cleaned: list[str] = []
|
|
1376
|
+
for line in lines:
|
|
1377
|
+
text = line.strip()
|
|
1378
|
+
prefix = "agent_instruction:"
|
|
1379
|
+
if text.casefold().startswith(prefix):
|
|
1380
|
+
text = text[len(prefix):].strip()
|
|
1381
|
+
if text:
|
|
1382
|
+
cleaned.append(text)
|
|
1383
|
+
return cleaned
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
def assert_process_chats_fsm_payload(payload: JsonObject) -> None:
|
|
1387
|
+
forbidden_keys = set(payload) & PROCESS_CHATS_FORBIDDEN_ROOT_KEYS
|
|
1388
|
+
if forbidden_keys:
|
|
1389
|
+
raise ValueError(f"process-chats FSM payload contains non-FSM root fields: {sorted(forbidden_keys)}")
|
|
1390
|
+
required_root_keys = PROCESS_CHATS_ALLOWED_ROOT_KEYS - {"diagnostic_context"}
|
|
1391
|
+
missing_keys = required_root_keys - set(payload)
|
|
1392
|
+
if missing_keys:
|
|
1393
|
+
raise ValueError(f"process-chats FSM payload missing canonical root fields: {sorted(missing_keys)}")
|
|
1394
|
+
unexpected_keys = set(payload) - PROCESS_CHATS_ALLOWED_ROOT_KEYS
|
|
1395
|
+
if unexpected_keys:
|
|
1396
|
+
raise ValueError(f"process-chats FSM payload contains unexpected root fields: {sorted(unexpected_keys)}")
|
|
1397
|
+
diagnostic_context = payload["diagnostic_context"] if "diagnostic_context" in payload else {}
|
|
1398
|
+
assert_diagnostic_context_evidence_only(diagnostic_context)
|
|
1399
|
+
if isinstance(diagnostic_context, dict) and "agent_directive" in diagnostic_context:
|
|
1400
|
+
raise ValueError("process-chats FSM diagnostic_context must not contain agent_directive")
|
|
1401
|
+
fields = _process_chats_payload_fields(payload)
|
|
1402
|
+
if fields.progress_view_model.status != fields.state_machine_snapshot.current_category:
|
|
1403
|
+
raise ValueError("process-chats FSM status must match state_machine_snapshot category")
|
|
1404
|
+
if fields.receipt.status != fields.progress_view_model.status:
|
|
1405
|
+
raise ValueError("process-chats FSM receipt status must match progress view status")
|
|
1406
|
+
if fields.progress_view_model.status in {
|
|
1407
|
+
WorkflowStateCategory.BLOCKED.value,
|
|
1408
|
+
WorkflowStateCategory.FAILED.value,
|
|
1409
|
+
} and not payload["error_context"]:
|
|
1410
|
+
raise ValueError("process-chats FSM blocked/failed payload requires error_context")
|
|
1411
|
+
reports = WorkflowReports.model_validate(payload["reports"])
|
|
1412
|
+
if "human" in payload["reports"]:
|
|
1413
|
+
raise ValueError("process-chats FSM reports must not expose legacy human report text")
|
|
1414
|
+
public_report = reports.public_report
|
|
1415
|
+
snapshot = WorkflowStateMachineSnapshot.model_validate(payload["state_machine_snapshot"])
|
|
1416
|
+
progress_view_model = WorkflowProgressViewModel.model_validate(payload["progress_view_model"])
|
|
1417
|
+
assert_public_report_matches_progress(
|
|
1418
|
+
public_report,
|
|
1419
|
+
workflow=PROCESS_CHATS_WORKFLOW,
|
|
1420
|
+
run_id=str(payload["run_id"]),
|
|
1421
|
+
progress_view_model=progress_view_model,
|
|
1422
|
+
label="process-chats FSM",
|
|
1423
|
+
)
|
|
1424
|
+
assert_agent_directive_matches_progress(
|
|
1425
|
+
AgentDirective.model_validate(payload[PROCESS_CHATS_AGENT_DIRECTIVE_FIELD]),
|
|
1426
|
+
workflow=PROCESS_CHATS_WORKFLOW,
|
|
1427
|
+
run_id=str(payload["run_id"]),
|
|
1428
|
+
progress_view_model=progress_view_model,
|
|
1429
|
+
snapshot=snapshot,
|
|
1430
|
+
allowed_effect_kinds=_allowed_agent_effect_kinds_for_category(snapshot.current_category),
|
|
1431
|
+
label="process-chats FSM",
|
|
1432
|
+
)
|
|
1433
|
+
_assert_process_chats_machine_snapshot(snapshot)
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
def _assert_process_chats_machine_snapshot(snapshot: WorkflowStateMachineSnapshot) -> None:
|
|
1437
|
+
if snapshot.workflow != PROCESS_CHATS_WORKFLOW:
|
|
1438
|
+
raise ValueError("process-chats FSM snapshot has invalid workflow")
|
|
1439
|
+
if snapshot.current_category != _machine_category_for_state(snapshot.current_state):
|
|
1440
|
+
raise ValueError("process-chats FSM snapshot category does not match StateChart state")
|
|
1441
|
+
edges = _process_chats_machine_edges()
|
|
1442
|
+
for transition in snapshot.transitions:
|
|
1443
|
+
if transition.to_category != _machine_category_for_state(transition.to_state):
|
|
1444
|
+
raise ValueError("process-chats FSM transition category does not match StateChart state")
|
|
1445
|
+
edge = (transition.trigger, transition.from_state, transition.to_state)
|
|
1446
|
+
if edge not in edges:
|
|
1447
|
+
raise ValueError(f"unauthorized FSM transition: {edge}")
|
|
1448
|
+
|
|
1449
|
+
|
|
1450
|
+
def _allowed_agent_effect_kinds_for_category(category: WorkflowStateCategory) -> set[WorkflowEffectKind]:
|
|
1451
|
+
"""Declare which executable effects may be projected to the agent per lane."""
|
|
1452
|
+
|
|
1453
|
+
match category:
|
|
1454
|
+
case WorkflowStateCategory.WAITING_AGENT:
|
|
1455
|
+
return {WorkflowEffectKind.CALL_SPECIALIST_MODEL, WorkflowEffectKind.RUN_SUBWORKFLOW}
|
|
1456
|
+
case WorkflowStateCategory.WAITING_EXTERNAL:
|
|
1457
|
+
return {WorkflowEffectKind.WAIT_EXTERNAL}
|
|
1458
|
+
case WorkflowStateCategory.WAITING_HUMAN:
|
|
1459
|
+
return {WorkflowEffectKind.ASK_HUMAN}
|
|
1460
|
+
case WorkflowStateCategory.BLOCKED | WorkflowStateCategory.FAILED:
|
|
1461
|
+
return {WorkflowEffectKind.RUN_SUBWORKFLOW}
|
|
1462
|
+
case _:
|
|
1463
|
+
return set()
|
|
1464
|
+
|
|
1465
|
+
|
|
1466
|
+
def _process_chats_machine_edges() -> set[tuple[str, str, str]]:
|
|
1467
|
+
edges: set[tuple[str, str, str]] = set()
|
|
1468
|
+
for event in ProcessChatsMachine.events:
|
|
1469
|
+
for transition in event._transitions:
|
|
1470
|
+
for target in transition._targets:
|
|
1471
|
+
edges.add((event.id, str(transition.source.value), str(target.value)))
|
|
1472
|
+
return edges
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def _process_chats_payload_fields(payload: JsonObject) -> _ProcessChatsPayloadFields:
|
|
1476
|
+
raw_fields: JsonObject = {
|
|
1477
|
+
"workflow": payload["workflow"],
|
|
1478
|
+
"progress_view_model": _json_object_subset(payload, "progress_view_model", ("status", "state")),
|
|
1479
|
+
"state_machine_snapshot": _json_object_subset(payload, "state_machine_snapshot", ("current_category",)),
|
|
1480
|
+
"receipt": _json_object_subset(payload, "receipt", ("status",)),
|
|
1481
|
+
}
|
|
1482
|
+
try:
|
|
1483
|
+
return _ProcessChatsPayloadFields.model_validate(raw_fields)
|
|
1484
|
+
except PydanticValidationError as exc:
|
|
1485
|
+
first = exc.errors()[0] if exc.errors() else {}
|
|
1486
|
+
loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
|
|
1487
|
+
msg = str(first.get("msg") or str(exc))
|
|
1488
|
+
raise ValueError(f"process-chats FSM payload invalid: {loc}: {msg}") from exc
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
def _json_object_subset(payload: JsonObject, field_name: str, keys: tuple[str, ...]) -> JsonObject:
|
|
1492
|
+
try:
|
|
1493
|
+
source = JsonObjectAdapter.validate_python(payload[field_name])
|
|
1494
|
+
except PydanticValidationError as exc:
|
|
1495
|
+
raise ValueError(f"process-chats FSM payload invalid: {field_name} must be an object") from exc
|
|
1496
|
+
return {key: source[key] for key in keys if key in source}
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
def process_chats_cli_exit_code(payload: JsonObject) -> int:
|
|
1500
|
+
fields = _process_chats_payload_fields(payload)
|
|
1501
|
+
status = fields.progress_view_model.status
|
|
1502
|
+
match status:
|
|
1503
|
+
case "completed" | "completed_with_warnings":
|
|
1504
|
+
return 0
|
|
1505
|
+
case "waiting_human" if (
|
|
1506
|
+
fields.progress_view_model.state == MachineProcessChatsState.PUBLISH_AWAITING_CONFIRMATION.value
|
|
1507
|
+
):
|
|
1508
|
+
return 0
|
|
1509
|
+
case "waiting_human" | "waiting_agent" | "blocked":
|
|
1510
|
+
return 3
|
|
1511
|
+
case "failed":
|
|
1512
|
+
return 5
|
|
1513
|
+
case _:
|
|
1514
|
+
return 1
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
def _blocked_item_count(facts: ProcessChatsFsmFacts, projection: _ProcessChatsMachineProjection) -> int:
|
|
1518
|
+
if projection.status not in {
|
|
1519
|
+
WorkflowProgressStatus.BLOCKED,
|
|
1520
|
+
WorkflowProgressStatus.FAILED,
|
|
1521
|
+
WorkflowProgressStatus.WAITING_HUMAN,
|
|
1522
|
+
WorkflowProgressStatus.WAITING_AGENT,
|
|
1523
|
+
}:
|
|
1524
|
+
return 0
|
|
1525
|
+
return max(1, facts.operational_summary.blocked_item_count)
|
|
1526
|
+
|
|
1527
|
+
|
|
1528
|
+
def _recoverable_blocker_plan(blocked_reason: str) -> ProcessChatsContinuationPlan | None:
|
|
1529
|
+
code = blocked_reason.strip()
|
|
1530
|
+
if not code:
|
|
1531
|
+
return None
|
|
1532
|
+
if code in {"ValidationError", "validation_errors", "validation_failed"}:
|
|
1533
|
+
lane, effect = _recovery_lane_and_effect(code)
|
|
1534
|
+
return ProcessChatsContinuationPlan(
|
|
1535
|
+
lane=lane,
|
|
1536
|
+
blocked_reason=code,
|
|
1537
|
+
next_effect=ProcessChatsContinuationEffect(kind=effect, blocked_reason=code),
|
|
1538
|
+
summary=_recovery_summary(code),
|
|
1539
|
+
directive_instructions=[
|
|
1540
|
+
"agent_instruction: workflow is waiting_agent, not completed.",
|
|
1541
|
+
"agent_instruction: repair or quarantine the invalid item before writing a final success report.",
|
|
1542
|
+
],
|
|
1543
|
+
)
|
|
1544
|
+
try:
|
|
1545
|
+
entry = blocker_entry(code)
|
|
1546
|
+
except BlockerRegistryError:
|
|
1547
|
+
return None
|
|
1548
|
+
if entry.default_decision not in {
|
|
1549
|
+
WorkflowDecisionKind.AUTO_FIX,
|
|
1550
|
+
WorkflowDecisionKind.AUTO_PLAN,
|
|
1551
|
+
WorkflowDecisionKind.AUTO_DEFER,
|
|
1552
|
+
}:
|
|
1553
|
+
return None
|
|
1554
|
+
lane, effect = _recovery_lane_and_effect(code)
|
|
1555
|
+
return ProcessChatsContinuationPlan(
|
|
1556
|
+
lane=lane,
|
|
1557
|
+
blocked_reason=code,
|
|
1558
|
+
next_effect=ProcessChatsContinuationEffect(kind=effect, blocked_reason=code),
|
|
1559
|
+
summary=_recovery_summary(code),
|
|
1560
|
+
directive_instructions=[
|
|
1561
|
+
"agent_instruction: workflow is waiting_agent, not completed.",
|
|
1562
|
+
"agent_instruction: run the official next_effect before writing a final success report.",
|
|
1563
|
+
],
|
|
1564
|
+
)
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
def _recovery_lane_and_effect(blocked_reason: str) -> tuple[str, str]:
|
|
1568
|
+
match blocked_reason:
|
|
1569
|
+
case "coverage_invalid" | "coverage_path_missing":
|
|
1570
|
+
return ("coverage_recovery", "regenerate_raw_coverage")
|
|
1571
|
+
case "dry_run_receipt_invalid" | "new_taxonomy_leaf_requires_dry_run_authorization":
|
|
1572
|
+
return ("publish_preview_recovery", "rerun_publish_preview")
|
|
1573
|
+
case "ValidationError" | "validation_errors" | "validation_failed":
|
|
1574
|
+
return ("note_validation_repair", "repair_or_quarantine_note")
|
|
1575
|
+
case "taxonomy_action_required" | "taxonomy_plan_blocked":
|
|
1576
|
+
return ("taxonomy_resolution", "resolve_taxonomy_or_quarantine")
|
|
1577
|
+
case _:
|
|
1578
|
+
return ("workflow_recovery", "recover_process_chats_blocker")
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
def _recovery_summary(blocked_reason: str) -> str:
|
|
1582
|
+
match blocked_reason:
|
|
1583
|
+
case "coverage_invalid" | "coverage_path_missing":
|
|
1584
|
+
return "Reconstruir a cobertura dos raw chats pela rota oficial e repetir a etapa de publicacao."
|
|
1585
|
+
case "dry_run_receipt_invalid" | "new_taxonomy_leaf_requires_dry_run_authorization":
|
|
1586
|
+
return "Gerar uma nova previa oficial de publicacao antes de aplicar o lote."
|
|
1587
|
+
case "ValidationError" | "validation_errors" | "validation_failed":
|
|
1588
|
+
return "Reparar ou quarentenar a nota invalida e continuar os demais itens seguros."
|
|
1589
|
+
case "taxonomy_action_required" | "taxonomy_plan_blocked":
|
|
1590
|
+
return "Resolver a taxonomia pela politica oficial antes de publicar o item afetado."
|
|
1591
|
+
case _:
|
|
1592
|
+
return "Recuperar o blocker pela rota oficial do process-chats antes de concluir."
|