CreaRack-SL

Cairn cherry-pick #2: Señal de co-cambio git en bib_impact_query

Resumen

Fusiona el grafo estático de la Biblioteca (imports, calls, direct docs) con el historial git: bib_impact_query ahora enriquece sus resultados con un campo cochanged_files que contiene los archivos que históricamente cambian juntos al consultado, capturando acoplamiento empírico cross-capa invisible para las aristas declaradas.

Ejemplo real del backfill: monitoring/api/signage.py ↔ static/js/pages/signage/SignageContentManager.js (backend ↔ frontend) — esta dependencia nunca aparece en imports ni calls, pero el historial git lo revela.


Contexto: ¿Por qué co-cambio?

El grafo estático de la Biblioteca se construye a partir de:

  • Direct docs (archivos enlazados en update_checklists).
  • Imports (el scanner Python/TS detecta from X import Y).
  • Calls (se infieren de relaciones de modelo/función).
  • Edges declarados (definiciones bib_edges en configuración).

Pero no cubre:

  • Acoplamiento implícito: cambios sincronizados en archivos que nunca se importan mutuamente (ej. backend API ↔ componente frontend de la misma feature).
  • Patrones específicos del dominio: archivos que siempre se toca juntos en refactors temáticos.
  • Efectos transversales: cambios en un template HTML que siempre van con una API en Django.

La señal de co-cambio (número de commits donde dos archivos cambiaron juntos) es un indicador empírico de este acoplamiento que el grafo estático nunca vería.


Componentes

1. Tabla D1 bib_cochanges (migración 0043)

Almacena pares de archivos y cuántas veces cambiaron juntos:

CREATE TABLE bib_cochanges (
    id INTEGER PRIMARY KEY,
    repo TEXT NOT NULL,           -- 'CreaRack-Pro' | 'CreaRackSL-workspace'
    file_a TEXT NOT NULL,         -- par ordenado: file_a < file_b
    file_b TEXT NOT NULL,
    count INTEGER NOT NULL,       -- nº de merges donde cambiaron juntos
    last_seen TEXT NOT NULL,      -- timestamp del último cambio conjunto
    UNIQUE (repo, file_a, file_b)
);
  • El par se almacena ordenado (file_a < file_b lexicográficamente) → cada pareja tiene una sola fila.
  • count es aditivo: cada merge que toca ambos archivos suma +1.
  • last_seen se actualiza con UPSERT.
  • Índices en file_a y file_b para búsquedas rápidas.

2. MCP Handler bib_record_cochanges (interno)

Registra co-cambios en D1. No está en la lista pública de tools; se invoca desde:

  • Post-merge ingest (.github/scripts/bib_ingest.py): cada merge envía la lista de archivos tocados.
  • Backfill (CreaRack-Pro/scripts/bib_cochange_backfill.py): lee el historial git completo y pre-agrega pares.

Modos de entrada:

  • files: [path1, path2, ...] — lista de archivos de un commit/merge → genera todos los pares y suma +1 (ingest).
  • pairs: [[file_a, file_b, count], ...] — pares pre-agregados del backfill → suma += count.

Anti-ruido:

  • Solo archivos de código (.py, .ts, .tsx, .js, .jsx, .mjs, .cjs, .astro, .vue, .html, .css, .scss, .sql).
  • Excluye .md, CHANGELOG, scripts harness, docs públicas.
  • Tope MAX_FILES = 25: si un merge toca >25 archivos (refactor masivo), se descarta → evita N·(N-1)/2 pares basura.
  • Tope MAX_PAIRS = 1000: no acepta >1000 pares en una llamada (el backfill los trocea).
  • Denylist: config/settings/base.py (cambia en cada release por versionado).

3. Enriquecimiento de bib_impact_query

Cuando se consulta bib_impact_query(file_path=X), el resultado JSON ahora incluye:

{
  "cochanged_files": [
    { "file": "monitoring/api/signage.py", "count": 7, "last_seen": "2026-06-08T10:22:00Z" },
    { "file": "static/js/pages/signage/SignageContentManager.js", "count": 6, "last_seen": "2026-06-07T15:44:00Z" }
  ]
}
  • Se ordena por count DESC (co-cambios más frecuentes primero), después por last_seen DESC.
  • Filtra por cochange_min (default 2; parámetro nuevo de la tool).
  • Límite: top 15 archivos.
  • Degradación elegante: si la tabla bib_cochanges no existe (migración no aplicada), devuelve [] sin romper la tool CORE.

4. Post-merge ingest (record_cochanges())

Función en .github/scripts/bib_ingest.py que se ejecuta incondicional después de cada merge:

def record_cochanges(changed_files: list[str], mcp_token: str, repo_name: str) -> dict:
    """Registra qué archivos cambian JUNTOS en este merge."""
    # Filtra solo código (mismas reglas que backfill)
    code_paths = [f for f in changed_files if ...]
    if len(code_paths) < 2:
        return {"recorded_pairs": 0, "skipped": "..."}
    # Llama a bib_record_cochanges con modo `files`
    raw = call_mcp("bib_record_cochanges", {"files": code_paths, "repo": repo}, mcp_token)
    return json.loads(raw)
  • Tolerante a fallos: si la llamada MCP falla, se registra el error pero NO rompe el flujo del ingest.
  • Idempotente: si un merge se reintentan varias veces, los UPSERT son seguros.
  • Costo: ~50ms por merge, negligible.

Verificación

✅ tsc --noEmit verde (TS tipado en handlers). ✅ ruff verde en ingest. ✅ Backfill dry-run produce señal limpia: 1561 pares con count ≥ 2, top = acoplamiento código ↔ plantilla/frontend (esperado).


Despliegue

Orden crítico:

  1. Aplicar migración 0043 a D1 PROD: wrangler d1 execute --remote < migrations/0043_create_bib_cochanges.sql.
  2. CF Pages despliega Functions automáticamente (handlers + enriquecimiento en bib_impact_query).
  3. Backfill one-shot desde CreaRack-Pro/scripts/bib_cochange_backfill.py (PR companion, opcional pero recomendado para historial completo).

Si el backfill no se ejecuta, bib_impact_query igualmente funciona: registra co-cambios desde el merge actual en adelante.


Ventajas

  • Impacto sin dudar: al modificar un archivo, devs verán automáticamente qué otros archivos suelen cambiar con él → detección temprana de acoplamiento no documentado.
  • Señal empírica limpia: basada en el historial real de merges, no en reglas estáticas frágiles.
  • Transparente: la degradación elegante garantiza que bib_impact_query jamás se rompe aunque algo falle.
  • Escala: UPSERT en batch respeta el presupuesto de subrequests D1.

Véase también

  • [[entity—functions—tool—bib-impact-query]]
  • [[entity—functions—service—bib-record-cochanges]]
  • [[concept—biblioteca—grafo-estatico-vs-empirico]]
  • [[decision—20260609—cairn-cherry-picks]]
  • [[runbook—biblioteca—backfill-cochanges]]