CreaRack-SL

Servicio: Colector de frescura de dependencias (Python)

Descripción general

Desde task #275 (28-08-2026, v1.86.4) el colector está troceado en 4 módulos para no pasar el límite de 500 LOC/módulo (Regla 5). El CLI y el punto de entrada del workflow siguen siendo scripts/deps_freshness.py; delega en:

  • scripts/deps_specs.py (264 LOC) — resolución de horquillas: qué versión publicada satisface de verdad un spec (>=2.31.0, ^9.3.0…), si el spec tiene techo superior, y si el major efectivo ya se ha movido sin que nadie lo note. Usa packaging (PEP 440) si está disponible; si no, un respaldo propio para >=, >, <, <=, ==, !=, ~= (PyPI) y caret/tilde/exacto/comodín/|| (npm).
  • scripts/deps_sources.py (136 LOC) — el dataclass Dep, los parsers de manifiestos (requirements.txt, package.json) y los lectores de “instalada” (pip list --format=json, package-lock.json, pnpm list --json).
  • scripts/deps_holds.py (97 LOC, nuevo · v1.86.10, 28-08-2026) — retenciones deliberadas: carga y valida deps_holds.json (raíz del repo) y marca las filas del colector que están paradas A PROPÓSITO, con el motivo. Ver sección dedicada abajo.
  • scripts/deps_freshness.py — orquestador: cruza registro (PyPI/npm) + OSV.dev, aplica los holds, decide la fuente de “instalada”, deduplica y publica en POST /api/tools/deps.

Colecta el estado de frescura de dependencias en dos repositorios (CreaRack-Pro y CreaRackSL-Workspace) sin hardcodear paquetes.

El bug que motivó el troceo

Hasta v1.86.3, una horquilla como requests>=2.31.0 se evaluaba literalmente por el mínimo (2.31.0) en vez de por la versión que pip/npm instalarían de verdad (la mayor publicada que la satisface). Cruzar ese mínimo contra OSV.dev inventaba avisos de seguridad de versiones que nadie tenía instaladas: 22 HIGH de aiohttp, 6 de requests, 2 de pytest, contrastado contra el pip freeze real de PROD. deps_specs.py resuelve ahora la horquilla a la versión real (Dep.resolved) antes de consultar OSV — los tres casos bajaron a 0 avisos.

Retenciones deliberadas (deps_holds.json, v1.86.10)

Una dependencia parada A PROPÓSITO (redis 7.4.1 porque la serie 8.x corta el channel layer en vivo, Django 6.0 porque django-prometheus exige <6.1, ruff/mypy con pin exacto hasta auditar las reglas nuevas) no es una dependencia olvidada — pero antes de este cambio salía en rojo igual que una atrasada sin motivo. deps_holds.json (raíz de CreaRack-Pro) declara esas retenciones; deps_holds.py las carga y las cruza contra las filas del colector.

  • load(path) — lee y valida deps_holds.json. Fichero ausente → {} (falla suave, 0 retenciones, sin ruido). Fichero mal escrito (falta un campo obligatorio, ecosystem/repo fuera de catálogo, since no es YYYY-MM-DD, retención duplicada) → excepción HoldsError, que deps_freshness.py convierte en ::error:: y exit 2 — a propósito duro: un hold que no se aplicase pasaría desapercibido.
  • apply(deps, holds) — empareja cada hold con su dependencia por (repo, ecosystem, paquete normalizado — PEP 503 en PyPI) y le rellena hold_reason/hold_since/hold_ref/hold_until. Los holds que no casan con ninguna fila publican ::warning:: (paquete borrado o nombre mal escrito en deps_holds.json).
  • Efecto en el resumen (deps_freshness.lag_counts): una fila con hold_reason y desfase major/minor no cuenta en esos contadores — se resta aparte como «retenidas a propósito». El patch/uptodate sigue contando igual.
  • Campos obligatorios de cada entrada: package, ecosystem (pypi|npm), repo (crearack-pro|workspace), reason (llano, 1-2 frases), since (YYYY-MM-DD), ref (task/issue/PR); until es opcional (condición o fecha para revisar).
  • Retenciones vigentes (28-08-2026): redis (7.4.1, task #204 · redis-py #2807), Django (<6.1 por django-prometheus), ruff y mypy (pin exacto hasta septiembre 2026, con Dani y Txell) — las 4, task #275.

Cómo se resuelve “instalada” (por prioridad)

  1. PROD real — --prod-versions-url / --prod-token contra GET /api/installed-packages ([[entity—api—endpoint—installed-packages]]), el mismo contenedor que sirve producción. installed_source = "prod".
  2. Runner de CI — pip-list.json / pnpm-list.json generados en el propio job. installed_source = "runner".
  3. Lockfile — package-lock.json (100% ==). installed_source = "lockfile".

Si PROD no responde (fetch_prod_versions falla suave), cae al runner sin romper la ejecución.

Campos nuevos en Dep

CampoQué dice
manifestsTodos los manifiestos donde aparece el paquete tras deduplicar, separados por coma
resolvedMayor versión publicada que satisface pinned
installed_sourceprod | runner | lockfile | None
unboundedEl spec no pone techo superior (ej. openai>=2.26.0 sin <3)
unbounded_majorAdemás, el major efectivo ya se ha movido del declarado (bajo 0.x el minor cuenta como major — así se detectó que anthropic>=0.52.0 ya corría en 1.x)
fix_within_specTrue: el aviso OSV tiene fix dentro de la horquilla, basta un redeploy · False: hay que cambiar el pin a mano · None: sin datos de fix
hold_reason / hold_since / hold_ref / hold_until(v1.86.10) No nulo = fila retenida a propósito. Motivo, fecha de la decisión, task/issue/PR que la respalda, y condición o fecha para revisarla — ver [[#retenciones-deliberadas-deps_holds-json-v1-86-10]] arriba

Deduplicación

dedupe(deps) colapsa a una fila por (repo, ecosistema, paquete): antes, un paquete presente en varios manifiestos salía repetido (asyncssh ×2, requests ×3, pysnmp ×2). Se queda con la horquilla más restrictiva (restrictiveness()) y lista el resto en manifests. El snapshot pasó de ~130 filas a 109 en el primer dry-run tras el cambio.

Manifiestos soportados

RepoEcosistemaRutaInstalada desde
CreaRack-ProPyPIrequirements.txtPROD → pip list --format=json (CI) → lockfile
CreaRack-ProPyPIterminal/agent/requirements-agent.lockLockfile (100% ==)
CreaRack-ProPyPItests/requirements.txt, tests/parity_validation/requirements.txtSolo pineadas (is_dev=True)
CreaRack-Pronpmfrontend/package.jsonfrontend/package-lock.json
Workspacenpmpackage.jsonpnpm list --json (CI)

Funciones principales (por módulo)

deps_sources.py: parse_requirements, parse_package_json (parsers de manifiesto), normalize_pypi (PEP 503), load_pip_list, load_package_lock, load_pnpm_list (lectores de “instalada”).

deps_specs.py: version_key/is_prerelease/major_key (comparación de versiones), pypi_satisfies/npm_satisfies (¿esta versión cumple el spec?), has_upper_bound, resolve (mayor versión publicada que satisface un spec).

deps_holds.py: load (lee y valida deps_holds.json), apply (marca las filas retenidas y avisa de los holds huérfanos).

deps_freshness.py: resolve_registry (consulta PyPI/npm y trae la lista de versiones publicadas), resolve_specs (calcula resolved/unbounded/unbounded_major por dep), resolve_vulns (cruza resolved contra OSV.dev, calcula fix_within_spec), fetch_prod_versions (GET al endpoint de PROD), dedupe, lag_counts (resumen de desfase, resta las retenidas de major/minor), collect (orquestador), publish (POST a /api/tools/deps, valida el body — Regla 15).

Ejecución en local

python scripts/deps_freshness.py --dry-run

Resuelve versiones sin necesidad de CI y sin token. Output: tabla en stdout, con la cuenta de «retenidas a propósito» aparte.

Ejecución en CI

Activada por [[entity—ci—workflow—deps-freshness]] (cron semanal lunes 06:30 UTC, push a manifiestos, a deps_holds.json o a alguno de los 4 módulos, o manual).

Tolerancias / Comportamiento robusto

  • Sin --prod-versions-url: cae al runner de CI, luego a lockfile.
  • Sin backend-installed: compara contra la horquilla resuelta (resolved), no contra el mínimo.
  • Sin workspace-installed: cubre solo CreaRack-Pro — no falla.
  • Registro offline: cada dep fallida se marca latest=None → lag_kind="unknown". Publicable.
  • Sin deps_holds.json: 0 retenciones, sin ruido — falla suave (no confundir con un fichero mal escrito, que sí corta la ejecución).

Reglas aplicadas

  • Regla 5 (máx 500 LOC/módulo): motivo del troceo en 4 ficheros.
  • Regla 15 (HTTP 200 ≠ éxito): validación del body en publish().
  • Regla 17 (cloud, no PC): colector sin dependencias externas, ejecutable en GitHub Actions. Local solo para --dry-run.

Véase también

  • [[entity—ci—workflow—deps-freshness]] — el workflow que lo ejecuta y le pasa --prod-versions-url/--prod-token.
  • [[entity—api—endpoint—installed-packages]] — la fuente PROD real de “instalada”.
  • [[feature—harness—osv-security-deps-freshness]] — el eje OSV que ahora cruza contra resolved en vez del mínimo de la horquilla.
  • [[feature—ci—herramienta-frescura-dependencias]] — la feature original (s93) que este colector implementa.
  • [[concept—saas—observability]]
  • [[concept—infra—dependency-management]]