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. Usapackaging(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 dataclassDep, 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 validadeps_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 enPOST /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 validadeps_holds.json. Fichero ausente →{}(falla suave, 0 retenciones, sin ruido). Fichero mal escrito (falta un campo obligatorio,ecosystem/repofuera de catálogo,sinceno esYYYY-MM-DD, retención duplicada) → excepciónHoldsError, quedeps_freshness.pyconvierte en::error::yexit 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 rellenahold_reason/hold_since/hold_ref/hold_until. Los holds que no casan con ninguna fila publican::warning::(paquete borrado o nombre mal escrito endeps_holds.json).- Efecto en el resumen (
deps_freshness.lag_counts): una fila conhold_reasony desfasemajor/minorno cuenta en esos contadores — se resta aparte como «retenidas a propósito». Elpatch/uptodatesigue 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);untiles opcional (condición o fecha para revisar). - Retenciones vigentes (28-08-2026):
redis(7.4.1, task #204 · redis-py #2807),Django(<6.1pordjango-prometheus),ruffymypy(pin exacto hasta septiembre 2026, con Dani y Txell) — las 4, task #275.
Cómo se resuelve “instalada” (por prioridad)
- PROD real —
--prod-versions-url/--prod-tokencontraGET /api/installed-packages([[entity—api—endpoint—installed-packages]]), el mismo contenedor que sirve producción.installed_source = "prod". - Runner de CI —
pip-list.json/pnpm-list.jsongenerados en el propio job.installed_source = "runner". - 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
| Campo | Qué dice |
|---|---|
manifests | Todos los manifiestos donde aparece el paquete tras deduplicar, separados por coma |
resolved | Mayor versión publicada que satisface pinned |
installed_source | prod | runner | lockfile | None |
unbounded | El spec no pone techo superior (ej. openai>=2.26.0 sin <3) |
unbounded_major | Ademá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_spec | True: 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
| Repo | Ecosistema | Ruta | Instalada desde |
|---|---|---|---|
| CreaRack-Pro | PyPI | requirements.txt | PROD → pip list --format=json (CI) → lockfile |
| CreaRack-Pro | PyPI | terminal/agent/requirements-agent.lock | Lockfile (100% ==) |
| CreaRack-Pro | PyPI | tests/requirements.txt, tests/parity_validation/requirements.txt | Solo pineadas (is_dev=True) |
| CreaRack-Pro | npm | frontend/package.json | frontend/package-lock.json |
| Workspace | npm | package.json | pnpm 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
resolveden 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]]