Proyecto independiente No afiliado, patrocinado ni avalado por la Watch Tower Bible and Tract Society o Jehovah's Witnesses.
jw-agent-toolkit
EN

Release F81 + F82 · operaciones + módulo legal

Scheduler operativo · casos legales TJ vs Estado

Dos módulos del bloque de aplicación. F81 cierra el flujo operativo del meeting-scheduler con importer organized-app + CRDT diff, CLI de edición manual del roster que respeta last_updated para no machacar ediciones en re-imports, y constraints YAML Pydantic strict por congregación. F82 abre el módulo legal con catálogo Territory de 30 países (RU/KP/ER/SG/TJ banned, CN/AZ/BY/VN/MM/TR/CU/KZ restricted, 17 free) componiendo LocaleContext existente sin duplicar campos culturales, y plugin jw-legal con BrainDomain legal-cases-tj registrado via entry-point jw_agent_toolkit.brain_domains — 6 NodeTypeSpec + 8 EdgeTypeSpec, CONTRADICTS non-directional sensitive.

Sub-fases

5

Tests nuevos

+305

Packages nuevos

2

jw-meeting-scheduler · jw-legal

Suite total

3318

passing · 0 regresiones

▌ Decisiones de diseño que ataron las 5 sub-fases

  • CRDT respect: upsert_person guard sobre last_updated, re-import futuro NO machaca edits manuales.
  • Cifrado at-rest opt-in: salt PBKDF2 derivada del congregation_id — mismo passphrase + dos congregaciones = keys distintas.
  • Constraints YAML strict Pydantic: extra='forbid' + validators en cada campo.
  • Territory compone LocaleContext: cero duplicación de campos culturales, test estructural lo enforce.
  • CONTRADICTS non-directional + sensitive=True: la política flag en vez de fusionar leyes incompatibles.
  • Plugin discovery end-to-end: nuevo grupo jw_agent_toolkit.brain_domains en GROUPS, sin tocar dominios existentes.
  • ban_history con fuente inline (URL jw.org/legal, ECHR app no, sentencia SCJN) — cero entries sin fuente.
  • Hand-rolled commented YAML porque PyYAML no preserva comentarios — el coordinador necesita ver qué fila edita.

Fase 81.0

organized-app-importer

Backup JSON → roster local con CRDT diff + dry-run + cifrado at-rest

✅ Entregada 🧪 38 tests T1 Operacional / clave
Guía técnica →

Paquete nuevo jw-meeting-scheduler. Importa el JSON exportado desde organized-app web/desktop a un store SQLite local por congregación. Mapper PersonType → PersonRecord plano: gender derivado de male/female booleans separados, status derivado de publisher_baptized/unbaptized.active, privileges activos sólo, eligible_assignments flattened+deduped, last_updated = max(updatedAt) de los campos Timestamped. Walk recursivo del SchedWeek anidado picking up dict slots {value, type, updatedAt} → AssignmentHistoryEntry con aula auto-detected (suffix _B→aux_class_1, _C→aux_class_2). Pipeline run_import con dry-run flag + compute_person_diff clasifica added/updated/kept_local/unchanged. Cifrado at-rest opt-in con FieldEncryptor + salt PBKDF2 derivada del congregation_id.

Qué se entregó

  • load_backup con Pydantic OrganizedAppBackup envelope + lista PersonType del schema F51.
  • map_person: gender derivado de booleans separados male/female, status de publisher_baptized/unbaptized.active + statusHistory, privileges activos sólo (end_date vacía o futura), eligible_assignments flattened+deduped por sorted(value).
  • slugify_person_id con NFD strip diacritics + intra-word decoration strip (Juan Pérez → juan-perez, O'Connor → oconnor).
  • map_schedule_week walk recursivo del dict tree picking up slots {value, type, updatedAt}, aula auto-detected del field suffix.
  • SchedulerStore SQLite con índice compuesto (person_id, assignment_code, meeting_date DESC) para rotation queries rápidas.
  • CRDT guard en upsert_person: si existing.last_updated ≥ rec.last_updated, no escribe (local newer wins).
  • record_history con INSERT OR IGNORE por entry_id (idempotente).
  • FieldEncryptor wrapper con salt b'jw-meeting-scheduler/v1:' + congregation_id → mismo passphrase + distinto cong = keys distintas (verificado en tests con Fernet InvalidToken).
  • run_import pipeline con dry-run flag + ImportDiff (added/updated/kept_local/unchanged) renderizado en Rich table.
  • jw scheduler import --backup --congregation [--dry-run] [--passphrase] CLI completo.
  • 26 tests scheduler (5 models + 3 crypto + 4 loader + 5 person_mapper + 4 schedule_mapper + 7 store + 4 diff + 4 pipeline) + 2 CLI tests.
  • Guía docs/guias/meeting-scheduler-import.md con quickstart + recetas de cifrado.

Fase 81.1

roster-cli-crdt-preserving

people list · person edit · history — un re-import no machaca tus edits

✅ Entregada 🧪 8 tests T2 CLI / UX
Guía técnica →

CLI de edición manual del roster sobre el store F81.0. La pieza crítica: cada edición toca last_updated con datetime.now(UTC) → un re-import futuro desde organized-app con last_updated anterior NO machaca el edit manual (CRDT guarantee). Permite añadir/quitar privilegios (ms, elder), añadir/quitar eligible assignments (acepta nombre MM_BibleReading o código numérico 100), cambiar status (active/irregular/inactive/disfellowshipped). Y un comando history listing meeting_date DESC con field, code (enum name), aula, confirmed, cancelled para ver el historial de una persona.

Qué se entregó

  • jw scheduler people list (Rich table del roster con privileges, eligible count, last_updated).
  • jw scheduler person edit <slug> con --add-privilege ms/elder, --remove-privilege.
  • --add-eligible / --remove-eligible aceptan nombre AssignmentCode (MM_BibleReading) o código numérico (100).
  • --set-status para cambiar active/irregular/inactive/disfellowshipped/deceased.
  • Cada edición toca last_updated con datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%S') — CRDT-preserving: re-import futuro respeta el edit.
  • jw scheduler history --person <slug> --congregation <id> listing meeting_date DESC con field, code, aula, confirmed, cancelled.
  • Errores claros: persona desconocida → exit code 2 con mensaje Rich rojo, código asignación inválido → ValueError de Pydantic.
  • 8 tests CLI (2 people list + 4 person edit + 2 history).

Fase 81.2

constraints-yaml-per-congregation

Pydantic strict + hand-rolled commented YAML + lint/show

✅ Entregada 🧪 23 tests T2 CLI / UX
Guía técnica →

AssignmentConstraints Pydantic v2 strict (extra='forbid') con 9 campos: congregation_id regex pattern, gap_minimum_days dict[AssignmentCode, int] con 18 códigos default (60d bible_reading, 90d speaker+TGWTalk, 45d student parts), max_assignments_per_month [1,10], pair_experienced_with_novice, require_brother_for_reading, languages_active (≥1), aulas_active (subset de main_hall/aux_class_1/aux_class_2), weights dict[str, float] no-negativos. Loader/writer en constraints_io.py: jw scheduler constraints init escribe el template con comentarios hand-rolled (PyYAML no preserva comments — solución hand-rolled que renderiza cada AssignmentCode.value con su .name como comentario para que el coordinador entienda qué fila edita).

Qué se entregó

  • AssignmentConstraints Pydantic v2 con extra='forbid' — fields desconocidos rechazados al parsear.
  • DEFAULT_GAP_MINIMUM_DAYS: 18 códigos con valores razonables (60d MM_BibleReading, 90d MM_TGWTalk + WM_Speaker + WM_WTStudyConductor, 45d student parts y starting_conversation).
  • Validator field_validator rechaza gap_minimum_days negativos, weights negativos, languages_active vacíos, aulas_active fuera de {main_hall, aux_class_1, aux_class_2}.
  • congregation_id pattern regex ^[a-z0-9_-]{3,64}$ — mismo que PersonRecord.person_id.
  • constraints_io.load_constraints con typed ConstraintsLoadError para malformed YAML vs schema mismatch vs missing file.
  • write_default_constraints con --overwrite guard: por defecto raises si el archivo existe.
  • Hand-rolled commented YAML dump renderiza ' 100: 60 # MM_BibleReading' por cada AssignmentCode (PyYAML no preserva comments).
  • jw scheduler constraints init [--force] · jw scheduler constraints lint · jw scheduler constraints show con 3 Rich tables (key fields + gap_minimum_days + weights).
  • pyyaml>=6 añadido como dep de jw-meeting-scheduler.
  • 23 tests verdes (8 model + 9 IO roundtrip + 6 CLI).

Fase 82.0

territory-catalog-composing-locale-context

30 países curados que componen LocaleContext sin duplicar campos culturales

✅ Entregada 🧪 200 tests T1 Operacional / clave
Guía técnica →

Catálogo Territory en jw_core/territories.py como dataclass que compone jw_core.data.locale_context.LocaleContext via property self.locale por iso_3166. Cero duplicación: campos culturales (name, languages, dominant_religions, sensitive_topics, cultural_anchors, holidays_to_acknowledge, notes) viven SOLO en LocaleContext; campos legales (jw_branch_region, legal_status_summary, ban_history) viven SOLO en Territory. get_territory_full(iso) combina ambos. 30 países curados con ban_history con fuente inline (URL jw.org/legal, número ECHR como Krupko v Russia 26587/07, sentencia SCJN/SCOTUS como Barnette 1943): 5 banned (RU, KP, ER, SG, TJ), 8 restricted (CN, AZ, BY, VN, MM, TR, CU, KZ), 17 free.

Qué se entregó

  • Territory frozen dataclass con iso_3166 + jw_branch_region + legal_status_summary + ban_history; self.locale property compone LocaleContext.
  • LOCALE_CONTEXTS extendido con 15 países nuevos requeridos por el catálogo legal (KP, ER, SG, TJ, CU, VN, MM, GR, AM, AZ, TR, GE, MD, BY, KZ).
  • Bloque 1 banned (5): RU con 2017 Supreme Court ruling + Taganrog ECHR, KP, ER 1994 decree, SG 1972 Societies Act, TJ 2007 Ministerial ban.
  • Bloque 2 free con historial resuelto (12): ES 1970 Religious Liberty, MX 1992 reforma art. 24, US Cantwell + Barnette + Watchtower v Stratton, AR decreto 1867 → 1029, KR 2018 alternative service, DE 2000 Bundesverwaltungsgericht, FR 2011 ECHR Témoins de Jéhovah, GR Kokkinakis + Manoussakis, AM Bayatyan v Armenia.
  • Bloque 3 con contexto adicional (10): VN, MM, TR Tarhan ECHR, GE 97 Members of Gldani, MD, CO, PE, PH Ebralinag, CU, KZ 2011 Religion Law.
  • Helpers get_territory_full (combina Territory + LocaleContext en dict), territories_by_status (banned/restricted/free/unknown), territories_by_branch (substring match).
  • pycountry>=24 añadido como dep de jw-core para validación ISO 3166-1.
  • 91 invariants tests parametrizados: iso_is_valid_alpha2 (pycountry), every_territory_has_locale_context, jw_branch_region_non_empty, ≥30 territorios.
  • Test estructural test_territory_no_field_duplicates_locale_context: assertion sobre Territory.__dataclass_fields__ vs forbidden_overlap {name, languages, dominant_religions, sensitive_topics, cultural_anchors, holidays_to_acknowledge, notes}.
  • Guía docs/guias/territories.md con quickstart + tabla de los 30 países + cómo añadir uno nuevo.

Fase 82.1

jw-legal-brain-domain-plugin

BrainDomain plugin discovery end-to-end con CONTRADICTS sensitive

✅ Entregada 🧪 36 tests T1 Operacional / clave
Guía técnica →

Paquete jw-legal nuevo que registra el BrainDomain legal-cases-tj via entry-point jw_agent_toolkit.brain_domains (sexto grupo del F41 plugin SDK, añadido a jw_core.plugins.registry.GROUPS junto a agents/parsers/embedders/vlm_providers/gen_providers). 6 NodeTypeSpec (LegalCase, Law, Territory referenciando F82.0 por iso, CourtPrecedent, LegalArgument, PersecutionEvent) + 8 EdgeTypeSpec con CONTRADICTS non-directional sensitive=True → la política de conflictos del segundo cerebro flag en vez de fusionar silenciosamente. discover_domains() retorna {'tj', 'legal-cases-tj'} sin conflicto. Conforma estructuralmente al Protocol BrainDomain runtime_checkable.

Qué se entregó

  • Package nuevo packages/jw-legal/ con entry-point [project.entry-points.'jw_agent_toolkit.brain_domains'] legal-cases-tj = 'jw_legal.brain:LegalCasesTJBrainDomain'.
  • Grupo jw_agent_toolkit.brain_domains añadido a jw_core.plugins.registry.GROUPS + REQUIRED_BY_GROUP = ('name', 'nodes', 'edges').
  • 6 NodeTypeSpec con canonical_id_pattern + properties + wiki_page_template + obsidian_subdir + confidence_threshold por tipo.
  • LegalCase confidence 0.95 (sentencias primarias caras de equivocarse), LegalArgument + PersecutionEvent 0.70 (más sujetos a interpretación).
  • Territory referencia el catálogo F82.0 por iso_3166_1_alpha2 — el plugin NO duplica los datos culturales del LocaleContext.
  • 8 EdgeTypeSpec: CITES_LAW, APPLIES_IN_TERRITORY, APPEALS_AGAINST self-link, SUPPORTED_BY_PRECEDENT, CONTRADICTS non-directional + sensitive (flag en vez de fusionar), GROUNDS_ARGUMENT, OCCURRED_IN, JUDGED_BY.
  • Test estructural every_source_and_target_references_known_node_type: cero referencias a tipos inexistentes.
  • LegalCasesTJBrainDomain conforma al Protocol BrainDomain runtime_checkable — isinstance(d, BrainDomain) pasa.
  • discover_domains() retorna {'tj', 'legal-cases-tj'} sin conflicto — verificado end-to-end con clear_plugin_cache().
  • 36 tests verdes (1 scaffold + 12 nodes + 10 edges + 6 BrainDomain class + 7 discovery + 1 plugin verify enumeration update).
  • Guía docs/guias/jw-legal-brain-domain.md con tabla nodos + tabla edges + ejemplos de discovery.

▌ Flujo end-to-end: del backup organized-app al plugin legal

Las 5 sub-fases componen dos flujos paralelos que comparten infra (FieldEncryptor, models_organized, BrainDomain SDK):

# ─── F81: operaciones congregacionales ─────────────────────────────

# 1. Importar el roster desde organized-app
uv run jw scheduler import \
  --backup ~/Downloads/organized-backup.json \
  --congregation kingdom-hall-central \
  --passphrase "correct-horse-battery-staple"

# 2. Editar manualmente (CRDT-preserving — un re-import no machaca)
uv run jw scheduler person edit juan-perez \
  --congregation kingdom-hall-central \
  --add-privilege ms \
  --add-eligible MM_TGWTalk

# 3. Declarar reglas locales en YAML strict
uv run jw scheduler constraints init --congregation kingdom-hall-central
# Editar ~/.jw-agent-toolkit/congregations/kingdom-hall-central/constraints.yaml
uv run jw scheduler constraints lint --congregation kingdom-hall-central
uv run jw scheduler constraints show --congregation kingdom-hall-central


# ─── F82: módulo legal ─────────────────────────────────────────────

# 4. Catálogo Territory F82.0 con 30 países y ban_history con fuente
python -c "
from jw_core.territories import get_territory_full, territories_by_status

full = get_territory_full('RU')
print(full['jw_branch_region'])        # 'Russia (closed since 2017)'
print(full['ban_history'][0])
# "2017-04-20: Supreme Court ruling designates JWs as 'extremist'"
print(full['name']['en'])              # 'Russia' (de LocaleContext)

# Filtrar por status
banned = territories_by_status('banned')
print([t.iso_3166 for t in banned])    # ['RU', 'KP', 'ER', 'SG', 'TJ']
"

# 5. BrainDomain plugin discovery F82.1
python -c "
from jw_brain.domain.registry import discover_domains
from jw_core.plugins.factory import clear_plugin_cache

clear_plugin_cache()
domains = discover_domains()
print(sorted(domains.keys()))          # ['legal-cases-tj', 'tj']

legal = domains['legal-cases-tj']
print([n.name for n in legal.nodes])
# ['LegalCase', 'Law', 'Territory', 'CourtPrecedent',
#  'LegalArgument', 'PersecutionEvent']
print([e.name for e in legal.edges])
# ['CITES_LAW', 'APPLIES_IN_TERRITORY', 'APPEALS_AGAINST',
#  'SUPPORTED_BY_PRECEDENT', 'CONTRADICTS', 'GROUNDS_ARGUMENT',
#  'OCCURRED_IN', 'JUDGED_BY']
"

▌ Próximas piezas — F81.3+ y F82.2+

F81 — meeting-scheduler

  • F81.3 — solver CP-SAT con OR-Tools, hard + soft constraints, ProposedSchedWeek + infeasibility_reason estructurado por slot.
  • F81.4 — agente assignment_generator con @fidelity_wrap.
  • F81.5 — REST endpoints /api/v1/scheduler/{suggest,confirm}.
  • F81.6 — Tauri UI con override slot por slot.

F82 — legal-cases-tj

  • F82.2HUDOCSource ingesta ECHR (Krupko, Bayatyan, Religionsgemeinschaft).
  • F82.3 — agente legal_case_researcher con @fidelity_wrap(PF020 no-hallucinated-rulings, hard).
  • F82.4LegalReasoningStep extiende ReasoningTree F67 con legal_kind.
  • F82.5 — agente hermeneutics_analyzer (10 goldens E2E).
  • F82.6precedent_synthesizer con Meta-orchestrator F65 DAG cross-país.
  • F82.7 — principios YAML PF020-PF024 en jw-eval.