Arquitectura
Manual de arquitectura del proyecto. Cubre objetivos, organización en capas, inventario de endpoints externos, decisiones de diseño clave y políticas que se mantienen vigentes a través de todas las fases.
Objetivos
- Fuente única de verdad para el acceso a contenido de jw.org / wol.jw.org en Python.
- Desacoplar el acceso a datos (
jw-core) de las superficies de exposición (jw-cli,jw-mcp) y de los comportamientos de alto nivel (jw-rag,jw-agents). - Citas siempre verificables: cada respuesta de cualquier agente debe poder enlazarse a una URL de wol.jw.org.
- Sin LLM en el camino crítico: los parsers, clientes y agentes son determinísticos. La síntesis con LLM ocurre fuera del toolkit (Claude Desktop, Claude Code, tu propio cliente).
Organización en capas
┌──────────────────────────────────────────────────────────────────────┐
│ Skills (Markdown) Agentes (orquestación multi-paso) │
│ skills/jw-*/SKILL.md packages/jw-agents/ │
└────────────────────────────────────┬─────────────────────────────────┘
│
┌────────────────────────────────────▼─────────────────────────────────┐
│ Superficies │
│ • CLI packages/jw-cli/ (Typer + Rich) │
│ • Servidor MCP packages/jw-mcp/ (FastMCP) │
│ • RAG packages/jw-rag/ (vector + BM25 + RRF) │
└────────────────────────────────────┬─────────────────────────────────┘
│
┌────────────────────────────────────▼─────────────────────────────────┐
│ jw-core (librería) │
│ ├─ clients/ cdn.py · mediator.py · wol.py │
│ │ pub_media.py · topic_index.py · weblang.py │
│ │ _polite.py (helper) · factory.py (suite) │
│ ├─ parsers/ reference.py · article.py · daily_text.py │
│ │ verse.py · study_notes.py · topic_index.py │
│ │ epub.py · jwpub.py (decrypt AES-128-CBC) │
│ │ jw_library_backup.py (Fase 19, .jwlibrary) │
│ ├─ integrations/ (Fase 19 — JW Library app, Fase 20 — Obsidian)│
│ │ jw_library.py (deep links jwlibrary://) │
│ │ jw_library_sync.py (sync incremental) │
│ │ jw_library_local.py (inspector + FDA macOS) │
│ │ meps_catalog.py (docid ↔ pub_code SQLite) │
│ │ markdown.py (linkify + convert + render md) │
│ │ obsidian_vault.py (vault → RAG + backup → md) │
│ ├─ data/bible_books/ (Fase 20 — 17 locales JSON) │
│ ├─ data/ books.py (66 libros × 3 idiomas) · objections │
│ ├─ models.py BibleRef · Verse · StudyNote · CrossReference │
│ │ TopicSubject/Subheading/Citation │
│ │ Epub · EpubDocument · JwpubMetadata · ... │
│ ├─ auth.py JWTManager (extraído de cdn) │
│ ├─ cache.py DiskCache (SQLite + TTL + WAL) │
│ ├─ throttle.py TokenBucket · Throttler · backoff_delay │
│ ├─ telemetry.py Telemetry (opt-in API drift detection) │
│ └─ languages.py │
└────────────────────────────────────┬─────────────────────────────────┘
│
jw.org / wol.jw.org / b.jw-cdn.org
data.jw-api.org/mediator · www.jw.org/{iso}/languages/
Las dependencias fluyen hacia abajo únicamente. Cada paquete depende de jw-core (y jw-rag también es usado por jw-agents y por jw-mcp).
Reglas duras:
jw-coreno importa nada del resto del workspace.jw-ragpuede importarjw-core(clientes para el ingest).jw-agentspuede importarjw-coreyjw-rag.jw-clipuede importarjw-core(no agentes — los agentes viven detrás del MCP por ahora).jw-mcppuede importar todos los anteriores y es el único que liga el RAG global.jw-interp(F80) depende solo dejw-eval(para principios). NO es importado porjw-agents: el Tier 4 defidelity_wrapse enchufa vía callable contractCallable[[str], dict[str, float]], sin acoplamiento de paquete.
Alineamiento y interpretabilidad (F77–F80)
┌────────────────────────────────────────────────────────────────────────┐
│ Pila de alineamiento doctrinal │
├────────────────────────────────────────────────────────────────────────┤
│ F77 principios YAML → jw_eval/principles/ (5 builtin, versioned) │
│ │ │
│ ▼ │
│ F78 judge oracle → jw_finetune/synth/judge/ (3-stage scoring) │
│ F78 SL-CAI critique → jw_finetune/synth/critique.py │
│ F78 preference data → jw_finetune/synth/preference.py │
│ │ │
│ ▼ │
│ F79 DPO/ORPO → jw_finetune/train/{dpo,orpo}.py (Unsloth) │
│ │ │
│ ▼ │
│ F80 SL-CAI CLI → jw-finetune build-critique-dataset │
│ F80 probing → jw_interp/{probing,activations,contrastive} │
│ F80 steering+patching → jw_interp/{steering,patching}.py │
│ F80 SAE adapters → jw_interp/{qwen,gemma}_scope.py │
│ F80 runtime probes → jw_interp/{probe_store,runtime}.py │
│ │ │
│ ▼ │
│ Runtime: fidelity_wrap(probe_evaluator=…) │
│ ├─ Tier 1: principios regex (F77) │
│ ├─ Tier 2: NLI entailment (F39) │
│ ├─ Tier 3: judge oracle (F78, training-time) │
│ └─ Tier 4: probes lineales (F80.5, observacional) │
└────────────────────────────────────────────────────────────────────────┘
Las cuatro fases preservan la regla cardinal: la fuente de verdad es el material vigente publicado por la organización; este toolkit solo refleja ese canon. F77–F79 es ingeniería de alineamiento aguas arriba; F80 es auditoría interpretable de runtime que nunca veta un Finding por sí sola — solo anota evidencia para el humano.
Inventario de endpoints JW.org
| Endpoint | Método | Auth | Envuelto por |
|---|---|---|---|
b.jw-cdn.org/tokens/jworg.jwt | GET | — | auth.JWTManager.get_token |
b.jw-cdn.org/apis/search/results/{lang}/{filter}?q= | GET | JWT | clients.cdn.CDNClient.search |
b.jw-cdn.org/apis/pub-media/GETPUBMEDIALINKS | GET | — | clients.pub_media.PubMediaClient.get_publication |
data.jw-api.org/mediator/v1/languages/{lang}/web | GET | — | clients.mediator.MediatorClient.list_languages |
data.jw-api.org/mediator/finder?lang=&item= | GET | — | clients.mediator.MediatorClient.find_item |
www.jw.org/{iso}/languages/ | GET | — | clients.weblang.WeblangClient.list_languages |
wol.jw.org/{iso}/wol/b/{resource}/{lp_tag}/{pub}/{book}/{ch} | GET | — | clients.wol.WOLClient.get_bible_chapter |
wol.jw.org/{iso}/wol/d/{resource}/{lp_tag}/{docId} | GET | — | WOLClient.fetch · get_document_by_id · TopicIndexClient.get_subject_page |
wol.jw.org/{iso}/wol/dt/{resource}/{lp_tag}/{YYYY}/{M}/{D} | GET | — | WOLClient.get_daily_text_by_date |
wol.jw.org/{iso}/wol/h/{resource}/{lp_tag} | GET | — | WOLClient.get_today_homepage |
wol.jw.org/{iso}/wol/publication/{resource}/{lp_tag}/{pub}[/{n}] | GET | — | WOLClient.get_publication_page |
wol.jw.org/{iso}/wol/bc/{resource}/{lp_tag}/{doc}/{group}/{index} | GET | — | WOLClient.get_cross_reference_panel |
Formato JWPUB (offline): ZIP → manifest.json + ZIP interno → imágenes + SQLite .db con columna Document.Content cifrada AES-128-CBC sobre zlib. La derivación de clave es SHA256(f"{lang}_{symbol}_{year}") XOR _XOR_KEY (32-byte magic constant), descubierta por gokusander/jwpub-toolkit (MIT). Implementada en parsers.jwpub._compute_key_iv desde Fase 5.5.
Wire-up Fase 9: cada cliente acepta throttler, cache y telemetry opcionales en su constructor. Cuando se pasan (típicamente vía clients.factory.build_clients()), todo GET pasa por _polite.politely_get() que aplica:
- Rate limit per host (token bucket conservador: 2 req/s, burst 5).
- Cache hit-check en DiskCache (SQLite con TTL).
- Drift fingerprint en Telemetry (sólo si
JW_TELEMETRY_ENABLED=1).
Para el detalle de cada endpoint (parámetros, respuestas, ejemplos), ver docs/conceptos/inventario-endpoints.md.
Por qué monorepo
- Tipos compartidos (
BibleRef,Article,StudyNote, etc.) cambian con frecuencia al inicio; el overhead de PRs cross-repo sería caro. - Commits atómicos a través de core + MCP + tests.
- Un único
uv.lockhace los instalables reproducibles para CI y contribuidores. - Cada
packages/*sigue siendo publicable independientemente a PyPI cuando esté estable.
Estrategia de idiomas
Multi-idioma desde el día 1, pero sin pretender que todos sean iguales:
- Nivel 1 (parser, URLs, herramientas): Inglés (E), Español (S), Portugués (T).
- Nivel 2 (solo construcción de URLs): cualquier idioma registrado en
languages.py. - Nivel 3 (fallback elegante): idioma desconocido → inglés.
El parser de referencias tiene una limitación documentada: cuando dos idiomas comparten una ortografía idéntica tras quitar acentos (p.ej. “Corintios” ≈ “Coríntios”), gana el primer idioma registrado para detected_language. El número de libro siempre es correcto.
Detalles completos en docs/conceptos/estrategia-multi-idioma.md.
Diseño del parser de referencias
Ver packages/jw-core/src/jw_core/parsers/reference.py. Decisiones clave:
- Regex maestra única construida desde
BOOKSen tiempo de import, con alternativas ordenadas de mayor a menor longitud para evitar que “John” gane sobre “1 John”. - Matching en dos etapas: la regex captura el texto del libro normalizado; un lookup por clave despojada obtiene el número de libro e idioma.
- Idempotente: cacheado como singleton a nivel de módulo vía
lru_cache. - Sin I/O: puro CPU. Seguro de llamar dentro de handlers MCP.
Política de citas (Phase 4+)
Cada Finding que produce un agente carga metadata['source'], que sirve para que el LLM llamante haga ranking por autoridad:
topic_index # Índice de Publicaciones Watch Tower
> topic_index_entry # Subtítulos del índice
> question_refs # Citas explícitas en la pregunta del usuario
> verse_text # Texto del versículo enriquecido
> study_note # Notas de estudio nwtsty
> cdn_search # Resultados de búsqueda CDN
> rag # Corpus local RAG
El agente apologetics aplica este ranking implícitamente al orden en que añade findings.
Superficie de herramientas MCP
| Fase | Herramientas |
|---|---|
| 1 — Núcleo | resolve_reference, get_chapter, get_daily_text (con date opcional), search_content, get_article |
| 2 — Media | list_languages, list_publication_files, download_publication, get_publication_toc, list_weblang_languages |
| 3 — Notas | get_verse, get_study_notes, get_cross_references, compare_translations |
| 4 — Temas | search_topic_index, get_topic_articles |
| 5 — EPUB | extract_epub_text, ingest_epub |
| 5.5 — JWPUB | inspect_jwpub_metadata, extract_jwpub_text, ingest_jwpub |
| 6 — RAG | semantic_search, ingest_bible_chapter, ingest_search_topk |
| 7 — Agentes | verse_explainer, research_topic, meeting_helper, apologetics |
| 9 — Infra | get_cache_stats |
| 19 — Integraciones JW Library | open_in_jw_library, import_jw_library_backup, list_user_notes, ingest_user_notes, sync_jw_library_backup, register_jwpub_in_catalog, find_publication_in_catalog, open_publication_by_symbol, inspect_local_jw_library_tool, check_jw_library_full_disk_access, read_jw_library_live_userdata |
| 20 — Obsidian bridge | linkify_markdown_text, convert_jw_links_in_markdown, get_verse_as_markdown, index_obsidian_vault, export_jw_library_backup_to_vault |
Total con Fase 20: ~60 herramientas. Contratos completos en docs/referencia/jw-mcp.md y docs/referencia/integraciones.md.
Manejo de errores
Cada cliente HTTP tiene su propia excepción base:
CDNError(clients.cdn)WOLError(clients.wol)MediatorError(clients.mediator)PubMediaError(clients.pub_media)TopicIndexError(clients.topic_index)
La capa de integraciones (Fase 19) añade sus propias excepciones:
JWLibraryError(integrations.jw_library) — URL build / dispatchJWLibraryBackupError(parsers.jw_library_backup) — archivo.jwlibraryinválidoMacOSFullDiskAccessError(integrations.jw_library_local) — TCC bloqueó la lectura del container
Todas heredan de RuntimeError y se elevan en lugar de devolver None para errores HTTP. Las herramientas MCP capturan estas excepciones y devuelven un dict {"error": "..."} en lugar de propagar — esto mantiene la sesión MCP viva ante fallos transitorios.
Los parsers son tolerantes: devuelven listas vacías o None ante HTML mal formado, sin levantar excepciones.
Lo que deliberadamente NO está aquí (todavía)
- Resolución código de publicación → URL (p.ej. “g05 4/22 7” → URL real del artículo). Requiere combinar
GETPUBMEDIALINKScon un mapeopub-code → URL patternque aún no se ha construido. Hoy las citas del índice temático devuelven el texto abreviado. - Embedders reales por defecto (la interfaz
Embedderestá; los providers OpenAI / sentence-transformers son extras opcionales[openai]/[local]. El defaultFakeEmbedderdeja a BM25 cargando el peso real). - Publicar
jw-corea PyPI (tracking en Fase 9; queda como siguiente paso operacional).
Ya no son pendientes (estaban en versiones anteriores de este doc):
Decodificación JWPUB cifrado→ resuelto en Fase 5.5.Cache persistente en disco→cache.DiskCacheen Fase 9.Rate limiting→throttle.Throttleren Fase 9.Telemetría opt-in→telemetry.Telemetryen Fase 9.CI workflow→.github/workflows/ci.ymlen Fase 10.
Nota de licencia
Parte del código en jw-core/clients/ está informado por, pero no copia, jwlib (allejok96, GPL-3.0). El toolkit completo es GPL-3.0-only, así que la reutilización directa de snippets de jwlib sería compatible en licencia si fuera necesaria en fases posteriores.
Edit this page on docs/architecture.md