Volver a la wiki

bib_record_cochanges — MCP handler interno para registrar co-cambios git

Descripción

Handler MCP interno (no público, no está en TOOLS) que registra pares de archivos que cambian juntos en D1, enriqueciendo el grafo de la Biblioteca con señal empírica de acoplamiento derivada del historial git.

Firma: handler bib_record_cochanges(args) → JSON

Invocado por:


Parámetros

Entrada (args)

Modo 1 — Ingest post-merge:

{
  "files": ["monitoring/api/signage.py", "static/js/pages/signage/SignageContentManager.js"],
  "repo": "CreaRack-Pro" | "CreaRackSL-workspace"
}

Lista de archivos tocados en el merge. El handler genera todos los pares (archivo_i, archivo_j) y suma +1 al contador en D1.

Modo 2 — Backfill del historial:

{
  "pairs": [
    ["monitoring/api/signage.py", "static/js/pages/signage/SignageContentManager.js", 7],
    ["forms/device_form.py", "templates/device_form.html", 5]
  ],
  "repo": "CreaRack-Pro"
}

Pares pre-agregados como [file_a, file_b, count]. El handler suma += count en D1 para cada par.

Salida

{
  "recorded_pairs": 42,
  "repo": "CreaRack-Pro"
}

O con degradación:

{
  "recorded_pairs": 0,
  "skipped": "menos de 2 archivos",
  "repo": "CreaRackSL-workspace"
}

Lógica interna

1. Normalización de pares

2. Anti-ruido

CriterioLímiteRazón
MAX_FILES25 archivosRefactors masivos generarían N·(N-1)/2 pares basura; umbral conservador.
MAX_PAIRS1000 pares por llamadaProtege subrequest budget en D1; el backfill trocea en múltiples llamadas.
Extensiones code.py, .ts, .tsx, .js, .jsx, .mjs, .cjs, .astro, .vue, .html, .css, .scss, .sqlExcluye .md, CHANGELOG, públicos.
Exclusiones.github/scripts/bib_*, scripts/harness/, public/supercontext/, src/content/wiki/Scripts de infraestructura, generados, wikis (churn ruido).
Denylistconfig/settings/base.pyCambia en CADA release por versionado; haría que co-cambie con todo → invalida la señal.

Ejemplo: merge de 3 archivos .py → genera 3 pares (C(3,2) = 3). Merge de 30 archivos → se rechaza (>MAX_FILES); error devuelto.

3. UPSERT en batch

INSERT INTO bib_cochanges (repo, file_a, file_b, count, last_seen)
VALUES (?, ?, ?, ?, datetime('now'))
ON CONFLICT(repo, file_a, file_b)
DO UPDATE SET count = count + excluded.count, last_seen = datetime('now')

Implementación (functions/api/mcp/handlers/biblioteca.ts)

Case bib_record_cochanges en el switch de handlers:

  1. Parse args: extrae files (array de strings) O pairs (array de triples [a, b, count]).
  2. Modo files:
    • Filtra por extensiones y exclusiones (mismo código que backfill).
    • Verifica 2 ≤ count ≤ MAX_FILES.
    • Genera todos los pares (i, j con i < j).
  3. Modo pairs:
    • Valida estructura de triples.
    • Ordena lexicográficamente.
    • Filtra pares válidos (a !== b, ambos non-empty).
  4. Chequeos globales:
    • Si pares es 0 → devuelve skip.
    • Si pares > MAX_PAIRS → error (troceador debe hacerlo).
  5. Batch UPSERT: prepara statements, ejecuta en un solo db.batch().
  6. Respuesta: {recorded_pairs: N, repo} o error.

Control de acceso (gate readonly)

“Interno” arriba se refiere solo a que no figura en el catálogo público tools/list — pero el dispatcher MCP no bloquea por eso: bloquea por nombre, contra las listas LOGGED_TOOLS / AI_COST_TOOLS / UNLOGGED_WRITE_TOOLS de functions/api/mcp/index.ts. Hasta el 10-09-2026 este handler no figuraba en ninguna de las tres, así que un token marcado readonly (MCP_READONLY_TOKENS) podía invocarlo igual y escribir en bib_cochanges — el nombre del handler no es secreto (está en este mismo documento y en el código).

Lo detectó el verificador en Opus de la regeneración del Atlas de arquitectura (ficha ws2) y se corrigió en el commit b7bfcf74 (#169): bib_record_cochanges entra en UNLOGGED_WRITE_TOOLS, así que ahora un token readonly recibe error al intentar invocarlo. El mismo commit añadió un test de aptitud en test/mcp-dispatcher.test.ts que recorre el catálogo tools/list y exige que toda tool con verbo de escritura en el nombre pase el gate, para que este tipo de hueco no vuelva a pasar desapercibido.


Degración elegante en bib_impact_query

El enriquecimiento que consume estos datos:

const cochangedFiles: ... = [];
if (args.file_path) {
  try {
    const coRows = await db.prepare(`SELECT ... FROM bib_cochanges WHERE ...`).all();
    cochangedFiles = coRows.map(...);
  } catch {
    cochangedFiles = [];  // Si tabla no existe: devolver [], NO error
  }
}
return JSON.stringify({ ..., cochanged_files: cochangedFiles });

Si la migración 0043 no se aplicó aún, el error es atrapado y se devuelve lista vacía. La tool CORE nunca se rompe.


Invocaciones

Desde ingest (.github/scripts/bib_ingest.py)

def record_cochanges(changed_files: list[str], mcp_token: str, repo_name: str) -> dict:
    code_paths = [f for f in changed_files if ...]  # Filtra código
    if len(code_paths) < 2:
        return {"recorded_pairs": 0, "skipped": "..."}
    repo = "CreaRackSL-workspace" if "workspace" in repo_name.lower() else "CreaRack-Pro"
    raw = call_mcp("bib_record_cochanges", {"files": code_paths, "repo": repo}, mcp_token)
    result = json.loads(raw) if raw else {}
    return result

Desde backfill (CreaRack-Pro/scripts/bib_cochange_backfill.py)

# Pseudocódigo
for commit in git.log():
    files = commit.touched_files
    code_files = [f for f in files if ...]
    pairs = [(code_files[i], code_files[j], 1) for i < j in code_files]
    # Agrupa pares idénticos:
    aggregated = defaultdict(int)
    for a, b, c in pairs:
        key = (min(a,b), max(a,b))
        aggregated[key] += c
    # Trocea en lotes de MAX_PAIRS:
    for batch in chunk(aggregated.items(), MAX_PAIRS):
        call_mcp("bib_record_cochanges", {"pairs": batch, "repo": repo})

Datos internos

Fuente de datos:

Índices:

Ciclo de vida:


Error handling

EscenarioRespuesta
files vacío o <2 archivos{recorded_pairs: 0, skipped: "..."}
files >25 archivos{recorded_pairs: 0, skipped: ">25 archivos (ruido)", files: N, repo}
pairs genera >MAX_PAIRS{error: "Demasiados pares..."}
Input inválido (ni files ni pairs){error: "Falta filesopairs"}
BD falla (tabla no existe, conexión perdida){recorded_pairs: 0, error: "..."}

Ninguno de estos errores rompe al caller (ingest o backfill); se registran en logs y se ignora (es ok perder un merge de co-cambios).


Véase también

Subir