"""chapter_deps — mapa central de dependencias de cómputo por capítulo del EDA. Fuente de verdad ÚNICA de qué necesita cada capítulo de ``CHAPTER_ORDER`` para computarse COMPLETO (sin caer en su rama degradada "datos insuficientes"). Lo consume el pipeline ``render_automatic_eda`` cuando se le pide renderizar un SUBCONJUNTO de capítulos (kwarg ``only_chapters``): antes de perfilar, resuelve los requisitos de los capítulos pedidos y activa SOLO el cómputo que esos capítulos necesitan, de modo que un capítulo suelto siempre llegue poblado y a la vez no se malgaste CPU/LLM en piezas que ningún capítulo pedido usa. Diseño: el mapa es CENTRAL (este módulo), NO una constante por capítulo. Así se evita tocar los ``chapters/.py`` (cada agente es dueño de su capítulo) y se elimina el riesgo de colisión entre ramas. Si un capítulo cambia lo que lee del ``profile``/``ctx``, se actualiza ESTE mapa — es donde el motor mira. Dos clases de dependencia, derivadas inspeccionando qué lee cada capítulo: - ``profile_flags``: flags de coste de ``profile_table`` que hay que ACTIVAR para que el ``profile`` traiga el bloque que el capítulo lee. Son los caros: * ``run_models`` -> ``profile['models']`` (KMeans/IsolationForest/PCA). Lo leen ``outliers`` (fallback del multivariante) y ``modelos``. * ``run_series`` -> ``profile['series']`` (análisis de serie temporal). Lo lee ``timeseries``. * ``run_llm`` -> ``profile['llm']`` (interpretación del modelo). Lo lee ``analisis_llm``. - ``ctx``: etiquetas de las piezas de DATOS CRUDOS que construye ``build_eda_render_ctx`` y que el capítulo lee del ``ctx``. Si la lista está vacía, el capítulo no necesita datos crudos y el pipeline puede saltarse ``build_eda_render_ctx`` por completo cuando ningún capítulo pedido los pide. Etiquetas y claves reales que mapean (ver ``CTX_LABEL_TO_KEYS``): * ``head_rows`` -> ``ctx['head_rows']`` (overview: df.head real). * ``raw_numeric`` -> ``ctx['raw_numeric']`` (outliers/modelos/ correlacion/missingness/geospatial: muestra numérica alineada por fila). * ``timeseries_raw`` -> ``ctx['timeseries_raw']`` (timeseries: serie cruda). * ``geo_points`` -> ``ctx['geo_points']`` (+ ``raw_numeric``) (geospatial: lat/lon). * ``db_path_table`` -> ``ctx['db_path']`` + ``ctx['table']`` (agregacion/ text_distr/missingness/relaciones: push-down de queries propias). ``portada`` y ``glosario`` NO son opcionales: el pipeline los incluye SIEMPRE (la portada resume el documento y el glosario es el destino de los términos clicables), así que aquí se declaran sin requisitos de cómputo. Todas las funciones de este módulo son PURAS (no I/O, deterministas): se prestan a test unitario directo. """ from __future__ import annotations # Mapa central. Una entrada por id de CHAPTER_ORDER. ``profile_flags`` lista los # flags de coste a activar; ``ctx`` las etiquetas de datos crudos que lee. Las # claves vacías significan "no necesita ese tipo de dependencia". CHAPTER_DEPS = { # Portada y glosario: SIEMPRE presentes, sin cómputo propio (la portada lee # el document_summary que arma build_document; el glosario lee los términos # que el resto registró). Se declaran para que el mapa cubra CHAPTER_ORDER # entero y la validación los reconozca. "portada": {"profile_flags": [], "ctx": []}, "overview": {"profile_flags": [], "ctx": ["head_rows"]}, "analisis_llm": {"profile_flags": ["run_llm"], "ctx": []}, "num_distr": {"profile_flags": [], "ctx": []}, "cat_distr": {"profile_flags": [], "ctx": []}, # text_distr empuja su propia query de texto (no usa raw_numeric); necesita # db_path/table en el ctx para hacerlo. "text_distr": {"profile_flags": [], "ctx": ["db_path_table"]}, "calidad": {"profile_flags": [], "ctx": []}, # missingness lee la muestra numérica cruda (co-ocurrencia de ausencias) y # puede empujar una query de patrón de nulos con db_path/table. "missingness": {"profile_flags": [], "ctx": ["raw_numeric", "db_path_table"]}, # outliers corre IsolationForest EN VIVO sobre ctx['raw_numeric']; run_models # asegura además el fallback profile['models']['outliers'] si el ctx faltara. "outliers": {"profile_flags": ["run_models"], "ctx": ["raw_numeric"]}, "correlacion": {"profile_flags": [], "ctx": ["raw_numeric"]}, "relaciones": {"profile_flags": [], "ctx": ["db_path_table"]}, "modelos": {"profile_flags": ["run_models"], "ctx": ["raw_numeric"]}, "timeseries": {"profile_flags": ["run_series"], "ctx": ["timeseries_raw"]}, "geospatial": {"profile_flags": [], "ctx": ["geo_points", "raw_numeric"]}, "agregacion": {"profile_flags": [], "ctx": ["db_path_table"]}, "glosario": {"profile_flags": [], "ctx": []}, } # Capítulos que el documento incluye SIEMPRE, independientemente de only_chapters. ALWAYS_PRESENT = ("portada", "glosario") # Flags de coste reconocidos (el orden no importa; se devuelven como set). KNOWN_PROFILE_FLAGS = ("run_models", "run_series", "run_llm") # Mapeo de cada etiqueta de ctx a las claves REALES que produce # build_eda_render_ctx. ``db_path_table`` es especial: db_path/table siempre se # ponen para un backend válido y son inofensivos, por eso no se podan nunca (no # aparecen en DATA_CTX_KEYS). El resto (head_rows/raw_numeric/timeseries_raw/ # geo_points) son las piezas de datos podables. CTX_LABEL_TO_KEYS = { "head_rows": {"head_rows"}, "raw_numeric": {"raw_numeric"}, "timeseries_raw": {"timeseries_raw"}, "geo_points": {"geo_points", "raw_numeric"}, "db_path_table": set(), # db_path/table siempre presentes; nunca se podan. } # Claves de datos crudos del ctx que se pueden podar cuando ningún capítulo # pedido las necesita (las que cuestan muestreo). db_path/table NO entran aquí. DATA_CTX_KEYS = ("head_rows", "raw_numeric", "timeseries_raw", "geo_points") def _as_id_list(chapter_ids): """Normaliza la entrada a una lista de ids string, defensiva. None -> [].""" if chapter_ids is None: return [] if isinstance(chapter_ids, str): return [chapter_ids] return [c for c in chapter_ids if isinstance(c, str)] def validate_chapter_ids(chapter_ids, order): """Separa los ids pedidos en válidos y desconocidos respecto a ``order``. Args: chapter_ids: lista (o str) de ids de capítulo pedidos. order: lista canónica de ids válidos (CHAPTER_ORDER). Returns: dict ``{"valid": [...], "unknown": [...]}`` preservando el orden de aparición de la entrada. Función pura. """ valid_set = set(order or []) valid, unknown = [], [] for cid in _as_id_list(chapter_ids): (valid if cid in valid_set else unknown).append(cid) return {"valid": valid, "unknown": unknown} def resolve_requirements(chapter_ids): """Une los requisitos de cómputo de los capítulos pedidos. Es el corazón de la resolución de dependencias: dado el subconjunto de capítulos a renderizar, devuelve TODO lo que hay que activar/construir para que esos capítulos lleguen COMPLETOS, y solo eso. Los capítulos ``ALWAYS_PRESENT`` (portada/glosario) se añaden implícitamente porque el pipeline siempre los incluye; como no tienen requisitos, no alteran el resultado, pero se contemplan para que el conjunto sea coherente. Args: chapter_ids: lista (o str) de ids de capítulo. Ids desconocidos se ignoran silenciosamente (la validación estricta es de quien llama). None o lista vacía -> requisitos vacíos. Returns: dict ``{"profile_flags": set[str], "ctx_keys": set[str]}`` donde ``ctx_keys`` son las ETIQUETAS de ctx (no las claves reales). Función pura. """ ids = set(_as_id_list(chapter_ids)) | set(ALWAYS_PRESENT) profile_flags = set() ctx_keys = set() for cid in ids: dep = CHAPTER_DEPS.get(cid) if not isinstance(dep, dict): continue for f in dep.get("profile_flags", []) or []: if f in KNOWN_PROFILE_FLAGS: profile_flags.add(f) for k in dep.get("ctx", []) or []: ctx_keys.add(k) return {"profile_flags": profile_flags, "ctx_keys": ctx_keys} def resolve_profile_flags(chapter_ids): """Atajo: solo el set de profile_flags a activar para los capítulos pedidos. Función pura. Devuelve un set ⊆ KNOWN_PROFILE_FLAGS. """ return resolve_requirements(chapter_ids)["profile_flags"] def needs_render_ctx(chapter_ids): """True si algún capítulo pedido necesita datos crudos del ctx. Cuando es False, el pipeline puede saltarse ``build_eda_render_ctx`` entero (ahorro real de CPU/I/O): los capítulos pedidos no leen ninguna pieza de datos crudos. Función pura. """ return bool(resolve_requirements(chapter_ids)["ctx_keys"]) def resolve_ctx_data_keys(chapter_ids): """Claves REALES de datos del ctx a CONSERVAR para los capítulos pedidos. Traduce las etiquetas de ctx a las claves concretas que produce ``build_eda_render_ctx`` (head_rows/raw_numeric/timeseries_raw/geo_points). El pipeline poda del ctx las claves de datos que NO estén en este set, para que un capítulo suelto no arrastre piezas de datos que no usa. db_path/table nunca se podan (no aparecen aquí). Función pura. Returns: set[str] subconjunto de DATA_CTX_KEYS. """ req = resolve_requirements(chapter_ids) keep = set() for label in req["ctx_keys"]: keep |= CTX_LABEL_TO_KEYS.get(label, set()) # Solo claves de datos podables (db_path/table se gestionan aparte). return {k for k in keep if k in DATA_CTX_KEYS}