feat: DuckDB como fuente de verdad (multi-valor, ownership selectivo, escritura, libretas)
F1 — migraciones: 002_multivalue (persons +telefonos/emails/direcciones/extra_fm JSON,
backfill desde singulares con to_json) + 003_addressbooks (tabla addressbooks + seed
idempotente de la libreta por defecto). Conteos intactos (697/1065/98).
F2 — ingest_vault selectivo (anti-pisado): personas que ya existen en DB solo actualizan
note_path + extra_fm vía duckdb_upsert(update_cols=...), NO pisan los campos OWNED por la
DB; personas nuevas = bootstrap completo. _link_contacts enlaza por listas telefonos[]/
emails[] además del singular. ingest_dav itera todas las libretas de la tabla addressbooks.
F3 — escritura estructurada (server/writes.py + endpoints en main.py): CRUD
/api/person|contact|event, /api/addressbook, /api/calendar, /api/person/{slug}/render
(DB→nota preservando la prosa del cuerpo), /api/push/dav (reconcilia DB→Xandikos). El push
DAV y el render ocurren fuera de la transacción de escritura para no bloquear la DB con
latencia de red. registry_bridge.py importa las funciones nuevas; app.md actualizado.
Verificado: 18 tests verdes; ownership probado sobre datos reales (un centinela DB-owned
sobrevivió a POST /api/ingest/vault sobre las 697 fichas); person CRUD + materialización
de la ficha .md en vivo, con cleanup sin residuo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,9 +13,18 @@ uses_functions:
|
||||
- slugify_obsidian_name_py_obsidian
|
||||
- dav_get_collection_py_infra
|
||||
- dav_list_calendars_py_infra
|
||||
- dav_list_addressbooks_py_infra
|
||||
- dav_collection_ctag_py_infra
|
||||
- carddav_put_vcard_py_infra
|
||||
- caldav_put_event_py_infra
|
||||
- dav_delete_resource_py_infra
|
||||
- dav_make_addressbook_py_infra
|
||||
- dav_make_calendar_py_infra
|
||||
- pass_get_secret_py_infra
|
||||
- duckdb_query_readonly_py_infra
|
||||
- duckdb_execute_py_infra
|
||||
- duckdb_upsert_py_infra
|
||||
- build_vcard_py_core
|
||||
- render_markdown_table_py_core
|
||||
- upsert_sentinel_block_py_core
|
||||
uses_types: []
|
||||
@@ -59,7 +68,14 @@ en el body (el plugin parsea el body, no el código HTTP).
|
||||
completo del vault) + `persons`, `organizations`, `domains`, `cases`,
|
||||
`places` (fichas de nivel-1 de cada carpeta de entidades, excluyendo las
|
||||
notas con prefijo `_`). Cada una lleva `note_path`: el path relativo de la
|
||||
nota dentro del vault.
|
||||
nota dentro del vault. **`persons` es dueña de sus campos estructurados**
|
||||
(multi-valor `telefonos`/`emails`/`direcciones` JSON + singulares de compat
|
||||
`telefono`/`email`/`direccion`): la API los edita y los materializa a la
|
||||
nota. El ingest del vault es **selectivo** para `persons` — una ficha que ya
|
||||
existe en la DB solo refresca `note_path` + `extra_fm` (el frontmatter
|
||||
no-owned), conservando los campos OWNED; una ficha nueva se inserta completa
|
||||
(bootstrap desde el frontmatter). `addressbooks` (schema `main`) registra
|
||||
las libretas CardDAV: el ingest DAV las recorre todas (no solo la fija).
|
||||
2. **Maestras DAV** (schema `main`): `contacts` y `events` importados de
|
||||
Xandikos — fuente de verdad del lado agenda/calendario. `contacts.note_path`
|
||||
se enlaza contra `persons` matcheando por UID `osint-<slug>`, por el
|
||||
@@ -104,9 +120,16 @@ Health check: `curl http://127.0.0.1:8771/api/health`.
|
||||
| POST | `/api/query` | `{sql, params, max_rows}` → respuesta exacta de `duckdb_query_readonly` (solo lectura) |
|
||||
| GET | `/api/queries` | catálogo de queries con nombre (`server/named_queries.py`) |
|
||||
| POST | `/api/query/named` | `{name, max_rows}` → misma shape que `/api/query` |
|
||||
| POST | `/api/ingest/vault` | escanea el vault completo y reconstruye notes + entidades + derivadas |
|
||||
| POST | `/api/ingest/dav` | baja Xandikos (CardDAV + cada calendario CalDAV), reconstruye contacts/events, enlaza y reconstruye derivadas |
|
||||
| POST | `/api/ingest/vault` | escanea el vault completo; notes y entidades de espejo puro se reemplazan, persons se ingesta SELECTIVO (existentes solo `note_path`+`extra_fm`, nuevas bootstrap completo) |
|
||||
| POST | `/api/ingest/dav` | baja TODAS las libretas registradas en `addressbooks` + cada calendario CalDAV, reconstruye contacts/events, enlaza y reconstruye derivadas |
|
||||
| POST | `/api/render/note` | `{note_path, block_id, sql\|query, title?}` → tabla Markdown upsertada como bloque sentinel `osintdb` en la nota (la crea si no existe) |
|
||||
| POST/PUT/DELETE | `/api/person[/{slug}]` | CRUD de personas multi-valor (`telefonos`/`emails`/`direcciones` listas). Tras escribir la DB, materializa la ficha DB→nota (singulares = `lista[0]`) sin tocar la prosa |
|
||||
| POST | `/api/person/{slug}/render` | re-materializa la ficha DB→nota (frontmatter OWNED + merge `extra_fm`, preserva el body) |
|
||||
| POST/PUT/DELETE | `/api/contact[/{uid}]` | CRUD de contactos CardDAV (`tels`/`emails` listas). Tras la DB, push DB→Xandikos (`build_vcard`+`carddav_put_vcard`, o `dav_delete_resource` en delete) fuera de la transacción |
|
||||
| POST/PUT/DELETE | `/api/event[/{uid}]` | CRUD de eventos CalDAV. Push `caldav_put_event`/`dav_delete_resource` |
|
||||
| POST | `/api/addressbook` | `{slug, display_name?, description?, color?}` → `dav_make_addressbook` + INSERT en `addressbooks` |
|
||||
| POST | `/api/calendar` | `{slug, display_name?, color?}` → `dav_make_calendar` (paridad) |
|
||||
| POST | `/api/push/dav` | reconcilia en bloque: recorre `contacts` y `events` de la DB y los empuja a Xandikos (PUT, sin borrar). Útil tras la migración |
|
||||
|
||||
Queries con nombre incluidas: `personas_por_contexto`, `personas_recientes`,
|
||||
`eventos_proximos`, `contactos_sin_nota`, `stats_personas`,
|
||||
|
||||
Reference in New Issue
Block a user