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/vocabulary_map.py
ADDED
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
"""Vocabulary DB and YAML-claim diagnosis for Wiki link semantics."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TypeAlias, TypedDict
|
|
10
|
+
|
|
11
|
+
from mednotes.domains.wiki.batch_state import file_sha256
|
|
12
|
+
from mednotes.domains.wiki.capabilities.notes.note_iter import iter_notes
|
|
13
|
+
from mednotes.domains.wiki.capabilities.notes.note_style.frontmatter import infer_title
|
|
14
|
+
from mednotes.domains.wiki.capabilities.vocabulary.link_terms import extract_aliases, normalize_key
|
|
15
|
+
from mednotes.domains.wiki.capabilities.vocabulary.link_terms import is_index_note as _is_index_note
|
|
16
|
+
from mednotes.domains.wiki.common import wiki_cli_command
|
|
17
|
+
from mednotes.domains.wiki.contracts.workflow_blockers import decision_for_code
|
|
18
|
+
from mednotes.kernel.base import JsonObject
|
|
19
|
+
|
|
20
|
+
VOCABULARY_MAP_SCHEMA = "medical-notes-workbench.vocabulary-map.v1"
|
|
21
|
+
VocabularyHashRowValue: TypeAlias = str | int | float | None
|
|
22
|
+
VocabularyHashPayload: TypeAlias = dict[str, list[dict[str, VocabularyHashRowValue]]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VocabularyIssuePayload(TypedDict, total=False):
|
|
26
|
+
severity: str
|
|
27
|
+
code: str
|
|
28
|
+
message: str
|
|
29
|
+
phase: str
|
|
30
|
+
note_path: str
|
|
31
|
+
surface: str
|
|
32
|
+
next_action: str
|
|
33
|
+
required_inputs: list[str]
|
|
34
|
+
stale_count: int
|
|
35
|
+
surface_count: int
|
|
36
|
+
meaning_id: str
|
|
37
|
+
label: str
|
|
38
|
+
decision_summary: JsonObject
|
|
39
|
+
display_text: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class VocabularyDiagnosisPayload(TypedDict):
|
|
43
|
+
schema: str
|
|
44
|
+
status: str
|
|
45
|
+
db_path: str
|
|
46
|
+
map_hash: str
|
|
47
|
+
note_count: int
|
|
48
|
+
meaning_count: int
|
|
49
|
+
surface_count: int
|
|
50
|
+
ambiguous_surface_count: int
|
|
51
|
+
pending_semantic_ingestion_count: int
|
|
52
|
+
issues: list[VocabularyIssuePayload]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class KnownMeaningSeed:
|
|
57
|
+
surface: str
|
|
58
|
+
meaning: str
|
|
59
|
+
note_title: str = ""
|
|
60
|
+
semantic_type: str = "medical_concept"
|
|
61
|
+
intrinsically_ambiguous: bool = False
|
|
62
|
+
ambiguity_reason: str = ""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class AliasClaim:
|
|
67
|
+
note_path: str
|
|
68
|
+
alias_text: str
|
|
69
|
+
normalized_surface: str
|
|
70
|
+
claim_status: str
|
|
71
|
+
link_policy: str
|
|
72
|
+
visible_in_yaml: bool = True
|
|
73
|
+
meaning_ids: tuple[str, ...] = ()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class SurfaceInfo:
|
|
78
|
+
normalized_surface: str
|
|
79
|
+
best_display_text: str
|
|
80
|
+
intrinsically_ambiguous: bool
|
|
81
|
+
direct_link_allowed: bool
|
|
82
|
+
link_policy: str
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class VocabularyBlocker:
|
|
87
|
+
code: str
|
|
88
|
+
message: str
|
|
89
|
+
note_path: str = ""
|
|
90
|
+
surface: str = ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class ProjectionAlias:
|
|
95
|
+
text: str
|
|
96
|
+
normalized_surface: str
|
|
97
|
+
link_policy: str
|
|
98
|
+
visible_in_yaml: bool
|
|
99
|
+
source: str
|
|
100
|
+
order: int
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class VocabularyMap:
|
|
105
|
+
schema: str = VOCABULARY_MAP_SCHEMA
|
|
106
|
+
db_path: Path | None = None
|
|
107
|
+
alias_claims: list[AliasClaim] = field(default_factory=list)
|
|
108
|
+
surfaces: dict[str, SurfaceInfo] = field(default_factory=dict)
|
|
109
|
+
blockers: list[VocabularyBlocker] = field(default_factory=list)
|
|
110
|
+
note_aliases: dict[str, list[ProjectionAlias]] = field(default_factory=dict)
|
|
111
|
+
map_hash: str = ""
|
|
112
|
+
note_count: int = 0
|
|
113
|
+
meaning_count: int = 0
|
|
114
|
+
surface_count: int = 0
|
|
115
|
+
ambiguous_surface_count: int = 0
|
|
116
|
+
pending_semantic_ingestion_count: int = 0
|
|
117
|
+
issues: list[VocabularyIssuePayload] = field(default_factory=list)
|
|
118
|
+
|
|
119
|
+
def as_diagnosis_dict(self) -> VocabularyDiagnosisPayload:
|
|
120
|
+
human_codes = {
|
|
121
|
+
"vocabulary_map.duplicate_meaning",
|
|
122
|
+
"vocabulary_map.non_atomic_note",
|
|
123
|
+
"vocabulary_map.conflicting_alias",
|
|
124
|
+
}
|
|
125
|
+
has_human_issue = any(
|
|
126
|
+
issue.get("severity") == "human_decision" or issue.get("code") in human_codes
|
|
127
|
+
for issue in self.issues
|
|
128
|
+
)
|
|
129
|
+
if has_human_issue:
|
|
130
|
+
status = "blocked_human"
|
|
131
|
+
elif self.pending_semantic_ingestion_count > 0 or any(
|
|
132
|
+
issue.get("severity") == "blocker" for issue in self.issues
|
|
133
|
+
):
|
|
134
|
+
status = "blocked_pending"
|
|
135
|
+
else:
|
|
136
|
+
status = "ready"
|
|
137
|
+
return {
|
|
138
|
+
"schema": self.schema,
|
|
139
|
+
"status": status,
|
|
140
|
+
"db_path": str(self.db_path) if self.db_path else "",
|
|
141
|
+
"map_hash": self.map_hash,
|
|
142
|
+
"note_count": self.note_count,
|
|
143
|
+
"meaning_count": self.meaning_count,
|
|
144
|
+
"surface_count": self.surface_count,
|
|
145
|
+
"ambiguous_surface_count": self.ambiguous_surface_count,
|
|
146
|
+
"pending_semantic_ingestion_count": self.pending_semantic_ingestion_count,
|
|
147
|
+
"issues": self.issues,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def meaning_id_for(label: str) -> str:
|
|
152
|
+
normalized = normalize_key(label).replace(" ", "_")
|
|
153
|
+
return "meaning:" + (normalized or "unknown")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def initialize_vocabulary_db(db_path: Path) -> None:
|
|
157
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
with sqlite3.connect(db_path) as conn:
|
|
159
|
+
conn.executescript(
|
|
160
|
+
"""
|
|
161
|
+
PRAGMA foreign_keys = ON;
|
|
162
|
+
|
|
163
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
164
|
+
id INTEGER PRIMARY KEY,
|
|
165
|
+
path TEXT NOT NULL UNIQUE,
|
|
166
|
+
title TEXT NOT NULL,
|
|
167
|
+
stem TEXT NOT NULL,
|
|
168
|
+
content_hash TEXT NOT NULL,
|
|
169
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'deleted', 'renamed', 'merged')),
|
|
170
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
171
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
CREATE TABLE IF NOT EXISTS meanings (
|
|
175
|
+
id TEXT PRIMARY KEY,
|
|
176
|
+
label TEXT NOT NULL,
|
|
177
|
+
normalized_label TEXT NOT NULL,
|
|
178
|
+
semantic_type TEXT NOT NULL DEFAULT '',
|
|
179
|
+
atomic_status TEXT NOT NULL CHECK (atomic_status IN ('atomic', 'suspected_non_atomic', 'duplicate_candidate', 'unknown')),
|
|
180
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'retired', 'needs_review')),
|
|
181
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
182
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
CREATE TABLE IF NOT EXISTS surfaces (
|
|
186
|
+
id INTEGER PRIMARY KEY,
|
|
187
|
+
normalized_surface TEXT NOT NULL UNIQUE,
|
|
188
|
+
best_display_text TEXT NOT NULL,
|
|
189
|
+
intrinsically_ambiguous INTEGER NOT NULL DEFAULT 0,
|
|
190
|
+
ambiguity_reason TEXT NOT NULL DEFAULT '',
|
|
191
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
192
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
CREATE TABLE IF NOT EXISTS meaning_note_links (
|
|
196
|
+
id INTEGER PRIMARY KEY,
|
|
197
|
+
meaning_id TEXT NOT NULL REFERENCES meanings(id),
|
|
198
|
+
note_id INTEGER NOT NULL REFERENCES notes(id),
|
|
199
|
+
role TEXT NOT NULL CHECK (role IN ('canonical', 'alias_target', 'historical')),
|
|
200
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'retired', 'needs_review')),
|
|
201
|
+
confidence REAL NOT NULL DEFAULT 0,
|
|
202
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
203
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
204
|
+
UNIQUE(meaning_id, note_id, role)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
CREATE UNIQUE INDEX IF NOT EXISTS one_active_canonical_note_per_meaning
|
|
208
|
+
ON meaning_note_links(meaning_id)
|
|
209
|
+
WHERE role = 'canonical' AND status = 'active';
|
|
210
|
+
|
|
211
|
+
CREATE UNIQUE INDEX IF NOT EXISTS one_active_primary_meaning_per_note
|
|
212
|
+
ON meaning_note_links(note_id)
|
|
213
|
+
WHERE role = 'canonical' AND status = 'active';
|
|
214
|
+
|
|
215
|
+
CREATE TABLE IF NOT EXISTS surface_meaning_policy (
|
|
216
|
+
id INTEGER PRIMARY KEY,
|
|
217
|
+
surface_id INTEGER NOT NULL REFERENCES surfaces(id),
|
|
218
|
+
meaning_id TEXT NOT NULL REFERENCES meanings(id),
|
|
219
|
+
link_policy TEXT NOT NULL CHECK (link_policy IN ('direct', 'requires_context', 'blocked', 'no_link')),
|
|
220
|
+
visible_in_yaml INTEGER NOT NULL DEFAULT 1,
|
|
221
|
+
display_text TEXT NOT NULL DEFAULT '',
|
|
222
|
+
source TEXT NOT NULL CHECK (source IN ('curator', 'yaml', 'projection', 'human', 'llm', 'system')),
|
|
223
|
+
confidence REAL NOT NULL DEFAULT 0,
|
|
224
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
225
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
226
|
+
UNIQUE(surface_id, meaning_id)
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
CREATE TABLE IF NOT EXISTS note_semantic_ingestion_queue (
|
|
230
|
+
id INTEGER PRIMARY KEY,
|
|
231
|
+
note_id INTEGER NOT NULL REFERENCES notes(id),
|
|
232
|
+
note_path TEXT NOT NULL,
|
|
233
|
+
content_hash TEXT NOT NULL,
|
|
234
|
+
queue_flags_json TEXT NOT NULL,
|
|
235
|
+
assigned_agent TEXT NOT NULL,
|
|
236
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'claimed', 'applied', 'blocked', 'stale')),
|
|
237
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
238
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
239
|
+
UNIQUE(note_path, content_hash)
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
CREATE TABLE IF NOT EXISTS yaml_alias_claims (
|
|
243
|
+
id INTEGER PRIMARY KEY,
|
|
244
|
+
note_id INTEGER NOT NULL REFERENCES notes(id),
|
|
245
|
+
alias_text TEXT NOT NULL,
|
|
246
|
+
normalized_surface TEXT NOT NULL,
|
|
247
|
+
note_hash TEXT NOT NULL,
|
|
248
|
+
source TEXT NOT NULL CHECK (source IN ('yaml', 'projection', 'human', 'llm')),
|
|
249
|
+
claim_status TEXT NOT NULL CHECK (
|
|
250
|
+
claim_status IN ('accepted_alias', 'contextual_alias', 'duplicate_alias', 'conflicting_alias', 'stale_alias')
|
|
251
|
+
),
|
|
252
|
+
link_policy TEXT NOT NULL CHECK (link_policy IN ('direct', 'requires_context', 'blocked', 'no_link')),
|
|
253
|
+
visible_in_yaml INTEGER NOT NULL DEFAULT 1,
|
|
254
|
+
first_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
255
|
+
last_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
256
|
+
UNIQUE(note_id, normalized_surface)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
CREATE TABLE IF NOT EXISTS deferred_work_items (
|
|
260
|
+
work_id TEXT PRIMARY KEY,
|
|
261
|
+
source_agent TEXT NOT NULL,
|
|
262
|
+
assigned_agent TEXT NOT NULL,
|
|
263
|
+
reason TEXT NOT NULL,
|
|
264
|
+
note_path TEXT,
|
|
265
|
+
content_hash TEXT,
|
|
266
|
+
payload_json TEXT NOT NULL,
|
|
267
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'claimed', 'completed', 'blocked', 'cancelled')),
|
|
268
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
269
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
CREATE TABLE IF NOT EXISTS contextual_alias_decisions (
|
|
273
|
+
occurrence_id TEXT PRIMARY KEY,
|
|
274
|
+
note_path TEXT NOT NULL,
|
|
275
|
+
normalized_surface TEXT NOT NULL,
|
|
276
|
+
matched_text TEXT NOT NULL,
|
|
277
|
+
context_hash TEXT NOT NULL,
|
|
278
|
+
candidate_targets_json TEXT NOT NULL,
|
|
279
|
+
action TEXT NOT NULL CHECK (action IN ('link', 'no_link', 'defer')),
|
|
280
|
+
chosen_meaning_id TEXT NOT NULL DEFAULT '',
|
|
281
|
+
chosen_target_path TEXT NOT NULL DEFAULT '',
|
|
282
|
+
chosen_target TEXT NOT NULL DEFAULT '',
|
|
283
|
+
confidence REAL NOT NULL DEFAULT 0,
|
|
284
|
+
model TEXT NOT NULL DEFAULT '',
|
|
285
|
+
response_hash TEXT NOT NULL DEFAULT '',
|
|
286
|
+
reason_code TEXT NOT NULL DEFAULT '',
|
|
287
|
+
rationale_summary TEXT NOT NULL DEFAULT '',
|
|
288
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'rejected', 'stale')),
|
|
289
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
290
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
291
|
+
);
|
|
292
|
+
"""
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def note_content_hash(path: Path) -> str:
|
|
297
|
+
return "sha256:" + file_sha256(path)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def upsert_note(conn: sqlite3.Connection, *, path: Path, title: str, content_hash: str) -> int:
|
|
301
|
+
conn.execute(
|
|
302
|
+
"""
|
|
303
|
+
INSERT INTO notes(path, title, stem, content_hash, status)
|
|
304
|
+
VALUES (?, ?, ?, ?, 'active')
|
|
305
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
306
|
+
title=excluded.title,
|
|
307
|
+
stem=excluded.stem,
|
|
308
|
+
content_hash=excluded.content_hash,
|
|
309
|
+
status='active',
|
|
310
|
+
updated_at=CURRENT_TIMESTAMP
|
|
311
|
+
""",
|
|
312
|
+
(str(path), title, path.stem, content_hash),
|
|
313
|
+
)
|
|
314
|
+
row = conn.execute("SELECT id FROM notes WHERE path = ?", (str(path),)).fetchone()
|
|
315
|
+
if row is None: # pragma: no cover - sqlite invariant
|
|
316
|
+
raise RuntimeError(f"failed to upsert note: {path}")
|
|
317
|
+
return int(row[0])
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def upsert_meaning(
|
|
321
|
+
conn: sqlite3.Connection,
|
|
322
|
+
*,
|
|
323
|
+
meaning_id: str,
|
|
324
|
+
label: str,
|
|
325
|
+
semantic_type: str = "medical_concept",
|
|
326
|
+
atomic_status: str = "atomic",
|
|
327
|
+
) -> None:
|
|
328
|
+
conn.execute(
|
|
329
|
+
"""
|
|
330
|
+
INSERT INTO meanings(id, label, normalized_label, semantic_type, atomic_status, status)
|
|
331
|
+
VALUES (?, ?, ?, ?, ?, 'active')
|
|
332
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
333
|
+
label=excluded.label,
|
|
334
|
+
normalized_label=excluded.normalized_label,
|
|
335
|
+
semantic_type=excluded.semantic_type,
|
|
336
|
+
atomic_status=excluded.atomic_status,
|
|
337
|
+
status='active',
|
|
338
|
+
updated_at=CURRENT_TIMESTAMP
|
|
339
|
+
""",
|
|
340
|
+
(meaning_id, label, normalize_key(label), semantic_type, atomic_status),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def upsert_surface(
|
|
345
|
+
conn: sqlite3.Connection,
|
|
346
|
+
*,
|
|
347
|
+
display_text: str,
|
|
348
|
+
intrinsically_ambiguous: bool = False,
|
|
349
|
+
ambiguity_reason: str = "",
|
|
350
|
+
) -> int:
|
|
351
|
+
normalized = normalize_key(display_text)
|
|
352
|
+
row = conn.execute("SELECT id, intrinsically_ambiguous, best_display_text FROM surfaces WHERE normalized_surface = ?", (normalized,)).fetchone()
|
|
353
|
+
if row is None:
|
|
354
|
+
conn.execute(
|
|
355
|
+
"""
|
|
356
|
+
INSERT INTO surfaces(normalized_surface, best_display_text, intrinsically_ambiguous, ambiguity_reason)
|
|
357
|
+
VALUES (?, ?, ?, ?)
|
|
358
|
+
""",
|
|
359
|
+
(normalized, display_text, int(intrinsically_ambiguous), ambiguity_reason),
|
|
360
|
+
)
|
|
361
|
+
else:
|
|
362
|
+
existing_ambiguous = bool(row[1])
|
|
363
|
+
best = _best_display_text(str(row[2]), display_text)
|
|
364
|
+
conn.execute(
|
|
365
|
+
"""
|
|
366
|
+
UPDATE surfaces
|
|
367
|
+
SET best_display_text = ?, intrinsically_ambiguous = ?, ambiguity_reason = CASE WHEN ? != '' THEN ? ELSE ambiguity_reason END,
|
|
368
|
+
updated_at = CURRENT_TIMESTAMP
|
|
369
|
+
WHERE normalized_surface = ?
|
|
370
|
+
""",
|
|
371
|
+
(best, int(existing_ambiguous or intrinsically_ambiguous), ambiguity_reason, ambiguity_reason, normalized),
|
|
372
|
+
)
|
|
373
|
+
row = conn.execute("SELECT id FROM surfaces WHERE normalized_surface = ?", (normalized,)).fetchone()
|
|
374
|
+
if row is None: # pragma: no cover
|
|
375
|
+
raise RuntimeError(f"failed to upsert surface: {display_text}")
|
|
376
|
+
return int(row[0])
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def upsert_policy(
|
|
380
|
+
conn: sqlite3.Connection,
|
|
381
|
+
*,
|
|
382
|
+
surface_id: int,
|
|
383
|
+
meaning_id: str,
|
|
384
|
+
link_policy: str,
|
|
385
|
+
display_text: str,
|
|
386
|
+
visible_in_yaml: bool = True,
|
|
387
|
+
source: str = "system",
|
|
388
|
+
confidence: float = 0.0,
|
|
389
|
+
) -> None:
|
|
390
|
+
conn.execute(
|
|
391
|
+
"""
|
|
392
|
+
INSERT INTO surface_meaning_policy(
|
|
393
|
+
surface_id, meaning_id, link_policy, visible_in_yaml, display_text, source, confidence
|
|
394
|
+
)
|
|
395
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
396
|
+
ON CONFLICT(surface_id, meaning_id) DO UPDATE SET
|
|
397
|
+
link_policy=excluded.link_policy,
|
|
398
|
+
visible_in_yaml=excluded.visible_in_yaml,
|
|
399
|
+
display_text=excluded.display_text,
|
|
400
|
+
source=excluded.source,
|
|
401
|
+
confidence=excluded.confidence,
|
|
402
|
+
updated_at=CURRENT_TIMESTAMP
|
|
403
|
+
""",
|
|
404
|
+
(surface_id, meaning_id, link_policy, int(visible_in_yaml), display_text, source, confidence),
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _best_display_text(left: str, right: str) -> str:
|
|
409
|
+
def score(value: str) -> tuple[int, int, int]:
|
|
410
|
+
return (
|
|
411
|
+
int(any(ord(char) > 127 for char in value)),
|
|
412
|
+
int(value.isupper() and len(value) <= 8),
|
|
413
|
+
sum(1 for char in value if char.isupper()),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return max([left, right], key=score)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _scan_notes(wiki_dir: Path) -> list[tuple[Path, str, str]]:
|
|
420
|
+
notes: list[tuple[Path, str, str]] = []
|
|
421
|
+
if not wiki_dir.exists():
|
|
422
|
+
return notes
|
|
423
|
+
for path in iter_notes(wiki_dir):
|
|
424
|
+
text = path.read_text(encoding="utf-8")
|
|
425
|
+
if _is_index_note(path, text):
|
|
426
|
+
continue
|
|
427
|
+
notes.append((path, infer_title(text, path), text))
|
|
428
|
+
return notes
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _seed_meaning_id(seed: KnownMeaningSeed) -> str:
|
|
432
|
+
return meaning_id_for(seed.meaning)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def rebuild_vocabulary_map(
|
|
436
|
+
*,
|
|
437
|
+
wiki_dir: Path,
|
|
438
|
+
db_path: Path,
|
|
439
|
+
import_yaml_aliases: bool,
|
|
440
|
+
known_meanings: list[KnownMeaningSeed] | None = None,
|
|
441
|
+
) -> VocabularyMap:
|
|
442
|
+
initialize_vocabulary_db(db_path)
|
|
443
|
+
seeds = known_meanings or []
|
|
444
|
+
result = VocabularyMap(db_path=db_path)
|
|
445
|
+
notes = _scan_notes(wiki_dir)
|
|
446
|
+
note_ids: dict[Path, int] = {}
|
|
447
|
+
with sqlite3.connect(db_path) as conn:
|
|
448
|
+
for path, title, _text in notes:
|
|
449
|
+
note_ids[path] = upsert_note(conn, path=path, title=title, content_hash=note_content_hash(path))
|
|
450
|
+
|
|
451
|
+
for seed in seeds:
|
|
452
|
+
meaning_id = _seed_meaning_id(seed)
|
|
453
|
+
upsert_meaning(conn, meaning_id=meaning_id, label=seed.meaning, semantic_type=seed.semantic_type)
|
|
454
|
+
surface_id = upsert_surface(
|
|
455
|
+
conn,
|
|
456
|
+
display_text=seed.surface,
|
|
457
|
+
intrinsically_ambiguous=seed.intrinsically_ambiguous,
|
|
458
|
+
ambiguity_reason=seed.ambiguity_reason,
|
|
459
|
+
)
|
|
460
|
+
upsert_policy(
|
|
461
|
+
conn,
|
|
462
|
+
surface_id=surface_id,
|
|
463
|
+
meaning_id=meaning_id,
|
|
464
|
+
link_policy="requires_context" if seed.intrinsically_ambiguous else "direct",
|
|
465
|
+
display_text=seed.surface,
|
|
466
|
+
source="system",
|
|
467
|
+
)
|
|
468
|
+
if seed.note_title:
|
|
469
|
+
matching = [path for path, title, _text in notes if normalize_key(title) == normalize_key(seed.note_title) or normalize_key(path.stem) == normalize_key(seed.note_title)]
|
|
470
|
+
for path in matching:
|
|
471
|
+
try:
|
|
472
|
+
conn.execute(
|
|
473
|
+
"""
|
|
474
|
+
INSERT INTO meaning_note_links(meaning_id, note_id, role, status, confidence)
|
|
475
|
+
VALUES (?, ?, 'canonical', 'active', 1.0)
|
|
476
|
+
ON CONFLICT(meaning_id, note_id, role) DO UPDATE SET status='active', updated_at=CURRENT_TIMESTAMP
|
|
477
|
+
""",
|
|
478
|
+
(meaning_id, note_ids[path]),
|
|
479
|
+
)
|
|
480
|
+
except sqlite3.IntegrityError:
|
|
481
|
+
result.blockers.append(
|
|
482
|
+
VocabularyBlocker(
|
|
483
|
+
code="vocabulary_map.duplicate_meaning",
|
|
484
|
+
message=f"Meaning {seed.meaning} maps to more than one active canonical note.",
|
|
485
|
+
note_path=str(path),
|
|
486
|
+
surface=seed.surface,
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
if import_yaml_aliases:
|
|
491
|
+
_import_yaml_alias_claims(conn, result, notes, note_ids, seeds)
|
|
492
|
+
|
|
493
|
+
_load_surface_info(conn, result)
|
|
494
|
+
return result
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _import_yaml_alias_claims(
|
|
498
|
+
conn: sqlite3.Connection,
|
|
499
|
+
result: VocabularyMap,
|
|
500
|
+
notes: list[tuple[Path, str, str]],
|
|
501
|
+
note_ids: dict[Path, int],
|
|
502
|
+
seeds: list[KnownMeaningSeed],
|
|
503
|
+
) -> None:
|
|
504
|
+
seeds_by_surface: dict[str, list[KnownMeaningSeed]] = {}
|
|
505
|
+
for seed in seeds:
|
|
506
|
+
seeds_by_surface.setdefault(normalize_key(seed.surface), []).append(seed)
|
|
507
|
+
|
|
508
|
+
for path, title, text in notes:
|
|
509
|
+
seen: set[str] = set()
|
|
510
|
+
projection_items: list[ProjectionAlias] = []
|
|
511
|
+
seed_order = 0
|
|
512
|
+
for seed in seeds:
|
|
513
|
+
if seed.note_title and normalize_key(seed.note_title) not in {normalize_key(title), normalize_key(path.stem)}:
|
|
514
|
+
continue
|
|
515
|
+
normalized = normalize_key(seed.surface)
|
|
516
|
+
if normalized in seen:
|
|
517
|
+
continue
|
|
518
|
+
seen.add(normalized)
|
|
519
|
+
policy = "requires_context" if _surface_requires_context(seeds_by_surface.get(normalized, [])) else "direct"
|
|
520
|
+
projection_items.append(
|
|
521
|
+
ProjectionAlias(
|
|
522
|
+
text=seed.surface,
|
|
523
|
+
normalized_surface=normalized,
|
|
524
|
+
link_policy=policy,
|
|
525
|
+
visible_in_yaml=True,
|
|
526
|
+
source="seed",
|
|
527
|
+
order=seed_order,
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
seed_order += 1
|
|
531
|
+
|
|
532
|
+
for alias in extract_aliases(text):
|
|
533
|
+
normalized = normalize_key(alias)
|
|
534
|
+
surface_seeds = seeds_by_surface.get(normalized, [])
|
|
535
|
+
claim_status, link_policy, blocker = _classify_alias_claim(alias, title, surface_seeds)
|
|
536
|
+
meaning_ids = tuple(_seed_meaning_id(seed) for seed in surface_seeds)
|
|
537
|
+
claim = AliasClaim(
|
|
538
|
+
note_path=str(path),
|
|
539
|
+
alias_text=alias,
|
|
540
|
+
normalized_surface=normalized,
|
|
541
|
+
claim_status=claim_status,
|
|
542
|
+
link_policy=link_policy,
|
|
543
|
+
meaning_ids=meaning_ids,
|
|
544
|
+
)
|
|
545
|
+
result.alias_claims.append(claim)
|
|
546
|
+
if blocker is not None:
|
|
547
|
+
result.blockers.append(VocabularyBlocker(**{**blocker, "note_path": str(path), "surface": alias}))
|
|
548
|
+
conn.execute(
|
|
549
|
+
"""
|
|
550
|
+
INSERT INTO yaml_alias_claims(note_id, alias_text, normalized_surface, note_hash, source, claim_status, link_policy, visible_in_yaml)
|
|
551
|
+
VALUES (?, ?, ?, ?, 'yaml', ?, ?, 1)
|
|
552
|
+
ON CONFLICT(note_id, normalized_surface) DO UPDATE SET
|
|
553
|
+
alias_text=excluded.alias_text,
|
|
554
|
+
note_hash=excluded.note_hash,
|
|
555
|
+
claim_status=excluded.claim_status,
|
|
556
|
+
link_policy=excluded.link_policy,
|
|
557
|
+
visible_in_yaml=1,
|
|
558
|
+
last_seen_at=CURRENT_TIMESTAMP
|
|
559
|
+
""",
|
|
560
|
+
(note_ids[path], alias, normalized, note_content_hash(path), claim_status, link_policy),
|
|
561
|
+
)
|
|
562
|
+
if normalized not in seen and claim_status != "conflicting_alias":
|
|
563
|
+
seen.add(normalized)
|
|
564
|
+
projection_items.append(
|
|
565
|
+
ProjectionAlias(
|
|
566
|
+
text=alias,
|
|
567
|
+
normalized_surface=normalized,
|
|
568
|
+
link_policy=link_policy,
|
|
569
|
+
visible_in_yaml=True,
|
|
570
|
+
source="yaml",
|
|
571
|
+
order=seed_order,
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
seed_order += 1
|
|
575
|
+
result.note_aliases[str(path)] = projection_items
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _classify_alias_claim(
|
|
579
|
+
alias: str,
|
|
580
|
+
note_title: str,
|
|
581
|
+
surface_seeds: list[KnownMeaningSeed],
|
|
582
|
+
) -> tuple[str, str, dict[str, str] | None]:
|
|
583
|
+
if not surface_seeds:
|
|
584
|
+
return "contextual_alias", "requires_context", None
|
|
585
|
+
meaning_to_titles: dict[str, set[str]] = {}
|
|
586
|
+
for seed in surface_seeds:
|
|
587
|
+
meaning_to_titles.setdefault(seed.meaning, set())
|
|
588
|
+
if seed.note_title:
|
|
589
|
+
meaning_to_titles[seed.meaning].add(seed.note_title)
|
|
590
|
+
if any(len(titles) > 1 for titles in meaning_to_titles.values()):
|
|
591
|
+
return (
|
|
592
|
+
"conflicting_alias",
|
|
593
|
+
"blocked",
|
|
594
|
+
{
|
|
595
|
+
"code": "vocabulary_map.duplicate_meaning",
|
|
596
|
+
"message": f"Alias {alias} maps one meaning to multiple canonical notes.",
|
|
597
|
+
},
|
|
598
|
+
)
|
|
599
|
+
if _surface_requires_context(surface_seeds):
|
|
600
|
+
return "contextual_alias", "requires_context", None
|
|
601
|
+
if len({seed.meaning for seed in surface_seeds}) == 1:
|
|
602
|
+
return "accepted_alias", "direct", None
|
|
603
|
+
return "contextual_alias", "requires_context", None
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _surface_requires_context(surface_seeds: list[KnownMeaningSeed]) -> bool:
|
|
607
|
+
if len({seed.meaning for seed in surface_seeds}) > 1:
|
|
608
|
+
return True
|
|
609
|
+
return any(seed.intrinsically_ambiguous for seed in surface_seeds)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _load_surface_info(conn: sqlite3.Connection, result: VocabularyMap) -> None:
|
|
613
|
+
rows = conn.execute(
|
|
614
|
+
"""
|
|
615
|
+
SELECT s.normalized_surface, s.best_display_text, s.intrinsically_ambiguous,
|
|
616
|
+
COUNT(DISTINCT p.meaning_id) AS meaning_count,
|
|
617
|
+
SUM(CASE WHEN p.link_policy = 'direct' THEN 1 ELSE 0 END) AS direct_count
|
|
618
|
+
FROM surfaces s
|
|
619
|
+
LEFT JOIN surface_meaning_policy p ON p.surface_id = s.id
|
|
620
|
+
GROUP BY s.id
|
|
621
|
+
"""
|
|
622
|
+
).fetchall()
|
|
623
|
+
for normalized, display, ambiguous, meaning_count, direct_count in rows:
|
|
624
|
+
link_policy = "direct" if direct_count and meaning_count == 1 and not ambiguous else "requires_context"
|
|
625
|
+
result.surfaces[str(normalized)] = SurfaceInfo(
|
|
626
|
+
normalized_surface=str(normalized),
|
|
627
|
+
best_display_text=str(display),
|
|
628
|
+
intrinsically_ambiguous=bool(ambiguous),
|
|
629
|
+
direct_link_allowed=link_policy == "direct",
|
|
630
|
+
link_policy=link_policy,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def pending_semantic_ingestion_count(db_path: Path) -> int:
|
|
635
|
+
if not db_path.exists():
|
|
636
|
+
return 0
|
|
637
|
+
with sqlite3.connect(db_path) as conn:
|
|
638
|
+
row = conn.execute("SELECT COUNT(*) FROM note_semantic_ingestion_queue WHERE status IN ('pending', 'claimed')").fetchone()
|
|
639
|
+
return int(row[0]) if row else 0
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _query_scalar_count(conn: sqlite3.Connection, sql: str, params: tuple[object, ...] = ()) -> int:
|
|
643
|
+
row = conn.execute(sql, params).fetchone()
|
|
644
|
+
return int(row[0]) if row else 0
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _hash_row_value(value: object) -> VocabularyHashRowValue:
|
|
648
|
+
if value is None or isinstance(value, str | int | float):
|
|
649
|
+
return value
|
|
650
|
+
return str(value)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def vocabulary_map_hash(db_path: Path) -> str:
|
|
654
|
+
initialize_vocabulary_db(db_path)
|
|
655
|
+
tables = (
|
|
656
|
+
"notes",
|
|
657
|
+
"meanings",
|
|
658
|
+
"surfaces",
|
|
659
|
+
"meaning_note_links",
|
|
660
|
+
"surface_meaning_policy",
|
|
661
|
+
"yaml_alias_claims",
|
|
662
|
+
"note_semantic_ingestion_queue",
|
|
663
|
+
"deferred_work_items",
|
|
664
|
+
)
|
|
665
|
+
payload: VocabularyHashPayload = {}
|
|
666
|
+
with sqlite3.connect(db_path) as conn:
|
|
667
|
+
conn.row_factory = sqlite3.Row
|
|
668
|
+
for table in tables:
|
|
669
|
+
columns = [str(row[1]) for row in conn.execute(f"PRAGMA table_info({table})").fetchall()]
|
|
670
|
+
stable_columns = [column for column in columns if not column.endswith("_at")]
|
|
671
|
+
order_by = ", ".join(stable_columns) if stable_columns else "rowid"
|
|
672
|
+
rows = conn.execute(f"SELECT * FROM {table} ORDER BY {order_by}").fetchall()
|
|
673
|
+
payload[table] = [
|
|
674
|
+
{key: _hash_row_value(row[key]) for key in row.keys() if not key.endswith("_at")}
|
|
675
|
+
for row in rows
|
|
676
|
+
]
|
|
677
|
+
encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
678
|
+
return "sha256:" + hashlib.sha256(encoded).hexdigest()
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _issue(
|
|
682
|
+
*,
|
|
683
|
+
severity: str,
|
|
684
|
+
code: str,
|
|
685
|
+
message: str,
|
|
686
|
+
phase: str = "vocabulary_map_diagnosis",
|
|
687
|
+
note_path: str = "",
|
|
688
|
+
surface: str = "",
|
|
689
|
+
next_action: str = "",
|
|
690
|
+
required_inputs: list[str] | None = None,
|
|
691
|
+
) -> VocabularyIssuePayload:
|
|
692
|
+
return {
|
|
693
|
+
"severity": severity,
|
|
694
|
+
"code": code,
|
|
695
|
+
"message": message,
|
|
696
|
+
"phase": phase,
|
|
697
|
+
"note_path": note_path,
|
|
698
|
+
"surface": surface,
|
|
699
|
+
"next_action": next_action,
|
|
700
|
+
"required_inputs": required_inputs or [],
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def _load_alias_claims(conn: sqlite3.Connection, result: VocabularyMap) -> None:
|
|
705
|
+
rows = conn.execute(
|
|
706
|
+
"""
|
|
707
|
+
SELECT n.path, c.alias_text, c.normalized_surface, c.claim_status, c.link_policy, c.visible_in_yaml
|
|
708
|
+
FROM yaml_alias_claims c
|
|
709
|
+
JOIN notes n ON n.id = c.note_id
|
|
710
|
+
ORDER BY n.path, c.normalized_surface
|
|
711
|
+
"""
|
|
712
|
+
).fetchall()
|
|
713
|
+
for note_path, alias_text, normalized_surface, claim_status, link_policy, visible in rows:
|
|
714
|
+
claim = AliasClaim(
|
|
715
|
+
note_path=str(note_path),
|
|
716
|
+
alias_text=str(alias_text),
|
|
717
|
+
normalized_surface=str(normalized_surface),
|
|
718
|
+
claim_status=str(claim_status),
|
|
719
|
+
link_policy=str(link_policy),
|
|
720
|
+
visible_in_yaml=bool(visible),
|
|
721
|
+
)
|
|
722
|
+
result.alias_claims.append(claim)
|
|
723
|
+
if claim.visible_in_yaml:
|
|
724
|
+
result.note_aliases.setdefault(claim.note_path, []).append(
|
|
725
|
+
ProjectionAlias(
|
|
726
|
+
text=claim.alias_text,
|
|
727
|
+
normalized_surface=claim.normalized_surface,
|
|
728
|
+
link_policy=claim.link_policy,
|
|
729
|
+
visible_in_yaml=True,
|
|
730
|
+
source="yaml",
|
|
731
|
+
order=len(result.note_aliases.get(claim.note_path, [])),
|
|
732
|
+
)
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _load_db_projection_aliases(conn: sqlite3.Connection, result: VocabularyMap) -> None:
|
|
737
|
+
rows = conn.execute(
|
|
738
|
+
"""
|
|
739
|
+
SELECT n.path,
|
|
740
|
+
COALESCE(NULLIF(p.display_text, ''), s.best_display_text) AS display_text,
|
|
741
|
+
s.normalized_surface,
|
|
742
|
+
p.link_policy,
|
|
743
|
+
p.visible_in_yaml,
|
|
744
|
+
p.source
|
|
745
|
+
FROM notes n
|
|
746
|
+
JOIN meaning_note_links l ON l.note_id = n.id
|
|
747
|
+
JOIN surface_meaning_policy p ON p.meaning_id = l.meaning_id
|
|
748
|
+
JOIN surfaces s ON s.id = p.surface_id
|
|
749
|
+
WHERE n.status = 'active'
|
|
750
|
+
AND l.role = 'canonical'
|
|
751
|
+
AND l.status = 'active'
|
|
752
|
+
AND p.visible_in_yaml = 1
|
|
753
|
+
AND p.link_policy IN ('direct', 'requires_context')
|
|
754
|
+
ORDER BY n.path, s.normalized_surface
|
|
755
|
+
"""
|
|
756
|
+
).fetchall()
|
|
757
|
+
for note_path, display_text, normalized_surface, link_policy, visible, source in rows:
|
|
758
|
+
items = result.note_aliases.setdefault(str(note_path), [])
|
|
759
|
+
items.append(
|
|
760
|
+
ProjectionAlias(
|
|
761
|
+
text=str(display_text),
|
|
762
|
+
normalized_surface=str(normalized_surface),
|
|
763
|
+
link_policy=str(link_policy),
|
|
764
|
+
visible_in_yaml=bool(visible),
|
|
765
|
+
source=str(source or "curator"),
|
|
766
|
+
order=len(items),
|
|
767
|
+
)
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def load_vocabulary_map_diagnosis(db_path: Path) -> VocabularyMap:
|
|
772
|
+
initialize_vocabulary_db(db_path)
|
|
773
|
+
result = VocabularyMap(db_path=db_path)
|
|
774
|
+
with sqlite3.connect(db_path) as conn:
|
|
775
|
+
result.note_count = _query_scalar_count(conn, "SELECT COUNT(*) FROM notes WHERE status = 'active'")
|
|
776
|
+
result.meaning_count = _query_scalar_count(conn, "SELECT COUNT(*) FROM meanings WHERE status = 'active'")
|
|
777
|
+
result.surface_count = _query_scalar_count(conn, "SELECT COUNT(*) FROM surfaces")
|
|
778
|
+
result.ambiguous_surface_count = _query_scalar_count(conn, "SELECT COUNT(*) FROM surfaces WHERE intrinsically_ambiguous = 1")
|
|
779
|
+
result.pending_semantic_ingestion_count = _query_scalar_count(
|
|
780
|
+
conn,
|
|
781
|
+
"SELECT COUNT(*) FROM note_semantic_ingestion_queue WHERE status IN ('pending', 'claimed')",
|
|
782
|
+
)
|
|
783
|
+
stale_semantic_ingestion_count = _query_scalar_count(
|
|
784
|
+
conn,
|
|
785
|
+
"SELECT COUNT(*) FROM note_semantic_ingestion_queue WHERE status='stale'",
|
|
786
|
+
)
|
|
787
|
+
_load_surface_info(conn, result)
|
|
788
|
+
_load_db_projection_aliases(conn, result)
|
|
789
|
+
_load_alias_claims(conn, result)
|
|
790
|
+
if result.pending_semantic_ingestion_count:
|
|
791
|
+
result.issues.append(
|
|
792
|
+
_issue(
|
|
793
|
+
severity="blocker",
|
|
794
|
+
code="vocabulary_semantic_ingestion_pending",
|
|
795
|
+
message="Semantic ingestion queue has pending notes.",
|
|
796
|
+
next_action="Processar note_semantic_ingestion_queue com med-link-graph-curator.",
|
|
797
|
+
required_inputs=["vocabulary_semantic_ingestion"],
|
|
798
|
+
)
|
|
799
|
+
)
|
|
800
|
+
if stale_semantic_ingestion_count:
|
|
801
|
+
recovery = wiki_cli_command("vocabulary-recover", "--mode", "reconcile-queue", "--dry-run", "--json")
|
|
802
|
+
result.issues.append(
|
|
803
|
+
_issue(
|
|
804
|
+
severity="blocker",
|
|
805
|
+
code="vocabulary_semantic_ingestion_stale",
|
|
806
|
+
message="Semantic ingestion queue has stale notes that must be refreshed before curation can continue.",
|
|
807
|
+
next_action=recovery,
|
|
808
|
+
required_inputs=["vocabulary_recovery", "vocabulary_semantic_ingestion"],
|
|
809
|
+
)
|
|
810
|
+
| {"stale_count": stale_semantic_ingestion_count}
|
|
811
|
+
)
|
|
812
|
+
unresolved_surface_count = _query_scalar_count(
|
|
813
|
+
conn,
|
|
814
|
+
"""
|
|
815
|
+
SELECT COUNT(*)
|
|
816
|
+
FROM surfaces s
|
|
817
|
+
LEFT JOIN surface_meaning_policy p ON p.surface_id = s.id
|
|
818
|
+
WHERE p.id IS NULL
|
|
819
|
+
""",
|
|
820
|
+
)
|
|
821
|
+
if unresolved_surface_count:
|
|
822
|
+
result.issues.append(
|
|
823
|
+
_issue(
|
|
824
|
+
severity="blocker",
|
|
825
|
+
code="vocabulary_map.unresolved_surfaces_without_meanings",
|
|
826
|
+
message="Vocabulary DB has surfaces without a meaning policy.",
|
|
827
|
+
next_action=(
|
|
828
|
+
"Reconciliar ou reconstruir o vocabulary DB pelo fluxo oficial de /mednotes:link; "
|
|
829
|
+
"não projetar aliases nem rodar body linker."
|
|
830
|
+
),
|
|
831
|
+
required_inputs=["vocabulary_recovery", "vocabulary_semantic_ingestion"],
|
|
832
|
+
)
|
|
833
|
+
| {"surface_count": unresolved_surface_count}
|
|
834
|
+
)
|
|
835
|
+
for _meaning_id, label in conn.execute(
|
|
836
|
+
"SELECT id, label FROM meanings WHERE status = 'active' AND atomic_status = 'duplicate_candidate' ORDER BY id"
|
|
837
|
+
).fetchall():
|
|
838
|
+
result.issues.append(
|
|
839
|
+
_issue(
|
|
840
|
+
severity="human_decision",
|
|
841
|
+
code="vocabulary_map.duplicate_meaning",
|
|
842
|
+
message=f"Meaning requires duplicate/merge review: {label}",
|
|
843
|
+
next_action="Criar plano de merge preservando provenance.",
|
|
844
|
+
required_inputs=["note_merge_decision"],
|
|
845
|
+
)
|
|
846
|
+
)
|
|
847
|
+
for meaning_id, label, note_path in conn.execute(
|
|
848
|
+
"""
|
|
849
|
+
SELECT m.id, m.label, COALESCE(n.path, '')
|
|
850
|
+
FROM meanings m
|
|
851
|
+
LEFT JOIN meaning_note_links l
|
|
852
|
+
ON l.meaning_id = m.id
|
|
853
|
+
AND l.role = 'canonical'
|
|
854
|
+
AND l.status = 'active'
|
|
855
|
+
LEFT JOIN notes n ON n.id = l.note_id
|
|
856
|
+
WHERE m.status = 'active'
|
|
857
|
+
AND m.atomic_status = 'suspected_non_atomic'
|
|
858
|
+
ORDER BY m.id, n.path
|
|
859
|
+
"""
|
|
860
|
+
).fetchall():
|
|
861
|
+
issue = _issue(
|
|
862
|
+
severity="human_decision",
|
|
863
|
+
code="vocabulary_map.non_atomic_note",
|
|
864
|
+
message=f"Meaning may not be atomic: {label}",
|
|
865
|
+
note_path=str(note_path),
|
|
866
|
+
next_action="Separar ou reescrever a nota antes de linkar automaticamente.",
|
|
867
|
+
required_inputs=["atomicity_decision"],
|
|
868
|
+
)
|
|
869
|
+
issue["meaning_id"] = str(meaning_id)
|
|
870
|
+
issue["label"] = str(label)
|
|
871
|
+
result.issues.append(issue)
|
|
872
|
+
for path, alias_text, normalized_surface in conn.execute(
|
|
873
|
+
"""
|
|
874
|
+
SELECT n.path, c.alias_text, c.normalized_surface
|
|
875
|
+
FROM yaml_alias_claims c
|
|
876
|
+
JOIN notes n ON n.id = c.note_id
|
|
877
|
+
WHERE c.claim_status = 'conflicting_alias'
|
|
878
|
+
ORDER BY n.path, c.normalized_surface
|
|
879
|
+
"""
|
|
880
|
+
).fetchall():
|
|
881
|
+
result.issues.append(
|
|
882
|
+
_issue(
|
|
883
|
+
severity="human_decision",
|
|
884
|
+
code="vocabulary_map.conflicting_alias",
|
|
885
|
+
message=f"YAML alias conflicts with vocabulary meaning: {alias_text}",
|
|
886
|
+
note_path=str(path),
|
|
887
|
+
surface=str(normalized_surface),
|
|
888
|
+
next_action="Resolver alias conflitante no DB antes de projetar YAML/linkar corpo.",
|
|
889
|
+
required_inputs=["alias_conflict_decision"],
|
|
890
|
+
)
|
|
891
|
+
)
|
|
892
|
+
for normalized_surface, display_text in conn.execute(
|
|
893
|
+
"""
|
|
894
|
+
SELECT s.normalized_surface, s.best_display_text
|
|
895
|
+
FROM surfaces s
|
|
896
|
+
JOIN surface_meaning_policy p ON p.surface_id = s.id
|
|
897
|
+
GROUP BY s.id
|
|
898
|
+
HAVING
|
|
899
|
+
SUM(CASE WHEN p.link_policy = 'direct' THEN 1 ELSE 0 END) > 0
|
|
900
|
+
AND (
|
|
901
|
+
COUNT(DISTINCT p.meaning_id) > 1
|
|
902
|
+
OR MAX(s.intrinsically_ambiguous) = 1
|
|
903
|
+
)
|
|
904
|
+
ORDER BY s.normalized_surface
|
|
905
|
+
"""
|
|
906
|
+
).fetchall():
|
|
907
|
+
decision = decision_for_code(
|
|
908
|
+
"vocabulary_map.direct_policy_on_ambiguous_surface",
|
|
909
|
+
phase="vocabulary_map_diagnosis",
|
|
910
|
+
public_summary="Termo ambíguo tratado de forma contextual.",
|
|
911
|
+
developer_summary="Direct alias on ambiguous surface downgraded before linking.",
|
|
912
|
+
next_action="Continuar sem link direto automático para esta superfície.",
|
|
913
|
+
)
|
|
914
|
+
issue = _issue(
|
|
915
|
+
severity="warning",
|
|
916
|
+
code=decision.reason_code,
|
|
917
|
+
message=decision.public_summary,
|
|
918
|
+
surface=str(normalized_surface),
|
|
919
|
+
next_action=decision.next_action,
|
|
920
|
+
required_inputs=[],
|
|
921
|
+
)
|
|
922
|
+
issue["decision_summary"] = decision.decision_summary()
|
|
923
|
+
result.issues.append(
|
|
924
|
+
issue
|
|
925
|
+
| {
|
|
926
|
+
"display_text": str(display_text),
|
|
927
|
+
}
|
|
928
|
+
)
|
|
929
|
+
result.map_hash = vocabulary_map_hash(db_path)
|
|
930
|
+
return result
|