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:
.github/scripts/bib_ingest.py·record_cochanges()post-merge (modofiles).CreaRack-Pro/scripts/bib_cochange_backfill.py· backfill del historial (modopairs).
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
- Deduplica archivos idénticos en modo
files. - Ordena lexicográficamente:
file_a < file_b(garantiza una sola fila por pareja en BD). - Ignora autolazos (a !== b).
2. Anti-ruido
| Criterio | Límite | Razón |
|---|---|---|
MAX_FILES | 25 archivos | Refactors masivos generarían N·(N-1)/2 pares basura; umbral conservador. |
MAX_PAIRS | 1000 pares por llamada | Protege 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, .sql | Excluye .md, CHANGELOG, públicos. |
| Exclusiones | .github/scripts/bib_*, scripts/harness/, public/supercontext/, src/content/wiki/ | Scripts de infraestructura, generados, wikis (churn ruido). |
| Denylist | config/settings/base.py | Cambia 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')
- Un solo round-trip a D1 (batch de N statements).
countes aditivo: primer merge suma +1; posteriores suman lo que corresponda.last_seensiempre se actualiza al ahora.
Implementación (functions/api/mcp/handlers/biblioteca.ts)
Case bib_record_cochanges en el switch de handlers:
- Parse args: extrae
files(array de strings) Opairs(array de triples [a, b, count]). - 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).
- Modo pairs:
- Valida estructura de triples.
- Ordena lexicográficamente.
- Filtra pares válidos (a !== b, ambos non-empty).
- Chequeos globales:
- Si pares es 0 → devuelve skip.
- Si pares > MAX_PAIRS → error (troceador debe hacerlo).
- Batch UPSERT: prepara statements, ejecuta en un solo
db.batch(). - 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
- Se llama incondicional después de cada merge (NO depende de si el ingest procesa la PR).
- Idempotente: reintentos son seguros.
- Tolerante: si MCP falla, se registra error pero ingest sigue.
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:
- Tabla
bib_cochanges(D1). - Campos:
repo,file_a,file_b,count,last_seen.
Índices:
idx_bib_cochanges_file_a(búsqueda por file_a).idx_bib_cochanges_file_b(búsqueda por file_b).
Ciclo de vida:
- Backfill one-shot (opcional) popula tabla del histórico.
- Post-merge ingest suma +1 a los pares del merge actual.
- Consulta desde
bib_impact_querycon parámetrocochange_min(default 2).
Error handling
| Escenario | Respuesta |
|---|---|
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
- [[feature—biblioteca—cairn-cochanges]]
- [[entity—functions—tool—bib-impact-query]]
- [[entity—functions—database—bib-cochanges]]
- [[concept—biblioteca—grafo-estatico-vs-empirico]]
- [[entity—functions—tool—call-mcp]]