mednotes-opencode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/agents/med-chat-triager.md +204 -0
- package/.opencode/agents/med-flashcard-maker.md +63 -0
- package/.opencode/agents/med-knowledge-architect.md +230 -0
- package/.opencode/agents/med-link-graph-curator.md +177 -0
- package/.opencode/agents/med-publish-guard.md +62 -0
- package/.opencode/commands/flashcards.md +25 -0
- package/.opencode/commands/mednotes/create.md +25 -0
- package/.opencode/commands/mednotes/enrich.md +27 -0
- package/.opencode/commands/mednotes/fix-wiki.md +27 -0
- package/.opencode/commands/mednotes/history.md +22 -0
- package/.opencode/commands/mednotes/link-body.md +25 -0
- package/.opencode/commands/mednotes/link-related.md +27 -0
- package/.opencode/commands/mednotes/link.md +27 -0
- package/.opencode/commands/mednotes/pdf-library.md +27 -0
- package/.opencode/commands/mednotes/process-chats.md +23 -0
- package/.opencode/commands/mednotes/setup.md +21 -0
- package/.opencode/commands/mednotes/status.md +27 -0
- package/.opencode/commands/mednotes/telemetry.md +27 -0
- package/.opencode/commands/report.md +26 -0
- package/.opencode/mednotes/AGENTS.md +57 -0
- package/.opencode/mednotes/agents/med-chat-triager.md +197 -0
- package/.opencode/mednotes/agents/med-flashcard-maker.md +56 -0
- package/.opencode/mednotes/agents/med-knowledge-architect.md +224 -0
- package/.opencode/mednotes/agents/med-link-graph-curator.md +171 -0
- package/.opencode/mednotes/agents/med-publish-guard.md +55 -0
- package/.opencode/mednotes/contracts/.gitkeep +1 -0
- package/.opencode/mednotes/contracts/agents.json +116 -0
- package/.opencode/mednotes/contracts/opencode-plugin.json +70 -0
- package/.opencode/mednotes/docs/agent-prompt-hardening.md +567 -0
- package/.opencode/mednotes/docs/agent-role-contracts.md +94 -0
- package/.opencode/mednotes/docs/anki-mcp-twenty-rules.md +214 -0
- package/.opencode/mednotes/docs/anki-templates/README.md +39 -0
- package/.opencode/mednotes/docs/anki-templates/cloze.back.html +23 -0
- package/.opencode/mednotes/docs/anki-templates/cloze.front.html +14 -0
- package/.opencode/mednotes/docs/anki-templates/qa.back.html +24 -0
- package/.opencode/mednotes/docs/anki-templates/qa.front.html +14 -0
- package/.opencode/mednotes/docs/anki-templates/style.css +182 -0
- package/.opencode/mednotes/docs/atomicity-splitting-policy.md +113 -0
- package/.opencode/mednotes/docs/extension-docs.md +40 -0
- package/.opencode/mednotes/docs/flashcard-ingestion.md +278 -0
- package/.opencode/mednotes/docs/knowledge-architect.md +208 -0
- package/.opencode/mednotes/docs/merge-policy.md +110 -0
- package/.opencode/mednotes/docs/public-vocabulary.md +104 -0
- package/.opencode/mednotes/docs/semantic-linker.md +141 -0
- package/.opencode/mednotes/docs/taxonomy-policy.md +90 -0
- package/.opencode/mednotes/docs/triage-policy.md +187 -0
- package/.opencode/mednotes/docs/vault-version-control.md +758 -0
- package/.opencode/mednotes/docs/vocabulary-db-recovery.md +58 -0
- package/.opencode/mednotes/docs/workflow-output-contract.md +779 -0
- package/.opencode/mednotes/hooks/hooks.json +79 -0
- package/.opencode/mednotes/package-lock.json +6361 -0
- package/.opencode/mednotes/package.json +15 -0
- package/.opencode/mednotes/pyproject.toml +48 -0
- package/.opencode/mednotes/scripts/bootstrap_windows_python_uv.cmd +13 -0
- package/.opencode/mednotes/scripts/bootstrap_windows_python_uv.ps1 +172 -0
- package/.opencode/mednotes/scripts/enrich_notes.py +23 -0
- package/.opencode/mednotes/scripts/full_reset_windows_python_uv.cmd +13 -0
- package/.opencode/mednotes/scripts/hooks/antigravity_hook_status.mjs +212 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/antigravity.mjs +169 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/harness_payload.mjs +103 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/opencode_plugin.mjs +341 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/opencode_user_config_sync.mjs +177 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/anki_preflight.mjs +214 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/cli.mjs +143 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/diagnostics.mjs +11 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/domain/agent_directive_core.mjs +160 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/fsm_directive.mjs +1470 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/hook_errors.mjs +120 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/retention.mjs +114 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/runtime.mjs +174 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/telemetry_capture.mjs +511 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/vault_guard.mjs +624 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook.mjs +5 -0
- package/.opencode/mednotes/scripts/mednotes/_runtime_paths.py +24 -0
- package/.opencode/mednotes/scripts/mednotes/anki_model_validator.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/capture_extension_diff.py +1562 -0
- package/.opencode/mednotes/scripts/mednotes/feedback_report.py +16 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_index.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_pipeline.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_report.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_sources.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/obsidian/README.md +6 -0
- package/.opencode/mednotes/scripts/mednotes/obsidian_note_utils.py +20 -0
- package/.opencode/mednotes/scripts/mednotes/pdf_library/cli.py +16 -0
- package/.opencode/mednotes/scripts/mednotes/project_fsm.py +229 -0
- package/.opencode/mednotes/scripts/mednotes/setup_telemetry_email.py +404 -0
- package/.opencode/mednotes/scripts/mednotes/sync_anki_twenty_rules.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/sync_opencode_user_config.py +36 -0
- package/.opencode/mednotes/scripts/mednotes/wiki/cli.py +20 -0
- package/.opencode/mednotes/scripts/mednotes/wiki_graph.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/wiki_tree.py +134 -0
- package/.opencode/mednotes/scripts/reset_windows_python_uv.ps1 +625 -0
- package/.opencode/mednotes/scripts/run_python.mjs +109 -0
- package/.opencode/mednotes/scripts/vault/vault_commit.ps1 +19 -0
- package/.opencode/mednotes/scripts/vault/vault_commit.sh +18 -0
- package/.opencode/mednotes/scripts/vault/vault_git.ps1 +19 -0
- package/.opencode/mednotes/scripts/vault/vault_git.py +3107 -0
- package/.opencode/mednotes/scripts/vault/vault_git.sh +18 -0
- package/.opencode/mednotes/scripts/vault/vault_precommit.ps1 +19 -0
- package/.opencode/mednotes/scripts/vault/vault_precommit.sh +18 -0
- package/.opencode/mednotes/skills/THIRD_PARTY_NOTICES.md +45 -0
- package/.opencode/mednotes/skills/create-medical-flashcards/SKILL.md +113 -0
- package/.opencode/mednotes/skills/create-medical-note/SKILL.md +90 -0
- package/.opencode/mednotes/skills/enrich-medical-note/SKILL.md +120 -0
- package/.opencode/mednotes/skills/fix-medical-wiki/SKILL.md +559 -0
- package/.opencode/mednotes/skills/link-medical-wiki/SKILL.md +224 -0
- package/.opencode/mednotes/skills/obsidian-cli/SKILL.md +118 -0
- package/.opencode/mednotes/skills/obsidian-markdown/SKILL.md +207 -0
- package/.opencode/mednotes/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
- package/.opencode/mednotes/skills/obsidian-markdown/references/EMBEDS.md +63 -0
- package/.opencode/mednotes/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
- package/.opencode/mednotes/skills/obsidian-ops/SKILL.md +136 -0
- package/.opencode/mednotes/skills/pdf-library/SKILL.md +45 -0
- package/.opencode/mednotes/skills/process-medical-chats/SKILL.md +246 -0
- package/.opencode/mednotes/skills/workflow-report/SKILL.md +100 -0
- package/.opencode/mednotes/src/mednotes/__init__.py +5 -0
- package/.opencode/mednotes/src/mednotes/domains/__init__.py +5 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/README.md +26 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/__init__.py +2 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/build_demo_apkg.py +177 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/contracts.py +385 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/flashcards_machine.py +522 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/fsm.py +817 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/index.py +630 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/install_models.py +445 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/model.py +359 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/obsidian_links.py +135 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/obsidian_note_utils.py +546 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/pipeline.py +580 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/report.py +510 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/sources.py +682 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/sync_rules.py +184 -0
- package/.opencode/mednotes/src/mednotes/domains/history/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/history/history_fsm.py +852 -0
- package/.opencode/mednotes/src/mednotes/domains/history/history_machine.py +453 -0
- package/.opencode/mednotes/src/mednotes/domains/setup/__init__.py +7 -0
- package/.opencode/mednotes/src/mednotes/domains/setup/setup_fsm.py +808 -0
- package/.opencode/mednotes/src/mednotes/domains/setup/setup_machine.py +973 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/README.md +64 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/api.py +668 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/batch_state.py +102 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/atomicity/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/atomicity/atomicity.py +877 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/body_link/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/body_link/body_linker.py +1562 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/effect_adapters.py +949 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/fix_wiki_runtime_adapters.py +433 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/coverage.py +413 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/graph.py +396 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/graph_fixes.py +161 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/hygiene/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/hygiene/hygiene.py +483 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/__init__.py +2 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/anchors.py +185 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/__init__.py +0 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/cache.py +223 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/config.py +131 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/download.py +224 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/frontmatter.py +59 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/insert.py +227 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/local_import.py +54 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/__init__.py +42 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/web_profiles.py +99 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/web_search.py +203 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/wikimedia.py +102 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_db_adapter.mjs +434 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_node_runtime.py +274 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_query.py +227 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/artifacts.py +605 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/canonical_merge.py +277 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/markdown_zones.py +85 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/meaning_planner.py +307 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_iter.py +67 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_merge.py +278 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_plan.py +409 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_policy.py +22 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/__init__.py +79 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/fixes.py +264 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/frontmatter.py +435 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/models.py +208 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/prompts.py +37 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/tables.py +236 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/validate.py +404 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/provenance.py +478 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/raw_chats.py +273 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/sources_backfill.py +235 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/__init__.py +10 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/anchors.py +16 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/captions.py +47 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/cli.py +179 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/cloud.py +52 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/config.py +196 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/context_packets.py +76 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/db.py +81 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/doctor.py +102 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/figure_ids.py +42 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/ingest.py +326 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/insert.py +316 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/mentions.py +57 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/ocr.py +71 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/paths.py +35 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/pdf_engine.py +77 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/schema.py +155 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/search.py +188 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/app.py +89 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/image_backend.py +29 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/state.py +65 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish.py +1139 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish_receipts.py +365 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish_recovery.py +240 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_behavior_corpus.py +2069 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_report_validation.py +4448 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_run_audit.py +852 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/architect_prompt_eval.py +341 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/body_linker_eval.py +240 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/curator_output_validation.py +175 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/curator_prompt_eval.py +865 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/triager_prompt_eval.py +1295 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/related_notes.py +1920 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/related_notes_headless.py +1186 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/plan_attestation.py +148 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_receipts.py +360 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_runtime.py +52 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_task_runner.py +2470 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/style/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/style/style.py +1952 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/subagents/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/subagents/agents.py +1767 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/alias_projection.py +331 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/link_terms.py +151 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/llm_disambiguation.py +182 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/__init__.py +116 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/audit.py +201 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/migration.py +314 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/normalize.py +72 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/policy.py +135 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/resolve.py +413 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/schema.py +157 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/status.py +137 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_bootstrap.py +509 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_curator_batch.py +1115 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_ingestion.py +632 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_map.py +930 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_recovery.py +1388 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/cli.py +6665 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/common.py +69 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/config.py +210 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/__init__.py +74 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agent_report.py +242 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agent_run_audit.py +196 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agents.py +601 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/curator.py +256 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/effect_payloads.py +519 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/happy_path.py +190 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/link_git.py +110 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/link_runtime_artifact.py +52 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/note_plan.py +75 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/paths.py +114 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/public_report.py +53 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/publish.py +111 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/raw_coverage.py +217 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes.py +136 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes_headless.py +153 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes_runtime.py +395 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/schema_registry.py +637 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/specialist.py +432 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/status.py +62 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/style_rewrite.py +568 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/vocabulary_ingestion.py +223 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_blockers.py +510 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_guardrails.py +637 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_outcomes.py +121 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_receipts.py +100 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/__main__.py +4 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/cli.py +275 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/__init__.py +2 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/candidates.py +193 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/cli.py +189 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/gemini.py +220 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/inputs.py +120 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/models.py +34 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/parsing.py +48 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/prompts.py +216 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/quality.py +54 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/reporting.py +24 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/runner.py +433 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/utils.py +39 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/vault_guard_bridge.py +17 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_context_packets.py +454 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_decision_projection.py +133 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_effects.py +1260 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_fsm.py +2768 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_machine.py +1588 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_plan.py +306 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_primary_objective.py +316 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_problem.py +153 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_receipt_evidence.py +306 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_states.py +290 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_user_report.py +342 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/health.py +6332 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_fsm.py +1119 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_git.py +638 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_machine.py +1106 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_retry_governance.py +374 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_runtime_result.py +485 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_triggers.py +183 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/linking.py +2758 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/reference_repair.py +718 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/related_notes_fsm.py +1855 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link_related/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link_related/link_related_machine.py +834 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_fsm.py +1592 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_machine.py +3097 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_primary_objective.py +28 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_runtime_result.py +185 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/performance.py +97 -0
- package/.opencode/mednotes/src/mednotes/kernel/__init__.py +6 -0
- package/.opencode/mednotes/src/mednotes/kernel/agent_directive.py +336 -0
- package/.opencode/mednotes/src/mednotes/kernel/base.py +51 -0
- package/.opencode/mednotes/src/mednotes/kernel/blockers.py +39 -0
- package/.opencode/mednotes/src/mednotes/kernel/effect_executor.py +55 -0
- package/.opencode/mednotes/src/mednotes/kernel/effect_intent.py +69 -0
- package/.opencode/mednotes/src/mednotes/kernel/effects.py +160 -0
- package/.opencode/mednotes/src/mednotes/kernel/errors.py +38 -0
- package/.opencode/mednotes/src/mednotes/kernel/fsm_event.py +35 -0
- package/.opencode/mednotes/src/mednotes/kernel/fsm_model.py +55 -0
- package/.opencode/mednotes/src/mednotes/kernel/fsm_transition_result.py +75 -0
- package/.opencode/mednotes/src/mednotes/kernel/guardrails.py +188 -0
- package/.opencode/mednotes/src/mednotes/kernel/progress.py +319 -0
- package/.opencode/mednotes/src/mednotes/kernel/public_report.py +346 -0
- package/.opencode/mednotes/src/mednotes/kernel/state_machine.py +164 -0
- package/.opencode/mednotes/src/mednotes/kernel/workflow.py +619 -0
- package/.opencode/mednotes/src/mednotes/platform/__init__.py +5 -0
- package/.opencode/mednotes/src/mednotes/platform/backup_policy.py +382 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/__init__.py +62 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/cli.py +275 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/contracts.py +83 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/core.py +4168 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/integrity.py +989 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/operational_contract.py +2293 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/telemetry.py +875 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/telemetry_config.py +65 -0
- package/.opencode/mednotes/src/mednotes/platform/opencode_runtime_config.py +182 -0
- package/.opencode/mednotes/src/mednotes/platform/paths/__init__.py +1560 -0
- package/.opencode/mednotes/src/mednotes/platform/secrets.py +89 -0
- package/.opencode/mednotes/src/mednotes/platform/user_config.py +103 -0
- package/.opencode/mednotes/src/mednotes/platform/vault_guard.py +214 -0
- package/.opencode/mednotes/uv.lock +932 -0
- package/.opencode/mednotes.generated.json +395 -0
- package/.opencode/opencode.json +31 -0
- package/.opencode/plugins/mednotes-fsm.mjs +7 -0
- package/.opencode/plugins/mednotes_hook/adapters/antigravity.mjs +169 -0
- package/.opencode/plugins/mednotes_hook/adapters/harness_payload.mjs +103 -0
- package/.opencode/plugins/mednotes_hook/adapters/opencode_plugin.mjs +341 -0
- package/.opencode/plugins/mednotes_hook/adapters/opencode_user_config_sync.mjs +177 -0
- package/.opencode/plugins/mednotes_hook/anki_preflight.mjs +214 -0
- package/.opencode/plugins/mednotes_hook/cli.mjs +143 -0
- package/.opencode/plugins/mednotes_hook/diagnostics.mjs +11 -0
- package/.opencode/plugins/mednotes_hook/domain/agent_directive_core.mjs +160 -0
- package/.opencode/plugins/mednotes_hook/fsm_directive.mjs +1470 -0
- package/.opencode/plugins/mednotes_hook/hook_errors.mjs +120 -0
- package/.opencode/plugins/mednotes_hook/retention.mjs +114 -0
- package/.opencode/plugins/mednotes_hook/runtime.mjs +174 -0
- package/.opencode/plugins/mednotes_hook/telemetry_capture.mjs +511 -0
- package/.opencode/plugins/mednotes_hook/vault_guard.mjs +624 -0
- package/AGENTS.md +57 -0
- package/README.md +194 -0
- package/adapters/antigravity/agents.json +80 -0
- package/adapters/antigravity/templates/med-chat-triager.md +214 -0
- package/adapters/antigravity/templates/med-flashcard-maker.md +72 -0
- package/adapters/antigravity/templates/med-knowledge-architect.md +241 -0
- package/adapters/antigravity/templates/med-link-graph-curator.md +187 -0
- package/adapters/antigravity/templates/med-publish-guard.md +71 -0
- package/adapters/gemini-cli/gemini-extension.json +14 -0
- package/adapters/gemini-cli/package.json +15 -0
- package/adapters/gemini-cli/pyproject.toml +48 -0
- package/bin/mednotes-opencode.mjs +155 -0
- package/contracts/agents.json +116 -0
- package/core/agents/med-chat-triager.md +197 -0
- package/core/agents/med-flashcard-maker.md +56 -0
- package/core/agents/med-knowledge-architect.md +224 -0
- package/core/agents/med-link-graph-curator.md +171 -0
- package/core/agents/med-publish-guard.md +55 -0
- package/core/commands/flashcards.toml +22 -0
- package/core/commands/mednotes/create.toml +22 -0
- package/core/commands/mednotes/enrich.toml +24 -0
- package/core/commands/mednotes/fix-wiki.toml +24 -0
- package/core/commands/mednotes/history.toml +19 -0
- package/core/commands/mednotes/link-body.toml +22 -0
- package/core/commands/mednotes/link-related.toml +24 -0
- package/core/commands/mednotes/link.toml +24 -0
- package/core/commands/mednotes/pdf-library.toml +24 -0
- package/core/commands/mednotes/process-chats.toml +20 -0
- package/core/commands/mednotes/setup.toml +18 -0
- package/core/commands/mednotes/status.toml +24 -0
- package/core/commands/mednotes/telemetry.toml +24 -0
- package/core/commands/report.toml +23 -0
- package/core/skills/THIRD_PARTY_NOTICES.md +45 -0
- package/core/skills/create-medical-flashcards/SKILL.md +113 -0
- package/core/skills/create-medical-note/SKILL.md +90 -0
- package/core/skills/enrich-medical-note/SKILL.md +120 -0
- package/core/skills/fix-medical-wiki/SKILL.md +559 -0
- package/core/skills/link-medical-wiki/SKILL.md +224 -0
- package/core/skills/obsidian-cli/SKILL.md +118 -0
- package/core/skills/obsidian-markdown/SKILL.md +207 -0
- package/core/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
- package/core/skills/obsidian-markdown/references/EMBEDS.md +63 -0
- package/core/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
- package/core/skills/obsidian-ops/SKILL.md +136 -0
- package/core/skills/pdf-library/SKILL.md +45 -0
- package/core/skills/process-medical-chats/SKILL.md +246 -0
- package/core/skills/workflow-report/SKILL.md +100 -0
- package/package.json +45 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Cache SQLite único para o pipeline.
|
|
2
|
+
|
|
3
|
+
Três tabelas:
|
|
4
|
+
- ``anchors``: chave ``sha256(markdown_body)``, JSON da lista de âncoras
|
|
5
|
+
produzida na Etapa 1 (Gemini). Sem TTL — markdown idêntico → mesma saída.
|
|
6
|
+
- ``candidates``: chave ``(source, query, visual_type)``, JSON da lista de
|
|
7
|
+
candidatas devolvida por um adapter na Etapa 2. TTL configurável (default
|
|
8
|
+
30d) porque APIs de fonte mudam.
|
|
9
|
+
- ``images``: chave ``sha256`` do conteúdo binário. Mapeia para o filename
|
|
10
|
+
local. Permanente (asset baixado é asset baixado).
|
|
11
|
+
|
|
12
|
+
API minimalista; só ``get_*`` / ``put_*`` por tabela. Sem migrations — o
|
|
13
|
+
schema é idempotente via ``CREATE TABLE IF NOT EXISTS``.
|
|
14
|
+
|
|
15
|
+
``Cache`` aceita ``clock`` injetável para tornar TTL testável sem manipular
|
|
16
|
+
relógio do sistema. Suporta ``":memory:"`` para testes.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import sqlite3
|
|
22
|
+
import time
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from mednotes.domains.wiki.capabilities.illustrate.core.config import expand_path
|
|
27
|
+
from mednotes.kernel.base import JsonArrayAdapter, JsonObject, JsonObjectAdapter
|
|
28
|
+
|
|
29
|
+
__all__ = ["Cache"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_SCHEMA = """
|
|
33
|
+
CREATE TABLE IF NOT EXISTS anchors (
|
|
34
|
+
note_sha TEXT PRIMARY KEY,
|
|
35
|
+
payload TEXT NOT NULL,
|
|
36
|
+
created_at REAL NOT NULL
|
|
37
|
+
);
|
|
38
|
+
CREATE TABLE IF NOT EXISTS candidates (
|
|
39
|
+
source TEXT NOT NULL,
|
|
40
|
+
query TEXT NOT NULL,
|
|
41
|
+
visual_type TEXT NOT NULL,
|
|
42
|
+
payload TEXT NOT NULL,
|
|
43
|
+
created_at REAL NOT NULL,
|
|
44
|
+
PRIMARY KEY (source, query, visual_type)
|
|
45
|
+
);
|
|
46
|
+
CREATE TABLE IF NOT EXISTS images (
|
|
47
|
+
sha TEXT PRIMARY KEY,
|
|
48
|
+
filename TEXT NOT NULL,
|
|
49
|
+
source TEXT NOT NULL,
|
|
50
|
+
source_url TEXT NOT NULL,
|
|
51
|
+
width INTEGER,
|
|
52
|
+
height INTEGER,
|
|
53
|
+
bytes INTEGER,
|
|
54
|
+
created_at REAL NOT NULL
|
|
55
|
+
);
|
|
56
|
+
CREATE TABLE IF NOT EXISTS url_index (
|
|
57
|
+
image_url TEXT PRIMARY KEY,
|
|
58
|
+
sha TEXT NOT NULL,
|
|
59
|
+
created_at REAL NOT NULL
|
|
60
|
+
);
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _json_object_list(payload: str) -> list[JsonObject]:
|
|
65
|
+
"""Validate cached JSON arrays before returning them to workflow code."""
|
|
66
|
+
|
|
67
|
+
values = JsonArrayAdapter.validate_python(json.loads(payload))
|
|
68
|
+
return [JsonObjectAdapter.validate_python(item) for item in values]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Cache:
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
path: str | Path,
|
|
75
|
+
*,
|
|
76
|
+
clock: Callable[[], float] = time.time,
|
|
77
|
+
) -> None:
|
|
78
|
+
path_str = str(path)
|
|
79
|
+
if path_str == ":memory:":
|
|
80
|
+
self.path: str | Path = path_str
|
|
81
|
+
else:
|
|
82
|
+
resolved = expand_path(path_str)
|
|
83
|
+
resolved.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
self.path = resolved
|
|
85
|
+
self._conn = sqlite3.connect(str(self.path))
|
|
86
|
+
self._conn.executescript(_SCHEMA)
|
|
87
|
+
self._conn.commit()
|
|
88
|
+
self._clock = clock
|
|
89
|
+
|
|
90
|
+
def close(self) -> None:
|
|
91
|
+
self._conn.close()
|
|
92
|
+
|
|
93
|
+
def __enter__(self) -> Cache:
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def __exit__(self, *_exc: object) -> None:
|
|
97
|
+
self.close()
|
|
98
|
+
|
|
99
|
+
# --- anchors -----------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def get_anchors(self, note_sha: str) -> list[JsonObject] | None:
|
|
102
|
+
row = self._conn.execute(
|
|
103
|
+
"SELECT payload FROM anchors WHERE note_sha = ?", (note_sha,)
|
|
104
|
+
).fetchone()
|
|
105
|
+
return _json_object_list(row[0]) if row else None
|
|
106
|
+
|
|
107
|
+
def put_anchors(self, note_sha: str, anchors: list[JsonObject]) -> None:
|
|
108
|
+
payload = JsonArrayAdapter.validate_python(anchors)
|
|
109
|
+
self._conn.execute(
|
|
110
|
+
"INSERT OR REPLACE INTO anchors(note_sha, payload, created_at) "
|
|
111
|
+
"VALUES (?, ?, ?)",
|
|
112
|
+
(note_sha, json.dumps(payload, ensure_ascii=False), self._clock()),
|
|
113
|
+
)
|
|
114
|
+
self._conn.commit()
|
|
115
|
+
|
|
116
|
+
# --- candidates (TTL) --------------------------------------------
|
|
117
|
+
|
|
118
|
+
def get_candidates(
|
|
119
|
+
self,
|
|
120
|
+
source: str,
|
|
121
|
+
query: str,
|
|
122
|
+
visual_type: str,
|
|
123
|
+
*,
|
|
124
|
+
ttl_days: int,
|
|
125
|
+
) -> list[JsonObject] | None:
|
|
126
|
+
row = self._conn.execute(
|
|
127
|
+
"SELECT payload, created_at FROM candidates "
|
|
128
|
+
"WHERE source = ? AND query = ? AND visual_type = ?",
|
|
129
|
+
(source, query, visual_type),
|
|
130
|
+
).fetchone()
|
|
131
|
+
if not row:
|
|
132
|
+
return None
|
|
133
|
+
payload, created_at = row
|
|
134
|
+
age_days = (self._clock() - created_at) / 86400.0
|
|
135
|
+
if age_days > ttl_days:
|
|
136
|
+
return None
|
|
137
|
+
return _json_object_list(payload)
|
|
138
|
+
|
|
139
|
+
def put_candidates(
|
|
140
|
+
self,
|
|
141
|
+
source: str,
|
|
142
|
+
query: str,
|
|
143
|
+
visual_type: str,
|
|
144
|
+
candidates: list[JsonObject],
|
|
145
|
+
) -> None:
|
|
146
|
+
payload = JsonArrayAdapter.validate_python(candidates)
|
|
147
|
+
self._conn.execute(
|
|
148
|
+
"INSERT OR REPLACE INTO candidates"
|
|
149
|
+
"(source, query, visual_type, payload, created_at) "
|
|
150
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
151
|
+
(
|
|
152
|
+
source,
|
|
153
|
+
query,
|
|
154
|
+
visual_type,
|
|
155
|
+
json.dumps(payload, ensure_ascii=False),
|
|
156
|
+
self._clock(),
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
self._conn.commit()
|
|
160
|
+
|
|
161
|
+
# --- images (permanente) -----------------------------------------
|
|
162
|
+
|
|
163
|
+
def get_image(self, sha: str) -> JsonObject | None:
|
|
164
|
+
row = self._conn.execute(
|
|
165
|
+
"SELECT filename, source, source_url, width, height, bytes "
|
|
166
|
+
"FROM images WHERE sha = ?",
|
|
167
|
+
(sha,),
|
|
168
|
+
).fetchone()
|
|
169
|
+
if not row:
|
|
170
|
+
return None
|
|
171
|
+
return JsonObjectAdapter.validate_python({
|
|
172
|
+
"sha": sha,
|
|
173
|
+
"filename": row[0],
|
|
174
|
+
"source": row[1],
|
|
175
|
+
"source_url": row[2],
|
|
176
|
+
"width": row[3],
|
|
177
|
+
"height": row[4],
|
|
178
|
+
"bytes": row[5],
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
def put_image(
|
|
182
|
+
self,
|
|
183
|
+
sha: str,
|
|
184
|
+
*,
|
|
185
|
+
filename: str,
|
|
186
|
+
source: str,
|
|
187
|
+
source_url: str,
|
|
188
|
+
width: int | None = None,
|
|
189
|
+
height: int | None = None,
|
|
190
|
+
size_bytes: int | None = None,
|
|
191
|
+
) -> None:
|
|
192
|
+
self._conn.execute(
|
|
193
|
+
"INSERT OR REPLACE INTO images"
|
|
194
|
+
"(sha, filename, source, source_url, width, height, bytes, created_at)"
|
|
195
|
+
" VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
196
|
+
(
|
|
197
|
+
sha,
|
|
198
|
+
filename,
|
|
199
|
+
source,
|
|
200
|
+
source_url,
|
|
201
|
+
width,
|
|
202
|
+
height,
|
|
203
|
+
size_bytes,
|
|
204
|
+
self._clock(),
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
self._conn.commit()
|
|
208
|
+
|
|
209
|
+
# --- url → sha lookup (evita re-baixar) -------------------------
|
|
210
|
+
|
|
211
|
+
def get_sha_for_url(self, image_url: str) -> str | None:
|
|
212
|
+
row = self._conn.execute(
|
|
213
|
+
"SELECT sha FROM url_index WHERE image_url = ?", (image_url,)
|
|
214
|
+
).fetchone()
|
|
215
|
+
return row[0] if row else None
|
|
216
|
+
|
|
217
|
+
def put_url_index(self, image_url: str, sha: str) -> None:
|
|
218
|
+
self._conn.execute(
|
|
219
|
+
"INSERT OR REPLACE INTO url_index(image_url, sha, created_at) "
|
|
220
|
+
"VALUES (?, ?, ?)",
|
|
221
|
+
(image_url, sha, self._clock()),
|
|
222
|
+
)
|
|
223
|
+
self._conn.commit()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Carrega configuração local e persistente do Medical Notes Workbench."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import tomllib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from mednotes.platform.paths import (
|
|
9
|
+
default_config_path as _shared_default_config_path,
|
|
10
|
+
)
|
|
11
|
+
from mednotes.platform.paths import (
|
|
12
|
+
expand_path as _shared_expand_path,
|
|
13
|
+
)
|
|
14
|
+
from mednotes.platform.paths import (
|
|
15
|
+
find_config as _shared_find_config,
|
|
16
|
+
)
|
|
17
|
+
from mednotes.platform.paths import (
|
|
18
|
+
resolve_wiki_dir,
|
|
19
|
+
)
|
|
20
|
+
from mednotes.platform.paths import (
|
|
21
|
+
user_state_dir as _shared_user_state_dir,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_DEFAULTS: dict[str, Any] = {
|
|
25
|
+
"vault": {"path": "", "attachments_subdir": "attachments/medicina"},
|
|
26
|
+
"enrichment": {
|
|
27
|
+
"max_anchors_per_note": 5,
|
|
28
|
+
"max_image_dimension": 1600,
|
|
29
|
+
"webp_min_savings_pct": 30,
|
|
30
|
+
# Idioma preferido das figuras retornadas. Afeta:
|
|
31
|
+
# - queries que o gemini gera (pt-br adiciona 1 query em PT)
|
|
32
|
+
# - params do SerpAPI (hl/gl)
|
|
33
|
+
# - regra de desempate no rerank (prefere figuras com texto no idioma)
|
|
34
|
+
# Valores: "pt-br", "en" (default), "any" (sem hl/gl).
|
|
35
|
+
"preferred_language": "en",
|
|
36
|
+
},
|
|
37
|
+
"sources": {
|
|
38
|
+
"enabled": [
|
|
39
|
+
"wikimedia",
|
|
40
|
+
"radiopaedia",
|
|
41
|
+
"nih_open_i",
|
|
42
|
+
"openstax",
|
|
43
|
+
"dermnet",
|
|
44
|
+
"teachmeanatomy",
|
|
45
|
+
"web_search",
|
|
46
|
+
],
|
|
47
|
+
"top_k_per_source": 6,
|
|
48
|
+
},
|
|
49
|
+
# `[gemini]` é consumido pelo orquestrador (`scripts/enrich_notes.py`),
|
|
50
|
+
# não pelo toolbox em si. O enricher core não invoca LLM.
|
|
51
|
+
"gemini": {
|
|
52
|
+
"binary": "gemini",
|
|
53
|
+
"model_anchors": "gemini-2.5-pro",
|
|
54
|
+
"model_rerank": "gemini-2.5-pro",
|
|
55
|
+
"max_candidates_per_anchor": 12,
|
|
56
|
+
"timeout_seconds": 120,
|
|
57
|
+
},
|
|
58
|
+
"download": {
|
|
59
|
+
# User-Agent pra fetch de bytes em `download.py`.
|
|
60
|
+
# Default: UA browser-like (Chrome/macOS) — destrava osmosis,
|
|
61
|
+
# thehealthy.com, e similares com anti-bot básico. Wikimedia também
|
|
62
|
+
# aceita (qualquer browser legítimo passa). Trocar de volta pra UA
|
|
63
|
+
# identificável (`medical-notes-workbench/0.1 (...)`) é mais
|
|
64
|
+
# respeitoso mas perde fontes; veja config.example.toml.
|
|
65
|
+
"user_agent": (
|
|
66
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
67
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
68
|
+
"Chrome/131.0.0.0 Safari/537.36"
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
"cache": {
|
|
72
|
+
"path": "~/Documents/medical-notes-workbench/cache.db",
|
|
73
|
+
"candidates_ttl_days": 30,
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def expand_path(p: str) -> Path:
|
|
79
|
+
return _shared_expand_path(p)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def user_state_dir() -> Path:
|
|
83
|
+
"""Diretório persistente para estado editável pelo usuário.
|
|
84
|
+
|
|
85
|
+
A extensão Gemini CLI é auto-updatable e pode recriar
|
|
86
|
+
``~/.gemini/extensions/medical-notes-workbench``. Configuração, chaves,
|
|
87
|
+
cache e venv não devem depender desse diretório volátil.
|
|
88
|
+
"""
|
|
89
|
+
return _shared_user_state_dir()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def default_config_path() -> Path:
|
|
93
|
+
return user_state_dir() / "config.toml"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def default_env_path() -> Path:
|
|
97
|
+
return user_state_dir() / ".env"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _deep_merge(base: dict[str, Any], over: dict[str, Any]) -> dict[str, Any]:
|
|
101
|
+
out = dict(base)
|
|
102
|
+
for k, v in over.items():
|
|
103
|
+
if isinstance(v, dict) and isinstance(out.get(k), dict):
|
|
104
|
+
out[k] = _deep_merge(out[k], v)
|
|
105
|
+
else:
|
|
106
|
+
out[k] = v
|
|
107
|
+
return out
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def find_config(start: Path | None = None) -> Path | None:
|
|
111
|
+
return _shared_find_config(start=start)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def load(path: Path | None = None) -> dict[str, Any]:
|
|
115
|
+
if path is None:
|
|
116
|
+
path = find_config()
|
|
117
|
+
if path is None or not path.exists():
|
|
118
|
+
return dict(_DEFAULTS)
|
|
119
|
+
with path.open("rb") as f:
|
|
120
|
+
data = tomllib.load(f)
|
|
121
|
+
return _deep_merge(_DEFAULTS, data)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def resolve_wiki_root(config_path: Path | None = None, *, start: Path | None = None) -> Path | None:
|
|
125
|
+
"""Return canonical wiki root for workflows that can fall back from vault.path."""
|
|
126
|
+
resolution = resolve_wiki_dir(config=config_path, start=start or Path.cwd(), enable_gemini_probe=False)
|
|
127
|
+
return resolution.path if resolution.ok else None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def wiki_memory_path() -> Path:
|
|
131
|
+
return _shared_default_config_path()
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Etapa 4 (numeração nova): fetch + validate + resize + dedupe.
|
|
2
|
+
|
|
3
|
+
Contrato:
|
|
4
|
+
- Recebe URL, ``vault_dir`` e parâmetros de redimensionamento/encoding.
|
|
5
|
+
- Baixa via ``httpx``, valida o conteúdo via ``Pillow.Image.open`` (magic
|
|
6
|
+
number — proteção contra Google/Bing servir HTML quando o asset some).
|
|
7
|
+
- Redimensiona se ``max(width, height) > max_dim`` (LANCZOS).
|
|
8
|
+
- Decide encoding final: tenta WebP; mantém WebP se a economia for ≥
|
|
9
|
+
``webp_min_savings_pct``%, senão preserva o formato original
|
|
10
|
+
(PNG/JPEG; GIF vira PNG single-frame; SVG não é suportado).
|
|
11
|
+
- SHA-256 sobre os **bytes finais** (após resize/recode), pra dedupe correta.
|
|
12
|
+
- Idempotência por dois níveis:
|
|
13
|
+
1. ``cache.get_sha_for_url(url)``: se conhecemos a URL, recuperamos o SHA
|
|
14
|
+
sem ir à rede.
|
|
15
|
+
2. ``cache.get_image(sha)`` + arquivo existe: reusa.
|
|
16
|
+
|
|
17
|
+
Erros:
|
|
18
|
+
- :class:`DownloadError` para falhas HTTP, conteúdo não-imagem, formato não
|
|
19
|
+
suportado.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import io
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import httpx
|
|
29
|
+
from PIL import Image, UnidentifiedImageError
|
|
30
|
+
|
|
31
|
+
from mednotes.domains.wiki.capabilities.illustrate.core.cache import Cache
|
|
32
|
+
|
|
33
|
+
__all__ = ["DownloadError", "download"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DownloadError(RuntimeError):
|
|
37
|
+
"""Falha no download/validação/encoding de uma imagem."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_SUPPORTED_FORMATS = {"PNG", "JPEG", "WEBP", "GIF"}
|
|
41
|
+
_FORMAT_TO_EXT = {"PNG": "png", "JPEG": "jpg", "WEBP": "webp", "GIF": "gif"}
|
|
42
|
+
|
|
43
|
+
# Wikimedia (e outros) rejeitam UAs genéricos como `python-httpx/X.Y` com 403.
|
|
44
|
+
_DEFAULT_USER_AGENT = (
|
|
45
|
+
"medical-notes-workbench/0.1 (personal study; "
|
|
46
|
+
"https://github.com/augustocaruso/medical-notes-workbench)"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def download(
|
|
51
|
+
url: str,
|
|
52
|
+
*,
|
|
53
|
+
vault_dir: Path,
|
|
54
|
+
max_dim: int = 1600,
|
|
55
|
+
webp_min_savings_pct: int = 30,
|
|
56
|
+
cache: Cache | None = None,
|
|
57
|
+
client: httpx.Client | None = None,
|
|
58
|
+
source: str = "unknown",
|
|
59
|
+
source_url: str | None = None,
|
|
60
|
+
user_agent: str | None = None,
|
|
61
|
+
) -> dict[str, Any]:
|
|
62
|
+
"""Baixa, valida, normaliza e indexa uma imagem.
|
|
63
|
+
|
|
64
|
+
Devolve ``{sha, filename, path, width, height, bytes, source, source_url, cached}``.
|
|
65
|
+
``cached=True`` quando a imagem já estava conhecida (por URL ou SHA) e o
|
|
66
|
+
arquivo no vault existe — nesse caso não houve fetch nem reescrita.
|
|
67
|
+
"""
|
|
68
|
+
# 1) URL cache hit → evita HTTP inteiramente.
|
|
69
|
+
if cache is not None:
|
|
70
|
+
sha_known = cache.get_sha_for_url(url)
|
|
71
|
+
if sha_known:
|
|
72
|
+
existing = cache.get_image(sha_known)
|
|
73
|
+
if existing:
|
|
74
|
+
path = vault_dir / existing["filename"]
|
|
75
|
+
if path.exists():
|
|
76
|
+
return _hit_dict(existing, path, source_url=source_url or url)
|
|
77
|
+
|
|
78
|
+
# 2) Fetch.
|
|
79
|
+
own = client is None
|
|
80
|
+
request_headers = _browser_like_headers(
|
|
81
|
+
user_agent=user_agent or _DEFAULT_USER_AGENT,
|
|
82
|
+
referer=source_url,
|
|
83
|
+
)
|
|
84
|
+
if own:
|
|
85
|
+
client = httpx.Client(
|
|
86
|
+
timeout=30.0,
|
|
87
|
+
follow_redirects=True,
|
|
88
|
+
)
|
|
89
|
+
try:
|
|
90
|
+
try:
|
|
91
|
+
resp = client.get(url, headers=request_headers)
|
|
92
|
+
resp.raise_for_status()
|
|
93
|
+
except httpx.HTTPError as e:
|
|
94
|
+
raise DownloadError(f"falha HTTP em {url}: {e}") from e
|
|
95
|
+
raw = resp.content
|
|
96
|
+
finally:
|
|
97
|
+
if own:
|
|
98
|
+
client.close()
|
|
99
|
+
|
|
100
|
+
# 3) Valida (magic number via Pillow).
|
|
101
|
+
try:
|
|
102
|
+
img = Image.open(io.BytesIO(raw))
|
|
103
|
+
img.load()
|
|
104
|
+
except (UnidentifiedImageError, OSError) as e:
|
|
105
|
+
raise DownloadError(f"conteúdo de {url} não é imagem válida: {e}") from e
|
|
106
|
+
|
|
107
|
+
fmt = (img.format or "").upper()
|
|
108
|
+
if fmt not in _SUPPORTED_FORMATS:
|
|
109
|
+
raise DownloadError(f"formato {fmt!r} não suportado ({url})")
|
|
110
|
+
|
|
111
|
+
# 4) Resize.
|
|
112
|
+
if max(img.size) > max_dim:
|
|
113
|
+
img.thumbnail((max_dim, max_dim), Image.Resampling.LANCZOS)
|
|
114
|
+
|
|
115
|
+
# 5) Decide encoding final.
|
|
116
|
+
final_bytes, final_fmt = _encode(
|
|
117
|
+
img, original_fmt=fmt, webp_min_savings_pct=webp_min_savings_pct
|
|
118
|
+
)
|
|
119
|
+
sha = hashlib.sha256(final_bytes).hexdigest()
|
|
120
|
+
ext = _FORMAT_TO_EXT[final_fmt]
|
|
121
|
+
filename = f"{sha[:12]}.{ext}"
|
|
122
|
+
out_path = vault_dir / filename
|
|
123
|
+
|
|
124
|
+
# 6) SHA cache hit → arquivo já existe (ou existia em outro lugar).
|
|
125
|
+
if cache is not None:
|
|
126
|
+
existing = cache.get_image(sha)
|
|
127
|
+
if existing and (vault_dir / existing["filename"]).exists():
|
|
128
|
+
cache.put_url_index(url, sha)
|
|
129
|
+
return _hit_dict(existing, vault_dir / existing["filename"], source_url=source_url or url)
|
|
130
|
+
|
|
131
|
+
# 7) Grava + indexa.
|
|
132
|
+
vault_dir.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
if not out_path.exists():
|
|
134
|
+
out_path.write_bytes(final_bytes)
|
|
135
|
+
|
|
136
|
+
width, height = img.size
|
|
137
|
+
size_bytes = len(final_bytes)
|
|
138
|
+
|
|
139
|
+
if cache is not None:
|
|
140
|
+
cache.put_image(
|
|
141
|
+
sha,
|
|
142
|
+
filename=filename,
|
|
143
|
+
source=source,
|
|
144
|
+
source_url=source_url or url,
|
|
145
|
+
width=width,
|
|
146
|
+
height=height,
|
|
147
|
+
size_bytes=size_bytes,
|
|
148
|
+
)
|
|
149
|
+
cache.put_url_index(url, sha)
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
"sha": sha,
|
|
153
|
+
"filename": filename,
|
|
154
|
+
"path": str(out_path),
|
|
155
|
+
"width": width,
|
|
156
|
+
"height": height,
|
|
157
|
+
"bytes": size_bytes,
|
|
158
|
+
"source": source,
|
|
159
|
+
"source_url": source_url or url,
|
|
160
|
+
"cached": False,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# --- helpers --------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _hit_dict(existing: dict[str, Any], path: Path, *, source_url: str) -> dict[str, Any]:
|
|
168
|
+
return {
|
|
169
|
+
"sha": existing["sha"],
|
|
170
|
+
"filename": existing["filename"],
|
|
171
|
+
"path": str(path),
|
|
172
|
+
"width": existing.get("width"),
|
|
173
|
+
"height": existing.get("height"),
|
|
174
|
+
"bytes": existing.get("bytes"),
|
|
175
|
+
"source": existing.get("source"),
|
|
176
|
+
"source_url": existing.get("source_url") or source_url,
|
|
177
|
+
"cached": True,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _browser_like_headers(*, user_agent: str, referer: str | None) -> dict[str, str]:
|
|
182
|
+
headers = {
|
|
183
|
+
"User-Agent": user_agent,
|
|
184
|
+
"Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
|
185
|
+
"Accept-Language": "en-US,en;q=0.9,pt-BR;q=0.8,pt;q=0.7",
|
|
186
|
+
}
|
|
187
|
+
if referer:
|
|
188
|
+
headers["Referer"] = referer
|
|
189
|
+
return headers
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _encode(
|
|
193
|
+
img: Image.Image, *, original_fmt: str, webp_min_savings_pct: int
|
|
194
|
+
) -> tuple[bytes, str]:
|
|
195
|
+
if original_fmt == "WEBP":
|
|
196
|
+
return _encode_as(img, "WEBP"), "WEBP"
|
|
197
|
+
|
|
198
|
+
target_fmt = "PNG" if original_fmt == "GIF" else original_fmt
|
|
199
|
+
orig_bytes = _encode_as(img, target_fmt)
|
|
200
|
+
webp_bytes = _encode_as(img, "WEBP")
|
|
201
|
+
|
|
202
|
+
if not orig_bytes:
|
|
203
|
+
return webp_bytes, "WEBP"
|
|
204
|
+
|
|
205
|
+
savings_pct = (len(orig_bytes) - len(webp_bytes)) / len(orig_bytes) * 100
|
|
206
|
+
if savings_pct >= webp_min_savings_pct:
|
|
207
|
+
return webp_bytes, "WEBP"
|
|
208
|
+
return orig_bytes, target_fmt
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _encode_as(img: Image.Image, fmt: str) -> bytes:
|
|
212
|
+
out = io.BytesIO()
|
|
213
|
+
save_img = img
|
|
214
|
+
if fmt == "JPEG" and img.mode in ("RGBA", "P", "LA"):
|
|
215
|
+
save_img = img.convert("RGB")
|
|
216
|
+
if fmt == "WEBP":
|
|
217
|
+
save_img.save(out, "WEBP", quality=88, method=6)
|
|
218
|
+
elif fmt == "JPEG":
|
|
219
|
+
save_img.save(out, "JPEG", quality=88, optimize=True)
|
|
220
|
+
elif fmt == "PNG":
|
|
221
|
+
save_img.save(out, "PNG", optimize=True)
|
|
222
|
+
else: # pragma: no cover
|
|
223
|
+
raise ValueError(f"formato de saída não suportado: {fmt}")
|
|
224
|
+
return out.getvalue()
|
package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/frontmatter.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Leitura e escrita aditiva do frontmatter YAML de notas Markdown.
|
|
2
|
+
|
|
3
|
+
Contrato:
|
|
4
|
+
- O frontmatter, quando presente, começa em ``---\\n`` na primeira linha e
|
|
5
|
+
termina no próximo ``---\\n``.
|
|
6
|
+
- ``read(text)`` retorna ``(meta_dict, body)``. Sem frontmatter -> ``({}, text)``.
|
|
7
|
+
- ``write(meta, body)`` reemite o frontmatter sempre, com ``meta == {}``
|
|
8
|
+
o frontmatter é omitido.
|
|
9
|
+
- ``update(text, patch)`` aplica patch aditivamente sobre o frontmatter
|
|
10
|
+
existente sem mexer em chaves que não estão no patch e sem reordenar
|
|
11
|
+
agressivamente as chaves originais.
|
|
12
|
+
|
|
13
|
+
Não usa nenhum hack contra os campos de origem do export
|
|
14
|
+
(``chat_id``, ``url``, ``title``, ``exported_at``, ``model``, ``source``,
|
|
15
|
+
``tags``); o caller é responsável por não passar essas chaves no patch.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import yaml
|
|
22
|
+
|
|
23
|
+
_FENCE = "---"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read(text: str) -> tuple[dict[str, Any], str]:
|
|
27
|
+
if not text.startswith(_FENCE + "\n") and not text.startswith(_FENCE + "\r\n"):
|
|
28
|
+
return {}, text
|
|
29
|
+
after_first = text.split("\n", 1)[1]
|
|
30
|
+
end_idx = after_first.find("\n" + _FENCE + "\n")
|
|
31
|
+
if end_idx == -1:
|
|
32
|
+
end_idx_crlf = after_first.find("\n" + _FENCE + "\r\n")
|
|
33
|
+
if end_idx_crlf == -1:
|
|
34
|
+
return {}, text
|
|
35
|
+
end_idx = end_idx_crlf
|
|
36
|
+
yaml_block = after_first[:end_idx]
|
|
37
|
+
rest = after_first[end_idx + len("\n" + _FENCE + "\n") :]
|
|
38
|
+
meta = yaml.safe_load(yaml_block) or {}
|
|
39
|
+
if not isinstance(meta, dict):
|
|
40
|
+
return {}, text
|
|
41
|
+
return meta, rest
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def write(meta: dict[str, Any], body: str) -> str:
|
|
45
|
+
if not meta:
|
|
46
|
+
return body
|
|
47
|
+
yaml_block = yaml.safe_dump(
|
|
48
|
+
meta,
|
|
49
|
+
sort_keys=False,
|
|
50
|
+
allow_unicode=True,
|
|
51
|
+
default_flow_style=False,
|
|
52
|
+
).rstrip("\n")
|
|
53
|
+
return f"{_FENCE}\n{yaml_block}\n{_FENCE}\n{body}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def update(text: str, patch: dict[str, Any]) -> str:
|
|
57
|
+
meta, body = read(text)
|
|
58
|
+
merged = {**meta, **patch}
|
|
59
|
+
return write(merged, body)
|