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
package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/normalize.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Path and filename normalization helpers for Wiki taxonomy."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
import unicodedata
|
|
6
|
+
from pathlib import Path, PureWindowsPath
|
|
7
|
+
|
|
8
|
+
from mednotes.domains.wiki.common import ValidationError
|
|
9
|
+
|
|
10
|
+
_DRIVE_RE = re.compile(r"^[A-Za-z]:")
|
|
11
|
+
_UNSAFE_TITLE_RE = re.compile(r'[\\/*?:"<>|\x00-\x1f]')
|
|
12
|
+
_UNSAFE_TAXONOMY_RE = re.compile(r'[<>:"|?*\x00-\x1f]')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_taxonomy(taxonomy: str) -> tuple[str, ...]:
|
|
16
|
+
taxonomy = taxonomy.strip()
|
|
17
|
+
if not taxonomy:
|
|
18
|
+
raise ValidationError("Taxonomy cannot be empty")
|
|
19
|
+
if _DRIVE_RE.match(taxonomy):
|
|
20
|
+
raise ValidationError(f"Taxonomy must be relative, got drive path: {taxonomy}")
|
|
21
|
+
normalized = taxonomy.replace("\\", "/")
|
|
22
|
+
if normalized.startswith("/") or PureWindowsPath(normalized).is_absolute():
|
|
23
|
+
raise ValidationError(f"Taxonomy must be relative: {taxonomy}")
|
|
24
|
+
parts = tuple(part.strip() for part in normalized.split("/"))
|
|
25
|
+
if any(not part for part in parts):
|
|
26
|
+
raise ValidationError(f"Taxonomy has an empty segment: {taxonomy}")
|
|
27
|
+
if any(part in {".", ".."} for part in parts):
|
|
28
|
+
raise ValidationError(f"Taxonomy cannot contain '.' or '..': {taxonomy}")
|
|
29
|
+
bad = [part for part in parts if _UNSAFE_TAXONOMY_RE.search(part)]
|
|
30
|
+
if bad:
|
|
31
|
+
raise ValidationError(f"Taxonomy has unsafe characters: {bad[0]}")
|
|
32
|
+
folded = [_fold_taxonomy_segment(part) for part in parts]
|
|
33
|
+
empty = [part for part, folded_part in zip(parts, folded, strict=False) if not folded_part]
|
|
34
|
+
if empty:
|
|
35
|
+
raise ValidationError(f"Taxonomy segment must contain letters or numbers: {empty[0]}")
|
|
36
|
+
for idx in range(1, len(folded)):
|
|
37
|
+
if folded[idx] == folded[idx - 1]:
|
|
38
|
+
raise ValidationError(f"Taxonomy has duplicated adjacent segments: {parts[idx - 1]}/{parts[idx]}")
|
|
39
|
+
return parts
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def safe_title(title: str) -> str:
|
|
43
|
+
cleaned = _UNSAFE_TITLE_RE.sub("", title).strip().rstrip(".")
|
|
44
|
+
cleaned = re.sub(r"\s+", " ", cleaned)
|
|
45
|
+
if not cleaned:
|
|
46
|
+
raise ValidationError("Title produced an empty filename")
|
|
47
|
+
return cleaned
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _fold_taxonomy_segment(value: str) -> str:
|
|
51
|
+
decomposed = unicodedata.normalize("NFKD", value)
|
|
52
|
+
without_accents = "".join(char for char in decomposed if not unicodedata.combining(char))
|
|
53
|
+
return re.sub(r"[^a-z0-9]+", "", without_accents.casefold())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _safe_relative_dir(value: str) -> tuple[str, ...]:
|
|
57
|
+
normalized = value.replace("\\", "/").strip("/")
|
|
58
|
+
if not normalized:
|
|
59
|
+
raise ValidationError("Relative directory path cannot be empty")
|
|
60
|
+
if _DRIVE_RE.match(value) or Path(value).is_absolute() or PureWindowsPath(value).is_absolute():
|
|
61
|
+
raise ValidationError(f"Directory path must be relative: {value}")
|
|
62
|
+
parts = tuple(part.strip() for part in normalized.split("/"))
|
|
63
|
+
if any(not part for part in parts):
|
|
64
|
+
raise ValidationError(f"Directory path has an empty segment: {value}")
|
|
65
|
+
if any(part in {".", ".."} for part in parts):
|
|
66
|
+
raise ValidationError(f"Directory path cannot contain '.' or '..': {value}")
|
|
67
|
+
if any(part.startswith(".") for part in parts):
|
|
68
|
+
raise ValidationError(f"Directory path cannot contain hidden segments: {value}")
|
|
69
|
+
bad = [part for part in parts if _UNSAFE_TAXONOMY_RE.search(part)]
|
|
70
|
+
if bad:
|
|
71
|
+
raise ValidationError(f"Directory path has unsafe characters: {bad[0]}")
|
|
72
|
+
return parts
|
package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/policy.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Single source of truth for the Wiki_Medicina taxonomy policy."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
TAXONOMY_POLICY_VERSION = "2026-05-15.taxonomy-v1"
|
|
8
|
+
|
|
9
|
+
TaxonomyAliasKind = Literal["area", "specialty"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class TaxonomyAliasPolicy:
|
|
14
|
+
alias: str
|
|
15
|
+
canonical_target: tuple[str, ...]
|
|
16
|
+
kind: TaxonomyAliasKind
|
|
17
|
+
reason: str
|
|
18
|
+
migration_safe: bool = True
|
|
19
|
+
requires_human_review: bool = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class TaxonomySpecialtyPolicy:
|
|
24
|
+
name: str
|
|
25
|
+
aliases: tuple[str, ...] = ()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class TaxonomyAreaPolicy:
|
|
30
|
+
name: str
|
|
31
|
+
aliases: tuple[str, ...] = ()
|
|
32
|
+
specialties: tuple[TaxonomySpecialtyPolicy, ...] = ()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
CANONICAL_TAXONOMY_POLICY: tuple[TaxonomyAreaPolicy, ...] = (
|
|
36
|
+
TaxonomyAreaPolicy(
|
|
37
|
+
name="1. Clínica Médica",
|
|
38
|
+
aliases=("Clinica Medica", "Clínica Médica"),
|
|
39
|
+
specialties=(
|
|
40
|
+
TaxonomySpecialtyPolicy("Cardiologia"),
|
|
41
|
+
TaxonomySpecialtyPolicy("Dermatologia"),
|
|
42
|
+
TaxonomySpecialtyPolicy("Endocrinologia"),
|
|
43
|
+
TaxonomySpecialtyPolicy("Gastroenterologia"),
|
|
44
|
+
TaxonomySpecialtyPolicy("Geriatria"),
|
|
45
|
+
TaxonomySpecialtyPolicy("Hematologia"),
|
|
46
|
+
TaxonomySpecialtyPolicy("Imunologia"),
|
|
47
|
+
TaxonomySpecialtyPolicy("Infectologia"),
|
|
48
|
+
TaxonomySpecialtyPolicy("Medicina Interna", aliases=("Medicina Interna",)),
|
|
49
|
+
TaxonomySpecialtyPolicy("Nefrologia"),
|
|
50
|
+
TaxonomySpecialtyPolicy("Neurologia"),
|
|
51
|
+
TaxonomySpecialtyPolicy("Nutrologia"),
|
|
52
|
+
TaxonomySpecialtyPolicy("Oncologia"),
|
|
53
|
+
TaxonomySpecialtyPolicy("Pneumologia"),
|
|
54
|
+
TaxonomySpecialtyPolicy("Reumatologia"),
|
|
55
|
+
TaxonomySpecialtyPolicy("Semiologia"),
|
|
56
|
+
TaxonomySpecialtyPolicy("Psiquiatria"),
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
TaxonomyAreaPolicy(
|
|
60
|
+
name="2. Cirurgia",
|
|
61
|
+
aliases=("Cirurgia",),
|
|
62
|
+
specialties=(
|
|
63
|
+
TaxonomySpecialtyPolicy("Cirurgia Geral", aliases=("Cirurgia_Geral", "Cirurgia Geral")),
|
|
64
|
+
TaxonomySpecialtyPolicy("Clínica Cirúrgica", aliases=("Clinica Cirurgica", "Clínica Cirúrgica")),
|
|
65
|
+
TaxonomySpecialtyPolicy("Oftalmologia"),
|
|
66
|
+
TaxonomySpecialtyPolicy("Urologia"),
|
|
67
|
+
TaxonomySpecialtyPolicy("Trauma"),
|
|
68
|
+
TaxonomySpecialtyPolicy("Anestesiologia"),
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
TaxonomyAreaPolicy(
|
|
72
|
+
name="3. Ginecologia e Obstetrícia",
|
|
73
|
+
aliases=(
|
|
74
|
+
"#. Ginecologia e Obstetricia",
|
|
75
|
+
"3. Ginecologia e Obstetricia",
|
|
76
|
+
"Ginecologia_Obstetricia",
|
|
77
|
+
"Ginecologia e Obstetricia",
|
|
78
|
+
"Ginecologia e Obstetrícia",
|
|
79
|
+
),
|
|
80
|
+
specialties=(
|
|
81
|
+
TaxonomySpecialtyPolicy("Ginecologia", aliases=("Ginecologia",)),
|
|
82
|
+
TaxonomySpecialtyPolicy("Obstetrícia", aliases=("Obstetricia",)),
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
TaxonomyAreaPolicy(
|
|
86
|
+
name="4. Pediatria",
|
|
87
|
+
aliases=("Pediatria",),
|
|
88
|
+
specialties=(
|
|
89
|
+
TaxonomySpecialtyPolicy("Pediatria"),
|
|
90
|
+
TaxonomySpecialtyPolicy("Neonatologia"),
|
|
91
|
+
TaxonomySpecialtyPolicy("Puericultura"),
|
|
92
|
+
TaxonomySpecialtyPolicy(
|
|
93
|
+
"Infecto Pediátrica",
|
|
94
|
+
aliases=("Infecto Pediatrica", "Infecto Pediátrica", "Infectopediatria"),
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
TaxonomyAreaPolicy(
|
|
99
|
+
name="5. Medicina Preventiva",
|
|
100
|
+
aliases=("Medicina Preventiva",),
|
|
101
|
+
specialties=(
|
|
102
|
+
TaxonomySpecialtyPolicy("Medicina Preventiva"),
|
|
103
|
+
TaxonomySpecialtyPolicy("SUS"),
|
|
104
|
+
TaxonomySpecialtyPolicy("Epidemiologia"),
|
|
105
|
+
TaxonomySpecialtyPolicy("Ética Médica", aliases=("Etica Medica", "Ética Médica")),
|
|
106
|
+
TaxonomySpecialtyPolicy("Saúde do Trabalho", aliases=("Saude do Trabalho", "Saúde do Trabalho")),
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def iter_taxonomy_aliases() -> tuple[TaxonomyAliasPolicy, ...]:
|
|
113
|
+
aliases: list[TaxonomyAliasPolicy] = []
|
|
114
|
+
for area in CANONICAL_TAXONOMY_POLICY:
|
|
115
|
+
aliases.extend(
|
|
116
|
+
TaxonomyAliasPolicy(
|
|
117
|
+
alias=alias,
|
|
118
|
+
canonical_target=(area.name,),
|
|
119
|
+
kind="area",
|
|
120
|
+
reason="legacy_no_accent" if "Obstetricia" in alias else "legacy_short_name",
|
|
121
|
+
)
|
|
122
|
+
for alias in area.aliases
|
|
123
|
+
)
|
|
124
|
+
for specialty in area.specialties:
|
|
125
|
+
aliases.extend(
|
|
126
|
+
TaxonomyAliasPolicy(
|
|
127
|
+
alias=alias,
|
|
128
|
+
canonical_target=(area.name, specialty.name),
|
|
129
|
+
kind="specialty",
|
|
130
|
+
reason="legacy_no_accent" if "Obstetricia" in alias else "legacy_specialty_alias",
|
|
131
|
+
)
|
|
132
|
+
for alias in specialty.aliases
|
|
133
|
+
if alias != specialty.name
|
|
134
|
+
)
|
|
135
|
+
return tuple(aliases)
|
package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/resolve.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Taxonomy resolution against the existing Wiki tree."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import difflib
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from mednotes.domains.wiki.capabilities.vocabulary.taxonomy.normalize import (
|
|
9
|
+
_fold_taxonomy_segment,
|
|
10
|
+
normalize_taxonomy,
|
|
11
|
+
safe_title,
|
|
12
|
+
)
|
|
13
|
+
from mednotes.domains.wiki.capabilities.vocabulary.taxonomy.schema import (
|
|
14
|
+
CANONICAL_TAXONOMY,
|
|
15
|
+
TaxonomyResolution,
|
|
16
|
+
_canonical_area_aliases_by_fold,
|
|
17
|
+
_canonical_specialties_by_fold,
|
|
18
|
+
_canonical_specialties_for_root,
|
|
19
|
+
)
|
|
20
|
+
from mednotes.domains.wiki.common import MissingPathError, ValidationError
|
|
21
|
+
from mednotes.domains.wiki.contracts.workflow_outcomes import DecisionEvidence, RejectedAutomation, WorkflowDecision
|
|
22
|
+
from mednotes.kernel.base import JsonObject, JsonObjectAdapter
|
|
23
|
+
|
|
24
|
+
_NEAR_DUPLICATE_CUTOFF = 0.9
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TaxonomyDecisionRequired(ValidationError):
|
|
28
|
+
"""Taxonomy could be resolved only after a human choice."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, packet: JsonObject) -> None:
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
self.human_decision_packet = packet
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _taxonomy_option(value: str, *, consequence: str = "") -> JsonObject:
|
|
36
|
+
option: JsonObject = {
|
|
37
|
+
"id": _fold_taxonomy_segment(value).replace(" ", "_") or "taxonomy_option",
|
|
38
|
+
"label": value,
|
|
39
|
+
"value": value,
|
|
40
|
+
}
|
|
41
|
+
if consequence:
|
|
42
|
+
option["consequence"] = consequence
|
|
43
|
+
return option
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _taxonomy_decision_packet(
|
|
47
|
+
*,
|
|
48
|
+
kind: str,
|
|
49
|
+
question: str,
|
|
50
|
+
options: Sequence[JsonObject | str],
|
|
51
|
+
resume_action: str,
|
|
52
|
+
target_key: str,
|
|
53
|
+
context: JsonObject | None = None,
|
|
54
|
+
recommended_option_id: str | None = None,
|
|
55
|
+
) -> JsonObject:
|
|
56
|
+
context_payload = JsonObjectAdapter.validate_python({"context": context or {}})["context"]
|
|
57
|
+
context_object = JsonObjectAdapter.validate_python(context_payload)
|
|
58
|
+
clean_options = [_normalize_taxonomy_decision_option(option, index) for index, option in enumerate(options, start=1)]
|
|
59
|
+
recommended = recommended_option_id or (clean_options[0]["id"] if clean_options else "choose_existing_taxonomy")
|
|
60
|
+
decision = WorkflowDecision(
|
|
61
|
+
kind="ask_human",
|
|
62
|
+
phase="taxonomy-resolve",
|
|
63
|
+
reason_code="taxonomy_resolution_required",
|
|
64
|
+
public_summary=question,
|
|
65
|
+
developer_summary=f"Taxonomy resolution for {target_key} remains ambiguous after canonical matching.",
|
|
66
|
+
evidence=[
|
|
67
|
+
DecisionEvidence(
|
|
68
|
+
summary=f"Taxonomia solicitada: {target_key}.",
|
|
69
|
+
technical_code="taxonomy_resolution_required",
|
|
70
|
+
source="taxonomy-resolve",
|
|
71
|
+
candidates=[{"target_key": target_key, **context_object}],
|
|
72
|
+
risk="Mover automaticamente pode arquivar a nota em categoria clínica errada.",
|
|
73
|
+
)
|
|
74
|
+
],
|
|
75
|
+
rejected_automations=[
|
|
76
|
+
RejectedAutomation(kind="auto_fix", reason_code="ambiguous_taxonomy_target", reason="Nao ha categoria canonica unica para corrigir automaticamente."),
|
|
77
|
+
RejectedAutomation(kind="auto_defer", reason_code="blocks_note_path", reason="Sem taxonomia resolvida nao ha caminho seguro para a nota."),
|
|
78
|
+
RejectedAutomation(kind="auto_plan", reason_code="plan_needs_human_choice", reason="O plano nao consegue escolher entre categorias plausiveis sem informacao externa."),
|
|
79
|
+
],
|
|
80
|
+
next_action=resume_action,
|
|
81
|
+
resume_action=resume_action,
|
|
82
|
+
recommended_option_id=recommended,
|
|
83
|
+
options=clean_options,
|
|
84
|
+
)
|
|
85
|
+
packet = decision.to_human_decision_packet()
|
|
86
|
+
packet["kind"] = kind
|
|
87
|
+
packet["type"] = kind
|
|
88
|
+
packet["target_kind"] = "taxonomy"
|
|
89
|
+
packet["target_key"] = target_key
|
|
90
|
+
packet_context = packet.setdefault("context", {})
|
|
91
|
+
if isinstance(packet_context, dict):
|
|
92
|
+
packet_context.update(context_object)
|
|
93
|
+
return JsonObjectAdapter.validate_python(packet)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _normalize_taxonomy_decision_option(option: JsonObject | str, index: int) -> JsonObject:
|
|
97
|
+
if isinstance(option, dict):
|
|
98
|
+
option_payload = JsonObjectAdapter.validate_python(option)
|
|
99
|
+
label = str(option_payload.get("label") or option_payload.get("value") or option_payload.get("id") or f"Opção {index}")
|
|
100
|
+
option_id = str(option_payload.get("id") or _fold_taxonomy_segment(label).replace(" ", "_") or f"option_{index}")
|
|
101
|
+
clean: JsonObject = {"id": option_id, "label": label}
|
|
102
|
+
for key in ("description", "consequence", "value", "resume_action"):
|
|
103
|
+
value = option_payload.get(key)
|
|
104
|
+
if value:
|
|
105
|
+
clean[key] = str(value)
|
|
106
|
+
return JsonObjectAdapter.validate_python(clean)
|
|
107
|
+
label = str(option)
|
|
108
|
+
return {
|
|
109
|
+
"id": _fold_taxonomy_segment(label).replace(" ", "_") or f"option_{index}",
|
|
110
|
+
"label": label,
|
|
111
|
+
"value": label,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _decision_guidance_for_root(root: str) -> list[str]:
|
|
116
|
+
if root == "3. Ginecologia e Obstetrícia":
|
|
117
|
+
return [
|
|
118
|
+
"Use Ginecologia para doenças, rastreio e cuidado ginecológico fora de gestação.",
|
|
119
|
+
"Use Obstetrícia para gestação, parto, puerpério e cuidado fetal/materno gestacional.",
|
|
120
|
+
]
|
|
121
|
+
return ["Escolha a especialidade canônica mais específica para o conteúdo da nota."]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _canonicalize_taxonomy_parts(parts: tuple[str, ...]) -> tuple[tuple[str, ...], tuple[dict[str, str], ...]]:
|
|
125
|
+
roots = _canonical_area_aliases_by_fold()
|
|
126
|
+
specialties = _canonical_specialties_by_fold()
|
|
127
|
+
first = parts[0]
|
|
128
|
+
first_folded = _fold_taxonomy_segment(first)
|
|
129
|
+
canonicalized: list[JsonObject] = []
|
|
130
|
+
|
|
131
|
+
if first_folded in roots:
|
|
132
|
+
root = roots[first_folded]
|
|
133
|
+
if len(parts) == 1:
|
|
134
|
+
raise ValidationError(f"Taxonomy must include a specialty under canonical area: {root}")
|
|
135
|
+
root_specialties = _canonical_specialties_for_root(root)
|
|
136
|
+
second = parts[1]
|
|
137
|
+
second_folded = _fold_taxonomy_segment(second)
|
|
138
|
+
if second_folded not in root_specialties:
|
|
139
|
+
options = [_taxonomy_option(value, consequence="Usar especialidade canônica existente.") for value in root_specialties.values()]
|
|
140
|
+
raise TaxonomyDecisionRequired(
|
|
141
|
+
f"Unknown specialty under {root}: {second}",
|
|
142
|
+
_taxonomy_decision_packet(
|
|
143
|
+
kind="taxonomy_specialty_required",
|
|
144
|
+
question=f"Qual especialidade canônica sob {root} deve receber '{second}'?",
|
|
145
|
+
options=options,
|
|
146
|
+
resume_action="Reexecutar taxonomy-resolve/stage-note com a especialidade escolhida.",
|
|
147
|
+
target_key="/".join(parts),
|
|
148
|
+
context={"root": root, "requested": second, "decision_guidance": _decision_guidance_for_root(root)},
|
|
149
|
+
recommended_option_id=options[0]["id"] if options else None,
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
specialty = root_specialties[second_folded]
|
|
153
|
+
canonical_parts = (root, specialty, *parts[2:])
|
|
154
|
+
if canonical_parts[:2] != parts[:2]:
|
|
155
|
+
canonicalized.append({"from": "/".join(parts[:2]), "to": "/".join(canonical_parts[:2]), "under": ""})
|
|
156
|
+
return canonical_parts, tuple(canonicalized)
|
|
157
|
+
|
|
158
|
+
if first_folded in specialties:
|
|
159
|
+
root, specialty = specialties[first_folded]
|
|
160
|
+
canonical_parts = (root, specialty, *parts[1:])
|
|
161
|
+
canonicalized.append({"from": first, "to": "/".join(canonical_parts[:2]), "under": ""})
|
|
162
|
+
return canonical_parts, tuple(canonicalized)
|
|
163
|
+
|
|
164
|
+
root_names = ", ".join(root for root, _specialties in CANONICAL_TAXONOMY)
|
|
165
|
+
options: list[JsonObject] = []
|
|
166
|
+
for root, specialties in CANONICAL_TAXONOMY:
|
|
167
|
+
options.append(_taxonomy_option(root, consequence="Escolher área canônica e depois uma especialidade."))
|
|
168
|
+
options.extend(
|
|
169
|
+
_taxonomy_option(specialty, consequence=f"Usar {root}/{specialty}.")
|
|
170
|
+
for specialty in specialties[:3]
|
|
171
|
+
)
|
|
172
|
+
raise TaxonomyDecisionRequired(
|
|
173
|
+
f"Taxonomy must start with a canonical area or known specialty. Got: {first}. "
|
|
174
|
+
f"Canonical areas: {root_names}",
|
|
175
|
+
_taxonomy_decision_packet(
|
|
176
|
+
kind="taxonomy_root_required",
|
|
177
|
+
question=f"Qual área/especialidade canônica deve receber '{first}'?",
|
|
178
|
+
options=options[:12],
|
|
179
|
+
resume_action="Reexecutar taxonomy-resolve/stage-note com área ou especialidade canônica.",
|
|
180
|
+
target_key="/".join(parts),
|
|
181
|
+
context={"requested": first},
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _visible_child_dirs(path: Path) -> list[Path]:
|
|
187
|
+
if not path.exists():
|
|
188
|
+
return []
|
|
189
|
+
return sorted(
|
|
190
|
+
(child for child in path.iterdir() if child.is_dir() and not child.name.startswith(".")),
|
|
191
|
+
key=lambda child: _fold_taxonomy_segment(child.name),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _suggest_existing_segments(siblings: list[Path], requested: str) -> list[str]:
|
|
196
|
+
folded_to_names: dict[str, list[str]] = {}
|
|
197
|
+
for sibling in siblings:
|
|
198
|
+
folded_to_names.setdefault(_fold_taxonomy_segment(sibling.name), []).append(sibling.name)
|
|
199
|
+
requested_folded = _fold_taxonomy_segment(requested)
|
|
200
|
+
close = difflib.get_close_matches(requested_folded, list(folded_to_names), n=4, cutoff=_NEAR_DUPLICATE_CUTOFF)
|
|
201
|
+
suggestions: list[str] = []
|
|
202
|
+
for folded in close:
|
|
203
|
+
suggestions.extend(folded_to_names[folded])
|
|
204
|
+
return suggestions
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _format_suggestions(suggestions: list[str]) -> str:
|
|
208
|
+
if not suggestions:
|
|
209
|
+
return ""
|
|
210
|
+
return " Sugestões existentes: " + ", ".join(suggestions)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _match_existing_segment(parent: Path, requested: str) -> tuple[str | None, list[str]]:
|
|
214
|
+
siblings = _visible_child_dirs(parent)
|
|
215
|
+
exact = [sibling.name for sibling in siblings if sibling.name == requested]
|
|
216
|
+
if exact:
|
|
217
|
+
return exact[0], []
|
|
218
|
+
|
|
219
|
+
requested_folded = _fold_taxonomy_segment(requested)
|
|
220
|
+
folded_matches = [sibling.name for sibling in siblings if _fold_taxonomy_segment(sibling.name) == requested_folded]
|
|
221
|
+
if len(folded_matches) == 1:
|
|
222
|
+
return folded_matches[0], []
|
|
223
|
+
if len(folded_matches) > 1:
|
|
224
|
+
raise TaxonomyDecisionRequired(
|
|
225
|
+
f"Taxonomy segment is ambiguous under {parent}: {requested}. Matches: {', '.join(folded_matches)}",
|
|
226
|
+
_taxonomy_decision_packet(
|
|
227
|
+
kind="taxonomy_ambiguous_segment",
|
|
228
|
+
question=f"Qual pasta existente sob {parent} corresponde a '{requested}'?",
|
|
229
|
+
options=[
|
|
230
|
+
_taxonomy_option(match, consequence="Usar esta pasta existente.")
|
|
231
|
+
for match in folded_matches
|
|
232
|
+
],
|
|
233
|
+
resume_action="Reexecutar taxonomy-resolve/stage-note com o segmento escolhido exatamente.",
|
|
234
|
+
target_key=requested,
|
|
235
|
+
context={"parent": str(parent), "matches": folded_matches},
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
return None, _suggest_existing_segments(siblings, requested)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _validate_taxonomy_not_title(parts: tuple[str, ...], title: str) -> None:
|
|
242
|
+
title_key = _fold_taxonomy_segment(safe_title(title))
|
|
243
|
+
if parts and _fold_taxonomy_segment(parts[-1]) == title_key:
|
|
244
|
+
raise TaxonomyDecisionRequired(
|
|
245
|
+
"Taxonomy must be the folder/category path only; do not repeat the note title "
|
|
246
|
+
f"as the final folder: taxonomy {'/'.join(parts)} + title {title}",
|
|
247
|
+
_taxonomy_decision_packet(
|
|
248
|
+
kind="taxonomy_title_repeated",
|
|
249
|
+
question="A taxonomia inclui o título da nota como pasta final. Qual correção usar?",
|
|
250
|
+
options=[
|
|
251
|
+
{
|
|
252
|
+
"id": "remove_title_folder",
|
|
253
|
+
"label": "Remover o título da taxonomia",
|
|
254
|
+
"value": "/".join(parts[:-1]),
|
|
255
|
+
"consequence": "Manter o título apenas como arquivo Markdown.",
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
resume_action="Reexecutar stage-note usando taxonomy sem repetir o título.",
|
|
259
|
+
target_key="/".join(parts),
|
|
260
|
+
context={"title": title, "taxonomy": "/".join(parts)},
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def resolve_taxonomy(
|
|
266
|
+
wiki_dir: Path,
|
|
267
|
+
taxonomy: str,
|
|
268
|
+
*,
|
|
269
|
+
title: str | None = None,
|
|
270
|
+
allow_new_leaf: bool = True,
|
|
271
|
+
) -> TaxonomyResolution:
|
|
272
|
+
requested_parts = normalize_taxonomy(taxonomy)
|
|
273
|
+
canonical_request_parts, alias_canonicalized = _canonicalize_taxonomy_parts(requested_parts)
|
|
274
|
+
if title is not None:
|
|
275
|
+
_validate_taxonomy_not_title(canonical_request_parts, title)
|
|
276
|
+
if not wiki_dir.exists():
|
|
277
|
+
raise MissingPathError(f"Wiki dir not found: {wiki_dir}")
|
|
278
|
+
if not wiki_dir.is_dir():
|
|
279
|
+
raise ValidationError(f"Wiki dir is not a directory: {wiki_dir}")
|
|
280
|
+
|
|
281
|
+
canonical_parts: list[str] = []
|
|
282
|
+
canonicalized: list[JsonObject] = [JsonObjectAdapter.validate_python(item) for item in alias_canonicalized]
|
|
283
|
+
new_dirs: list[str] = []
|
|
284
|
+
parent = wiki_dir
|
|
285
|
+
|
|
286
|
+
for idx, requested in enumerate(canonical_request_parts):
|
|
287
|
+
is_leaf = idx == len(canonical_request_parts) - 1
|
|
288
|
+
matched, suggestions = _match_existing_segment(parent, requested)
|
|
289
|
+
if matched is None:
|
|
290
|
+
if idx < 2:
|
|
291
|
+
canonical_parts.append(requested)
|
|
292
|
+
new_dirs.append("/".join(canonical_parts))
|
|
293
|
+
parent = parent / requested
|
|
294
|
+
continue
|
|
295
|
+
if is_leaf and allow_new_leaf and canonical_parts:
|
|
296
|
+
if suggestions:
|
|
297
|
+
options = [
|
|
298
|
+
_taxonomy_option(
|
|
299
|
+
suggestion,
|
|
300
|
+
consequence="Usar pasta parecida existente em vez de criar leaf nova.",
|
|
301
|
+
)
|
|
302
|
+
for suggestion in suggestions
|
|
303
|
+
]
|
|
304
|
+
options.append(
|
|
305
|
+
{
|
|
306
|
+
"id": "create_new_leaf",
|
|
307
|
+
"label": f"Criar nova pasta '{requested}'",
|
|
308
|
+
"value": requested,
|
|
309
|
+
"consequence": "Autorizar leaf nova apesar da semelhança; revisar taxonomia depois.",
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
raise TaxonomyDecisionRequired(
|
|
313
|
+
f"New taxonomy leaf '{requested}' under {'/'.join(canonical_parts)} is too similar to "
|
|
314
|
+
f"an existing folder.{_format_suggestions(suggestions)}",
|
|
315
|
+
_taxonomy_decision_packet(
|
|
316
|
+
kind="taxonomy_new_leaf_similar",
|
|
317
|
+
question=f"Criar nova pasta '{requested}' ou usar uma pasta existente?",
|
|
318
|
+
options=options,
|
|
319
|
+
resume_action="Escolher pasta existente ou confirmar leaf nova e reexecutar stage-note.",
|
|
320
|
+
target_key="/".join((*canonical_parts, requested)),
|
|
321
|
+
context={
|
|
322
|
+
"parent": "/".join(canonical_parts),
|
|
323
|
+
"requested": requested,
|
|
324
|
+
"suggestions": suggestions,
|
|
325
|
+
"decision_guidance": [
|
|
326
|
+
"Prefira pasta existente quando a diferença for só plural, acento, caixa, underscore ou sinônimo próximo.",
|
|
327
|
+
"Crie leaf nova apenas quando o conceito for realmente diferente e o pai canônico estiver correto.",
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
canonical_parts.append(requested)
|
|
333
|
+
new_dirs.append("/".join(canonical_parts))
|
|
334
|
+
parent = parent / requested
|
|
335
|
+
continue
|
|
336
|
+
location = "/".join(canonical_parts) if canonical_parts else "<wiki-root>"
|
|
337
|
+
options = [
|
|
338
|
+
_taxonomy_option(suggestion, consequence="Usar pasta existente.")
|
|
339
|
+
for suggestion in suggestions
|
|
340
|
+
]
|
|
341
|
+
if not options:
|
|
342
|
+
options.append(
|
|
343
|
+
{
|
|
344
|
+
"id": "choose_existing_taxonomy",
|
|
345
|
+
"label": "Escolher pasta existente",
|
|
346
|
+
"value": location,
|
|
347
|
+
"consequence": "Listar taxonomy-tree e repetir com caminho válido.",
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
if is_leaf and canonical_parts:
|
|
351
|
+
options.append(
|
|
352
|
+
{
|
|
353
|
+
"id": "allow_new_taxonomy_leaf",
|
|
354
|
+
"label": f"Autorizar nova pasta '{requested}'",
|
|
355
|
+
"value": requested,
|
|
356
|
+
"consequence": "Reexecutar com --allow-new-taxonomy-leaf quando essa for a decisão explícita.",
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
raise TaxonomyDecisionRequired(
|
|
360
|
+
f"Taxonomy segment must already exist under {location}: {requested}."
|
|
361
|
+
f"{_format_suggestions(suggestions)}",
|
|
362
|
+
_taxonomy_decision_packet(
|
|
363
|
+
kind="taxonomy_segment_missing",
|
|
364
|
+
question=f"Qual pasta deve substituir '{requested}' sob {location}?",
|
|
365
|
+
options=options,
|
|
366
|
+
resume_action="Reexecutar taxonomy-resolve/stage-note com uma taxonomia existente ou decisão explícita de nova leaf.",
|
|
367
|
+
target_key="/".join((*canonical_parts, requested)),
|
|
368
|
+
context={"parent": location, "requested": requested, "suggestions": suggestions},
|
|
369
|
+
),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if matched != requested:
|
|
373
|
+
canonicalized.append({"from": requested, "to": matched, "under": "/".join(canonical_parts)})
|
|
374
|
+
canonical_parts.append(matched)
|
|
375
|
+
parent = parent / matched
|
|
376
|
+
|
|
377
|
+
resolved_parts = tuple(canonical_parts)
|
|
378
|
+
if title is not None:
|
|
379
|
+
_validate_taxonomy_not_title(resolved_parts, title)
|
|
380
|
+
return TaxonomyResolution(
|
|
381
|
+
requested_taxonomy="/".join(requested_parts),
|
|
382
|
+
taxonomy="/".join(resolved_parts),
|
|
383
|
+
parts=resolved_parts,
|
|
384
|
+
canonicalized=tuple(canonicalized),
|
|
385
|
+
new_dirs=tuple(new_dirs),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def resolve_target_for_note(
|
|
390
|
+
wiki_dir: Path,
|
|
391
|
+
taxonomy: str,
|
|
392
|
+
title: str,
|
|
393
|
+
*,
|
|
394
|
+
allow_new_taxonomy_leaf: bool = True,
|
|
395
|
+
) -> tuple[Path, TaxonomyResolution]:
|
|
396
|
+
resolution = resolve_taxonomy(wiki_dir, taxonomy, title=title, allow_new_leaf=allow_new_taxonomy_leaf)
|
|
397
|
+
return wiki_dir.joinpath(*resolution.parts, f"{safe_title(title)}.md"), resolution
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def target_for_note(
|
|
401
|
+
wiki_dir: Path,
|
|
402
|
+
taxonomy: str,
|
|
403
|
+
title: str,
|
|
404
|
+
*,
|
|
405
|
+
allow_new_taxonomy_leaf: bool = True,
|
|
406
|
+
) -> Path:
|
|
407
|
+
target, _resolution = resolve_target_for_note(
|
|
408
|
+
wiki_dir,
|
|
409
|
+
taxonomy,
|
|
410
|
+
title,
|
|
411
|
+
allow_new_taxonomy_leaf=allow_new_taxonomy_leaf,
|
|
412
|
+
)
|
|
413
|
+
return target
|