changebook 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/agregados.js +496 -0
- package/dist/analyze.js +40 -2
- package/dist/friccionDelBrief.js +102 -0
- package/dist/friction.js +1016 -0
- package/dist/git.js +42 -2
- package/dist/guard.js +184 -2
- package/dist/impact.js +172 -17
- package/dist/index.js +31 -1
- package/dist/respuestas.js +111 -0
- package/dist/supabase.js +38 -2
- package/dist/sync.js +36 -5
- package/dist/toolActionPlan.js +98 -0
- package/dist/toolProjectBrief.js +310 -0
- package/dist/toolUsage.js +128 -0
- package/dist/tools.js +332 -332
- package/dist/usage.js +120 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/git.js
CHANGED
|
@@ -75,8 +75,9 @@ export function usableSummary(message) {
|
|
|
75
75
|
* exactamente eso.
|
|
76
76
|
*
|
|
77
77
|
* Un commit que SOLO toca estos ficheros produce un diff vacío y `analyze` lo
|
|
78
|
-
* salta
|
|
79
|
-
*
|
|
78
|
+
* salta, que es la respuesta correcta: regenerar el mapa no es un cambio de
|
|
79
|
+
* producto. Lo que NO era correcto era cómo lo contaba — ver
|
|
80
|
+
* `ficherosDelCommit` aquí abajo y el mensaje en `analyze.ts`.
|
|
80
81
|
*/
|
|
81
82
|
export const FICHEROS_GENERADOS = [
|
|
82
83
|
":(exclude)CLAUDE.md",
|
|
@@ -104,6 +105,45 @@ export async function commitDiff(cwd, hash) {
|
|
|
104
105
|
throw new Error(`Could not read the diff of ${hash.slice(0, 8)}: ${gitErrorMessage(error)}`);
|
|
105
106
|
}
|
|
106
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Los ficheros que toca un commit, SIN excluir los generados.
|
|
110
|
+
*
|
|
111
|
+
* POR QUÉ EXISTE. `commitDiff` excluye `CLAUDE.md`/`AGENTS.md`, así que un
|
|
112
|
+
* commit que solo toca esos devuelve diff vacío — igual que un merge, que no
|
|
113
|
+
* tiene diff propio. Dos situaciones distintas, una sola señal, y `analyze`
|
|
114
|
+
* elegía la equivocada: decía «has no analyzable diff (merge?)» de un commit
|
|
115
|
+
* normal que ni siquiera era un merge (medido el 04/08 con `0bac769`).
|
|
116
|
+
*
|
|
117
|
+
* Es la invariante 17 del repo dentro del producto: la ausencia de diff no
|
|
118
|
+
* prueba la ausencia de cambio mientras el instrumento no sepa distinguir por
|
|
119
|
+
* qué está ausente. Esta función es lo que le da esa segunda pregunta.
|
|
120
|
+
*
|
|
121
|
+
* Devuelve `[]` si el commit no toca ningún fichero — el caso del merge de
|
|
122
|
+
* verdad, y también el de un commit vacío.
|
|
123
|
+
*/
|
|
124
|
+
export async function ficherosDelCommit(cwd, hash) {
|
|
125
|
+
try {
|
|
126
|
+
const { stdout } = await execFileAsync("git",
|
|
127
|
+
// Mismo `--end-of-options` que commitDiff, por el mismo motivo.
|
|
128
|
+
["show", "--pretty=format:", "--name-only", "--end-of-options", hash], { cwd, encoding: "utf8", maxBuffer: GIT_MAX_BUFFER_BYTES });
|
|
129
|
+
return stdout
|
|
130
|
+
.split("\n")
|
|
131
|
+
.map((l) => l.trim())
|
|
132
|
+
.filter(Boolean);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
throw new Error(`Could not read the files of ${hash.slice(0, 8)}: ${gitErrorMessage(error)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* ¿Es un fichero de los que ChangeBook genera? Se decide con las MISMAS rutas
|
|
140
|
+
* que excluye `commitDiff`, derivadas de ella y no reescritas: dos listas que
|
|
141
|
+
* pudieran separarse harían que el mensaje explicara un salto que no ocurrió.
|
|
142
|
+
*/
|
|
143
|
+
export function esFicheroGenerado(ruta) {
|
|
144
|
+
const nombre = ruta.split("/").pop() ?? "";
|
|
145
|
+
return FICHEROS_GENERADOS.some((patron) => patron.replace(/^:\(exclude\)(\*\*\/)?/, "") === nombre);
|
|
146
|
+
}
|
|
107
147
|
/** Pulls git's stderr out of an execFile rejection for a readable message. */
|
|
108
148
|
export function gitErrorMessage(error) {
|
|
109
149
|
if (typeof error === "object" &&
|
package/dist/guard.js
CHANGED
|
@@ -274,6 +274,151 @@ export function dondeApareceElSimbolo(alert, buscarFicheros) {
|
|
|
274
274
|
return null;
|
|
275
275
|
return ficheros;
|
|
276
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* «El modelo nombro un simbolo que este cambio no toca», si el servidor lo
|
|
279
|
+
* comprobo y salio que no.
|
|
280
|
+
*
|
|
281
|
+
* ES LA SEÑAL QUE YA SE CALCULABA Y SE GUARDABA EN UN CAJON.
|
|
282
|
+
* `simboloFueraDelCambio` corre en cada analisis desde el 28/07 en modo SOMBRA:
|
|
283
|
+
* anota en `discarded_warnings` y el aviso se sirve igual. Medido el 08/08
|
|
284
|
+
* cruzandola con una auditoria a mano de los 21 avisos de FacelessOS, marco
|
|
285
|
+
* **15 de los 17 falsos**.
|
|
286
|
+
*
|
|
287
|
+
* POR QUE SE SIRVE EN VEZ DE DESCARTAR. Desde el servidor, un aviso legitimo
|
|
288
|
+
* sobre codigo de fuera del cambio —«alguien todavia llama a buildReport con el
|
|
289
|
+
* formato viejo»— es INDISTINGUIBLE del inventado: los dos afirman una relacion
|
|
290
|
+
* que el modelo no ha podido ver en el diff. Quien puede separarlos es este
|
|
291
|
+
* lado, que tiene el arbol: por eso esta linea viaja pegada a `fichaDelSimbolo`,
|
|
292
|
+
* que dice donde vive el simbolo de verdad. Juntas, el juicio se resuelve sin
|
|
293
|
+
* salir del aviso; por separado, cada una es media pista.
|
|
294
|
+
*
|
|
295
|
+
* SOLO HABLA CUANDO SABE. `true` no se dice —que el simbolo estuviera en el
|
|
296
|
+
* diff es lo normal y no es noticia— y `null`/ausente tampoco: puede ser un
|
|
297
|
+
* backfill, un servidor anterior a la columna o una cache vieja, y presentar
|
|
298
|
+
* «no se comprobo» como «esta comprobado» es la clase de silencio que este
|
|
299
|
+
* fichero entero existe para no cometer.
|
|
300
|
+
*/
|
|
301
|
+
export function fueraDelCambio(alert) {
|
|
302
|
+
if (alert.evidence_in_diff !== false)
|
|
303
|
+
return null;
|
|
304
|
+
const simbolo = (alert.evidence_symbol ?? "").trim();
|
|
305
|
+
// LA FRASE DICE EXACTAMENTE LO QUE SE MIDIO, ni una palabra mas. El primer
|
|
306
|
+
// borrador decia «it is talking about code it did not see in the diff» y era
|
|
307
|
+
// FALSO: `simboloFueraDelCambio` solo mira las lineas AÑADIDAS Y QUITADAS, no
|
|
308
|
+
// las de contexto, asi que el modelo pudo verlo perfectamente. Lo cazo su
|
|
309
|
+
// propio contrato al pasarle un simbolo en linea de contexto. Una linea de
|
|
310
|
+
// comprobacion que afirma de mas es peor que no tenerla: se sirve con
|
|
311
|
+
// prioridad 0 y se lee como hecho verificado.
|
|
312
|
+
return (`${simbolo ? `"${simbolo}"` : "the alert's symbol"} is in no line this change` +
|
|
313
|
+
" added or removed — the claim may still hold, but nothing in this diff" +
|
|
314
|
+
" supports it");
|
|
315
|
+
}
|
|
316
|
+
/** Cuantos ficheros se nombran en la ficha antes de resumir. */
|
|
317
|
+
const MAX_FICHEROS_EN_FICHA = 3;
|
|
318
|
+
/**
|
|
319
|
+
* ¿Es esta linea la DEFINICION del simbolo, y esta exportada?
|
|
320
|
+
*
|
|
321
|
+
* Puro y separado del grep a proposito: la decision se puede probar con lineas
|
|
322
|
+
* escritas a mano, sin repo y sin disco. Conservador — solo reconoce la forma
|
|
323
|
+
* `[export] [default] [async] function|const|let|var|class|type|interface NOMBRE`,
|
|
324
|
+
* que es donde vive el 100% de los casos medidos. Un `module.exports.x = ...` o
|
|
325
|
+
* un `export { x }` a distancia NO se reconocen, y eso hace que la ficha diga
|
|
326
|
+
* "no exportado" de menos, jamas de mas... salvo por `export {`, que se mira
|
|
327
|
+
* aparte justo por eso.
|
|
328
|
+
*/
|
|
329
|
+
export function defineElSimbolo(texto, simbolo) {
|
|
330
|
+
const esc = simbolo.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
331
|
+
const decl = new RegExp(`^\\s*(export\\s+)?(default\\s+)?(async\\s+)?(function|const|let|var|class|type|interface|enum)\\s+${esc}\\b`);
|
|
332
|
+
const m = decl.exec(texto);
|
|
333
|
+
if (m)
|
|
334
|
+
return { define: true, exportado: Boolean(m[1]) };
|
|
335
|
+
// `export { simbolo }` / `export { simbolo as otro }`: exporta sin declarar.
|
|
336
|
+
// Se mira porque su ausencia SI mentiria — diria "no exportado" de un simbolo
|
|
337
|
+
// que lo esta, y esa es la unica direccion en la que esta ficha puede hacer
|
|
338
|
+
// dano (invitar a concluir que no hay llamadores fuera cuando los hay).
|
|
339
|
+
if (new RegExp(`^\\s*export\\s*\\{[^}]*\\b${esc}\\b`).test(texto)) {
|
|
340
|
+
return { define: false, exportado: true };
|
|
341
|
+
}
|
|
342
|
+
return { define: false, exportado: false };
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* La ficha del simbolo de un aviso `present`: donde se define, si se exporta y
|
|
346
|
+
* en cuantos ficheros aparece.
|
|
347
|
+
*
|
|
348
|
+
* ── POR QUE EXISTE, y por que NO descarta ───────────────────────────────────
|
|
349
|
+
*
|
|
350
|
+
* Medido el 2026-08-08 auditando los 19 avisos abiertos de FacelessOS: 15 eran
|
|
351
|
+
* falsos y **8 de esos 15** decian lo mismo — «cualquier llamador que envie N
|
|
352
|
+
* argumentos fallara» — sobre funciones NO EXPORTADAS con UNA sola llamada, en
|
|
353
|
+
* su propio fichero. El aviso pedia comprobar algo que un grep contesta en 30 ms.
|
|
354
|
+
*
|
|
355
|
+
* La primera version de esta idea era un FILTRO que descartaba esos avisos. Se
|
|
356
|
+
* descarto al escribirla: contar apariciones no prueba que los llamadores se
|
|
357
|
+
* actualizaran —eso solo lo prueba el compilador, y el guardian no tiene
|
|
358
|
+
* presupuesto para un `tsc`—, asi que descartar puede tirar un aviso verdadero.
|
|
359
|
+
* Es exactamente la catastrofe por la que el 2026-07-26 se quito la rama
|
|
360
|
+
* 'absent' de `avisoRefutado`.
|
|
361
|
+
*
|
|
362
|
+
* Asi que se aplica a `present` la regla que ya estaba escrita para `absent`:
|
|
363
|
+
* **no descartar, CONTESTAR**. Nombrar el sitio es util en las dos lecturas y
|
|
364
|
+
* falso en ninguna; descartar es catastrofico en una de las dos.
|
|
365
|
+
*
|
|
366
|
+
* Y hay un segundo caso que esto caza solo, tambien medido ese dia: el aviso de
|
|
367
|
+
* `LIMITE_MS` apuntaba a «ai33.ts» y en ese repo hay TRES ficheros llamados
|
|
368
|
+
* ai33.ts con ese mismo simbolo. Decir «definido en 3 ficheros» deja ver el
|
|
369
|
+
* ancla ambigua sin tener que adivinar cual era.
|
|
370
|
+
*
|
|
371
|
+
* Devuelve null cuando no hay nada FIABLE que añadir: otro `expect`, sin
|
|
372
|
+
* simbolo, ambito que no es el repo, el grep fallo, o el simbolo no aparece.
|
|
373
|
+
*/
|
|
374
|
+
export function fichaDelSimbolo(alert, buscarLineas) {
|
|
375
|
+
const simbolo = (alert.evidence_symbol ?? "").trim();
|
|
376
|
+
// El espejo exacto de `dondeApareceElSimbolo`, con el `expect` opuesto: alli
|
|
377
|
+
// se contesta «sigue apareciendo» a quien esperaba que se fuera; aqui se
|
|
378
|
+
// contesta «vive aqui y se le llama desde aqui» a quien avisa de llamadores.
|
|
379
|
+
if (!simbolo || alert.evidence_expect !== "present")
|
|
380
|
+
return null;
|
|
381
|
+
// Mismo gate que alli, y por lo mismo: una afirmacion sobre produccion no se
|
|
382
|
+
// contesta con un grep del repo. Para esas va `dondeComprobarlo`.
|
|
383
|
+
if (alcanceDelRepo(alert) !== "si")
|
|
384
|
+
return null;
|
|
385
|
+
let lineas;
|
|
386
|
+
try {
|
|
387
|
+
lineas = buscarLineas(simbolo);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
if (!lineas || lineas.length === 0)
|
|
393
|
+
return null;
|
|
394
|
+
const ficheros = [...new Set(lineas.map((l) => l.fichero))];
|
|
395
|
+
const definiciones = [
|
|
396
|
+
...new Set(lineas.filter((l) => defineElSimbolo(l.texto, simbolo).define).map((l) => l.fichero)),
|
|
397
|
+
];
|
|
398
|
+
const exportado = lineas.some((l) => defineElSimbolo(l.texto, simbolo).exportado);
|
|
399
|
+
// Sin definicion a la vista no se dice nada: la ficha entera se apoya en
|
|
400
|
+
// saber DONDE vive el simbolo, y «aparece en 4 ficheros» sin saber cual lo
|
|
401
|
+
// declara es ruido que ya sirve `dondeApareceElSimbolo` para los 'absent'.
|
|
402
|
+
if (definiciones.length === 0)
|
|
403
|
+
return null;
|
|
404
|
+
const lista = (xs) => xs.length <= MAX_FICHEROS_EN_FICHA
|
|
405
|
+
? xs.join(", ")
|
|
406
|
+
: `${xs.slice(0, MAX_FICHEROS_EN_FICHA).join(", ")} +${xs.length - MAX_FICHEROS_EN_FICHA}`;
|
|
407
|
+
// EL ANCLA AMBIGUA MANDA. Si el simbolo se declara en varios ficheros, el
|
|
408
|
+
// aviso no identifica uno y eso es lo primero que hay que saber: cualquier
|
|
409
|
+
// otra frase daria por buena una localizacion que no existe.
|
|
410
|
+
if (definiciones.length > 1) {
|
|
411
|
+
return (`"${simbolo}" is declared in ${definiciones.length} files (${lista(definiciones)})` +
|
|
412
|
+
` — the alert names a symbol, not a file, so check which one it means`);
|
|
413
|
+
}
|
|
414
|
+
if (!exportado) {
|
|
415
|
+
return (`"${simbolo}" is declared in ${definiciones[0]} and is NOT exported;` +
|
|
416
|
+
` it appears in ${ficheros.length} file(s) (${lista(ficheros)})` +
|
|
417
|
+
` — every caller is in view there`);
|
|
418
|
+
}
|
|
419
|
+
return (`"${simbolo}" is exported from ${definiciones[0]}` +
|
|
420
|
+
` and appears in ${ficheros.length} file(s) (${lista(ficheros)})`);
|
|
421
|
+
}
|
|
277
422
|
/**
|
|
278
423
|
* Open alerts × staged files → warnings, deduped by (module, message).
|
|
279
424
|
*
|
|
@@ -397,6 +542,43 @@ export function ficherosEnRepo(dir, simbolo) {
|
|
|
397
542
|
return null;
|
|
398
543
|
return out.split("\n").filter(Boolean).slice(0, MAX_FICHEROS_SERVIDOS);
|
|
399
544
|
}
|
|
545
|
+
/**
|
|
546
|
+
* Las LINEAS donde aparece el simbolo, para poder decir si se declara y si se
|
|
547
|
+
* exporta (ver `fichaDelSimbolo`). Tercera pregunta del mismo grep.
|
|
548
|
+
*
|
|
549
|
+
* El formato de `git grep -n` es `fichero:linea:texto`, y el texto puede llevar
|
|
550
|
+
* dos puntos, asi que se parte por los DOS primeros y nada mas. Partir por todos
|
|
551
|
+
* y quedarse con el ultimo trozo se comeria media linea de codigo — y una linea
|
|
552
|
+
* recortada por la mitad puede perder el `export` del principio, que es
|
|
553
|
+
* justamente lo que esto viene a leer.
|
|
554
|
+
*/
|
|
555
|
+
export function lineasEnRepo(dir, simbolo) {
|
|
556
|
+
const out = grepDelRepo(dir, simbolo, "-n");
|
|
557
|
+
if (out === null)
|
|
558
|
+
return null;
|
|
559
|
+
const lineas = [];
|
|
560
|
+
for (const l of out.split("\n")) {
|
|
561
|
+
if (!l)
|
|
562
|
+
continue;
|
|
563
|
+
const p1 = l.indexOf(":");
|
|
564
|
+
if (p1 < 0)
|
|
565
|
+
continue;
|
|
566
|
+
const p2 = l.indexOf(":", p1 + 1);
|
|
567
|
+
if (p2 < 0)
|
|
568
|
+
continue;
|
|
569
|
+
lineas.push({ fichero: l.slice(0, p1), texto: l.slice(p2 + 1) });
|
|
570
|
+
if (lineas.length >= MAX_LINEAS_LEIDAS)
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
return lineas;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Tope de lineas que se leen para hacer la ficha. Un simbolo corriente («id»,
|
|
577
|
+
* «data») puede salir miles de veces y la ficha solo necesita ver las
|
|
578
|
+
* declaraciones; leer sin tope convertiria una comprobacion de 30 ms en el
|
|
579
|
+
* cuello de botella del guardian.
|
|
580
|
+
*/
|
|
581
|
+
const MAX_LINEAS_LEIDAS = 400;
|
|
400
582
|
/** Tope de rutas que se nombran en un aviso: la lista es una pista, no un informe. */
|
|
401
583
|
const MAX_FICHEROS_SERVIDOS = 4;
|
|
402
584
|
/**
|
|
@@ -465,7 +647,7 @@ function grepDelRepo(dir, simbolo, modo) {
|
|
|
465
647
|
* Empieza en 1: una caché SIN `v` es anterior al versionado y se descarta
|
|
466
648
|
* siempre.
|
|
467
649
|
*/
|
|
468
|
-
const GUARD_CACHE_V =
|
|
650
|
+
export const GUARD_CACHE_V = 2;
|
|
469
651
|
/**
|
|
470
652
|
* Los hallazgos de B9 que tocan lo que se va a commitear.
|
|
471
653
|
*
|
|
@@ -568,7 +750,7 @@ async function fetchSignals(db, dir, env) {
|
|
|
568
750
|
// fuera barato. Lo vigila `test/alcanceLlegaAlCliente.test.ts`, que lee
|
|
569
751
|
// ESTA línea: un test de comportamiento no lo caza, porque los stubs
|
|
570
752
|
// inyectan el campo a mano.
|
|
571
|
-
`regression_alerts?select=id,module,plain,created_at,evidence_symbol,evidence_expect,evidence_scope,evidence_line,files&project_id=eq.${projectId}&resolved_at=is.null&order=created_at.desc&limit=${MAX_ALERTS}`),
|
|
753
|
+
`regression_alerts?select=id,module,plain,created_at,evidence_symbol,evidence_expect,evidence_scope,evidence_line,evidence_in_diff,files&project_id=eq.${projectId}&resolved_at=is.null&order=created_at.desc&limit=${MAX_ALERTS}`),
|
|
572
754
|
aliasesFor(db, projectId),
|
|
573
755
|
]);
|
|
574
756
|
alerts = canonicalizeModuleRows(alertasCrudas, aliases);
|
package/dist/impact.js
CHANGED
|
@@ -41,8 +41,9 @@ import { spawn } from "node:child_process";
|
|
|
41
41
|
import * as fs from "node:fs";
|
|
42
42
|
import * as path from "node:path";
|
|
43
43
|
import { presupuestoDeTiempo } from "./context.js";
|
|
44
|
+
import { dirDeTranscripciones, drenarFriccion, pendientesDeShellPath, procesarFriccion, tomarPendientesDeShell, } from "./friction.js";
|
|
44
45
|
import { execFileAsync, gitPath, projectNameFor } from "./git.js";
|
|
45
|
-
import { avisoRefutado, contarEnRepo, dondeApareceElSimbolo, dondeComprobarlo, entregaDe, ficherosEnRepo, hashDelTexto, headShaDe, slugifyProject, unoPorTexto, } from "./guard.js";
|
|
46
|
+
import { avisoRefutado, contarEnRepo, dondeApareceElSimbolo, dondeComprobarlo, fueraDelCambio, fichaDelSimbolo, lineasEnRepo, entregaDe, ficherosEnRepo, hashDelTexto, headShaDe, slugifyProject, unoPorTexto, } from "./guard.js";
|
|
46
47
|
import { aliasesFor, canonicalizeModuleRows, } from "./aliasDeModulo.js";
|
|
47
48
|
import { coChangePairs } from "./sync.js";
|
|
48
49
|
import { computeRecidivism, dependentsOf } from "./tools.js";
|
|
@@ -103,7 +104,11 @@ const SEEN_MAX = 200;
|
|
|
103
104
|
* equivocados; un SeenState viejo, como mucho, repite un aviso o se calla uno
|
|
104
105
|
* dentro de una sesión. No justifica tirar el estado de todo el mundo hoy.
|
|
105
106
|
*/
|
|
106
|
-
|
|
107
|
+
// 1 → 2 EL 24/08: el estado pasó de guardar RUTAS a guardar MÓDULOS. Subirla es
|
|
108
|
+
// lo que hace que el estado viejo se olvide en vez de interpretarse mal —una
|
|
109
|
+
// ruta leída como módulo no casaría nunca y el dedup quedaría desactivado en
|
|
110
|
+
// silencio, que es justo el fallo que este cambio viene a arreglar.
|
|
111
|
+
const SEEN_STATE_V = 2;
|
|
107
112
|
/** Cuántos módulos se describen si el archivo pertenece a varios. */
|
|
108
113
|
const MAX_MODULES = 3;
|
|
109
114
|
/**
|
|
@@ -412,6 +417,14 @@ export function lineasDeImpacto(file, modules) {
|
|
|
412
417
|
` — alert expects it gone: either a reference was missed, or the alert is stale`,
|
|
413
418
|
});
|
|
414
419
|
}
|
|
420
|
+
if (a.fueraDelCambio) {
|
|
421
|
+
lines.push({ prio: 0, text: ` → NOT IN THE DIFF: ${a.fueraDelCambio}` });
|
|
422
|
+
}
|
|
423
|
+
// La misma frase que sirve `atlas_file_context`, a proposito: dos
|
|
424
|
+
// redacciones del mismo hecho son dos hechos para el agente.
|
|
425
|
+
if (a.ficha) {
|
|
426
|
+
lines.push({ prio: 0, text: ` → CHECKED NOW: ${a.ficha}` });
|
|
427
|
+
}
|
|
415
428
|
// El caso opuesto: el repo NO puede contestar esta afirmacion. Se dice, en
|
|
416
429
|
// vez de dejar que el agente (o el refutador) la compruebe donde no era —
|
|
417
430
|
// que es lo que tiraba las dos alertas del desfase de esquema.
|
|
@@ -447,29 +460,29 @@ export function lineasDeImpacto(file, modules) {
|
|
|
447
460
|
return lines;
|
|
448
461
|
}
|
|
449
462
|
/**
|
|
450
|
-
*
|
|
463
|
+
* Módulos que ya se avisaron en ESTA sesión y siguen frescos. Sesión distinta →
|
|
451
464
|
* borrón y cuenta nueva: cada sesión abre con un contexto vacío, así que el
|
|
452
465
|
* aviso vuelve a hacer falta.
|
|
453
466
|
*/
|
|
454
|
-
export function
|
|
467
|
+
export function modulosYaAvisados(state, sessionId, ahora) {
|
|
455
468
|
if (!state || state.session_id !== sessionId)
|
|
456
469
|
return new Set();
|
|
457
470
|
// Otra forma de fichero: no se interpreta, se olvida. Ausente cuenta como la
|
|
458
471
|
// actual (ver SEEN_STATE_V).
|
|
459
|
-
if ((state.v ??
|
|
472
|
+
if ((state.v ?? 1) !== SEEN_STATE_V)
|
|
460
473
|
return new Set();
|
|
461
474
|
const vivas = Object.entries(state.seen ?? {}).filter(([, at]) => typeof at === "number" && ahora - at < SEEN_TTL_MS);
|
|
462
|
-
return new Set(vivas.map(([
|
|
475
|
+
return new Set(vivas.map(([modulo]) => modulo));
|
|
463
476
|
}
|
|
464
477
|
/** El estado siguiente, podado por TTL y por tamaño (más recientes primero). */
|
|
465
478
|
export function estadoSiguiente(state, sessionId, nuevas, ahora) {
|
|
466
479
|
const base = state &&
|
|
467
480
|
state.session_id === sessionId &&
|
|
468
|
-
(state.v ??
|
|
481
|
+
(state.v ?? 1) === SEEN_STATE_V
|
|
469
482
|
? { ...(state.seen ?? {}) }
|
|
470
483
|
: {};
|
|
471
|
-
for (const
|
|
472
|
-
base[
|
|
484
|
+
for (const modulo of nuevas)
|
|
485
|
+
base[modulo] = ahora;
|
|
473
486
|
const podado = Object.entries(base)
|
|
474
487
|
.filter(([, at]) => typeof at === "number" && ahora - at < SEEN_TTL_MS)
|
|
475
488
|
.sort((a, b) => b[1] - a[1])
|
|
@@ -507,6 +520,48 @@ export function modulosDelArchivo(file, rows) {
|
|
|
507
520
|
}
|
|
508
521
|
return [...out.entries()].map(([module, v]) => ({ module, ...v }));
|
|
509
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* El resolutor de módulos para el lector de fricción, sacado de la caché.
|
|
525
|
+
*
|
|
526
|
+
* Existe para que TODOS los caminos que encolan fricción resuelvan el módulo
|
|
527
|
+
* igual. Sin él, `changebook friction` encolaba con `modulo: null` y además
|
|
528
|
+
* avanzaba la marca de agua, así que esos bytes ya no los veía `--warm`: las
|
|
529
|
+
* filas subían sin módulo para siempre y el bloque del brief —que filtra por
|
|
530
|
+
* `module=not.is.null`— se quedaba ciego. Medido en prod el 24/08: 1.505 filas,
|
|
531
|
+
* cero con módulo.
|
|
532
|
+
*
|
|
533
|
+
* Sin caché devuelve `() => null` y se dice: es mejor una fila sin módulo que
|
|
534
|
+
* ninguna, pero quien llama puede avisar.
|
|
535
|
+
*/
|
|
536
|
+
export async function resolutorDeModulos(dir) {
|
|
537
|
+
const file = await cachePath(dir).catch(() => null);
|
|
538
|
+
const cache = file ? leerJson(file) : null;
|
|
539
|
+
if (!cache?.rows?.length)
|
|
540
|
+
return { moduloDe: () => null, hayCache: false };
|
|
541
|
+
// `repoRelative` Y NO la ruta tal cual. EL ATLAS GUARDA RUTAS RELATIVAS A LA
|
|
542
|
+
// RAÍZ DEL REPO (lo dice el comentario de `atlasSignals`, y estaba escrito
|
|
543
|
+
// antes de que yo lo ignorara). La transcripción da absolutas, así que sin
|
|
544
|
+
// relativizar `files.includes(...)` NO CASA NUNCA: medido el 24/08, 0 de 1.505
|
|
545
|
+
// eventos salieron con módulo, en LOS DOS caminos.
|
|
546
|
+
//
|
|
547
|
+
// El toplevel de git y no `dir`: el agente puede estar en un subdirectorio.
|
|
548
|
+
// Mismo `rev-parse` que el hook, y con el mismo fallback: sin repo, `dir`.
|
|
549
|
+
const toplevel = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
|
|
550
|
+
cwd: dir,
|
|
551
|
+
encoding: "utf8",
|
|
552
|
+
})
|
|
553
|
+
.then(({ stdout }) => stdout.trim())
|
|
554
|
+
.catch(() => dir);
|
|
555
|
+
return {
|
|
556
|
+
moduloDe: (ruta) => {
|
|
557
|
+
const rel = repoRelative(toplevel, ruta);
|
|
558
|
+
if (!rel)
|
|
559
|
+
return null; // Fuera del repo: no es de ningún módulo.
|
|
560
|
+
return modulosDelArchivo(rel, cache.rows)[0]?.module ?? null;
|
|
561
|
+
},
|
|
562
|
+
hayCache: true,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
510
565
|
/**
|
|
511
566
|
* ¿Se puede servir esta caché? Fresca Y de esta forma.
|
|
512
567
|
*
|
|
@@ -966,7 +1021,7 @@ async function fetchAtlas(db, dir, env) {
|
|
|
966
1021
|
// hace falta para poder atribuir después "se te avisó de esto" a la fila
|
|
967
1022
|
// que lo dijo. Vienen en la misma consulta porque son gratis aquí y una
|
|
968
1023
|
// caché escrita sin ellos sobreviviría 5 minutos a quien los espere.
|
|
969
|
-
`regression_alerts?select=id,module,plain,resolution,created_at,files,resolved_at,evidence_symbol,evidence_expect,evidence_scope,evidence_line&project_id=eq.${projectId}&order=created_at.desc&limit=${ALERT_WINDOW_ROWS}`)
|
|
1024
|
+
`regression_alerts?select=id,module,plain,resolution,created_at,files,resolved_at,evidence_symbol,evidence_expect,evidence_scope,evidence_line,evidence_in_diff&project_id=eq.${projectId}&order=created_at.desc&limit=${ALERT_WINDOW_ROWS}`)
|
|
970
1025
|
.catch(() => []),
|
|
971
1026
|
aliasesFor(db, projectId),
|
|
972
1027
|
]);
|
|
@@ -1053,6 +1108,31 @@ export async function warmImpactCache(db, dir, env = process.env) {
|
|
|
1053
1108
|
// seguiría avisando igual— y "¿acertó el aviso?" seguiría sin respuesta por
|
|
1054
1109
|
// el canal más frecuente, que es justo lo que esto viene a arreglar.
|
|
1055
1110
|
await drenarCola(db, dir);
|
|
1111
|
+
// La fricción va DESPUÉS del drenado y en su propio try: es la función
|
|
1112
|
+
// nueva, y la función nueva no puede tumbar el canal que ya funciona. Si
|
|
1113
|
+
// esto lanza, `atlas_reads` ya se vació y la caché se calienta igual.
|
|
1114
|
+
try {
|
|
1115
|
+
const cacheParaModulos = leerJson(file);
|
|
1116
|
+
// El MISMO resolutor que `changebook friction`. La versión anterior pasaba
|
|
1117
|
+
// la ruta cruda de la transcripción —absoluta— contra una caché de rutas
|
|
1118
|
+
// relativas, así que no resolvía NINGÚN módulo. Los dos caminos estaban
|
|
1119
|
+
// igual de roto, y unificarlos sin arreglar esto no cambió nada.
|
|
1120
|
+
const { moduloDe } = await resolutorDeModulos(dir);
|
|
1121
|
+
await procesarFriccion(dir, {
|
|
1122
|
+
ahoraMs: Date.now(),
|
|
1123
|
+
dirTranscripciones: dirDeTranscripciones(dir),
|
|
1124
|
+
moduloDe,
|
|
1125
|
+
});
|
|
1126
|
+
// `drenarFriccion` NO lanza: captura el fallo de subida, deja la cola
|
|
1127
|
+
// entera y lo anota para el diagnóstico. Si lanzara, este catch se lo
|
|
1128
|
+
// tragaría y un 403 se leería igual que «no había nada que subir».
|
|
1129
|
+
if (cacheParaModulos?.project_id) {
|
|
1130
|
+
await drenarFriccion(db, dir, cacheParaModulos.project_id);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
catch {
|
|
1134
|
+
// Ver arriba: aislamiento a propósito.
|
|
1135
|
+
}
|
|
1056
1136
|
// Carrera entre dos ediciones seguidas: si otro proceso ya dejó una caché
|
|
1057
1137
|
// fresca mientras este arrancaba, no gastar dos veces la misma consulta.
|
|
1058
1138
|
const cached = leerJson(file);
|
|
@@ -1137,10 +1217,27 @@ marca = { contado: false }) {
|
|
|
1137
1217
|
const ahora = Date.now();
|
|
1138
1218
|
const sessionId = payload.session_id?.trim() || "sin-sesion";
|
|
1139
1219
|
const seenFile = await gitPath(dir, "changebook-impact-seen.json").catch(() => null);
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1220
|
+
const modulosAvisados = modulosYaAvisados(leerJson(seenFile), sessionId, ahora);
|
|
1221
|
+
// TODOS los ficheros del repo siguen adelante: lo que se filtra ahora son los
|
|
1222
|
+
// MÓDULOS, más abajo. Un fichero que pertenece a un módulo del que aún no se
|
|
1223
|
+
// ha avisado tiene algo que decir aunque su ruta ya se haya visto.
|
|
1224
|
+
//
|
|
1225
|
+
// Y con ellos, LAS ESCRITURAS POR SHELL que el lector desacoplado apuntó: un
|
|
1226
|
+
// `sed -i` no pasa por este hook, así que su módulo no se avisaba jamás.
|
|
1227
|
+
// Medido el 24/08: 30 pares (sesión, módulo) tocados sólo por shell, de los
|
|
1228
|
+
// que ~19 valían un aviso —`Publicación` con 3 reincidencias entre ellos—.
|
|
1229
|
+
//
|
|
1230
|
+
// Se unen aquí y no en un hook de `Bash` porque esto NO cuesta latencia: la
|
|
1231
|
+
// lista ya está escrita en disco y sólo se lee. El dedup por módulo evita que
|
|
1232
|
+
// repitan lo que la edición en curso ya diga.
|
|
1233
|
+
const pendientes = [
|
|
1234
|
+
...new Set([
|
|
1235
|
+
...relativas,
|
|
1236
|
+
...tomarPendientesDeShell(await pendientesDeShellPath(dir))
|
|
1237
|
+
.map((r) => repoRelative(toplevel, r))
|
|
1238
|
+
.filter((r) => Boolean(r)),
|
|
1239
|
+
]),
|
|
1240
|
+
];
|
|
1144
1241
|
const signals = await atlasSignals(db, dir);
|
|
1145
1242
|
if (!signals?.project_id)
|
|
1146
1243
|
return callar("sin-atlas");
|
|
@@ -1204,6 +1301,20 @@ marca = { contado: false }) {
|
|
|
1204
1301
|
grepsRestantes -= 1;
|
|
1205
1302
|
return dondeApareceElSimbolo(a, (s) => ficherosEnRepo(toplevel, s));
|
|
1206
1303
|
};
|
|
1304
|
+
/**
|
|
1305
|
+
* La ficha del simbolo de un aviso 'present'. Gasta del MISMO presupuesto de
|
|
1306
|
+
* greps que las otras dos: el guardian corre pre-commit contra un plazo, y una
|
|
1307
|
+
* comprobacion nueva que no lo respetara lo volveria lento justo cuando mas
|
|
1308
|
+
* avisos abiertos hay — que es cuando mas falta hace que conteste.
|
|
1309
|
+
*/
|
|
1310
|
+
const fichar = (a) => {
|
|
1311
|
+
if (a.evidence_expect !== "present")
|
|
1312
|
+
return null;
|
|
1313
|
+
if (grepsRestantes <= 0)
|
|
1314
|
+
return null;
|
|
1315
|
+
grepsRestantes -= 1;
|
|
1316
|
+
return fichaDelSimbolo(a, (s) => lineasEnRepo(toplevel, s));
|
|
1317
|
+
};
|
|
1207
1318
|
const depsRows = signals.deps ?? [];
|
|
1208
1319
|
// Co-cambio: UNA vez por corrida, no por módulo. `coChangePairs` recorre la
|
|
1209
1320
|
// ventana entera y ya trae sus propios umbrales (MIN_PAIR_COUNT=3,
|
|
@@ -1219,6 +1330,12 @@ marca = { contado: false }) {
|
|
|
1219
1330
|
// Lineas con prioridad, no texto ya cocido: el techo se aplica al TOTAL, asi
|
|
1220
1331
|
// que hay que poder recortar despues de saber cuanto ocupa todo junto.
|
|
1221
1332
|
const lineas = [];
|
|
1333
|
+
/** Los MÓDULOS servidos: es lo que se recuerda para no repetir. */
|
|
1334
|
+
const modulosServidos = [];
|
|
1335
|
+
/** Los FICHEROS servidos: es lo que se anota en el libro de entregas.
|
|
1336
|
+
* Dos listas y no una: repurposar la columna `files` para que llevara
|
|
1337
|
+
* módulos habría cambiado el significado de un dato ya publicado sin
|
|
1338
|
+
* renombrarlo — la deriva silenciosa que este repo persigue. */
|
|
1222
1339
|
const avisadas = [];
|
|
1223
1340
|
/** Las alertas que de verdad salieron en el texto. Ver el encolado del final. */
|
|
1224
1341
|
const idsServidos = [];
|
|
@@ -1226,6 +1343,14 @@ marca = { contado: false }) {
|
|
|
1226
1343
|
* separa «no era de nadie» de «era de alguien y aun así no había nada que
|
|
1227
1344
|
* decir», que son los dos silencios que la tasa NO puede mezclar. */
|
|
1228
1345
|
let conModulo = 0;
|
|
1346
|
+
/** ¿Se calló algún módulo por haberse avisado ya en esta sesión?
|
|
1347
|
+
*
|
|
1348
|
+
* Lo necesita el contador de silencios: «ya avisado» y «no había nada que
|
|
1349
|
+
* decir» son motivos DISTINTOS, y mezclarlos deja la tasa sin poder explicar
|
|
1350
|
+
* por qué calló. Al pasar el dedup de fichero a módulo, el filtro dejó de
|
|
1351
|
+
* estar donde se emitía `ya-avisado` — y sin esta bandera los dos motivos se
|
|
1352
|
+
* volvían el mismo. */
|
|
1353
|
+
let algunoYaAvisado = false;
|
|
1229
1354
|
for (const file of pendientes) {
|
|
1230
1355
|
const modulos = modulosDelArchivo(file, signals.rows);
|
|
1231
1356
|
if (modulos.length === 0)
|
|
@@ -1249,20 +1374,39 @@ marca = { contado: false }) {
|
|
|
1249
1374
|
.map((a) => {
|
|
1250
1375
|
const donde = localizar(a);
|
|
1251
1376
|
const fuera = dondeComprobarlo(a);
|
|
1377
|
+
const ficha = fichar(a);
|
|
1378
|
+
// No gasta grep: es una columna que ya viene en la fila.
|
|
1379
|
+
const noVisto = fueraDelCambio(a);
|
|
1252
1380
|
return {
|
|
1253
1381
|
plain: a.plain,
|
|
1254
1382
|
alertIds: a.id ? [a.id] : [],
|
|
1383
|
+
...(noVisto ? { fueraDelCambio: noVisto } : {}),
|
|
1255
1384
|
...(donde
|
|
1256
1385
|
? { donde, simbolo: (a.evidence_symbol ?? "").trim() }
|
|
1257
1386
|
: {}),
|
|
1387
|
+
...(ficha ? { ficha } : {}),
|
|
1258
1388
|
...(fuera ? { fueraDelRepo: fuera } : {}),
|
|
1259
1389
|
...(a.evidence_line
|
|
1260
1390
|
? { linea: String(a.evidence_line).trim() }
|
|
1261
1391
|
: {}),
|
|
1262
1392
|
};
|
|
1263
|
-
}), (x) => x.plain,
|
|
1393
|
+
}), (x) => x.plain,
|
|
1394
|
+
// `ficha` cuenta como comprobacion igual que las otras dos: si dos
|
|
1395
|
+
// avisos comparten texto, sobrevive el que trae algo comprobado. Sin
|
|
1396
|
+
// añadirla aqui, el que la trae podia perder el desempate y la
|
|
1397
|
+
// comprobacion se tiraba junto con su duplicado.
|
|
1398
|
+
(x) => Boolean(x.donde || x.fueraDelRepo || x.ficha || x.fueraDelCambio)), abiertas.filter((a) => (a.module ?? "").trim() === m.module)),
|
|
1264
1399
|
priorRegressions: recidivism.get(m.module) ?? 0,
|
|
1265
1400
|
}))
|
|
1401
|
+
// El dedup, por MÓDULO y antes de `valeLaPena`: si de este módulo ya se
|
|
1402
|
+
// avisó en esta sesión, su texto sería idéntico y no deja hacer nada
|
|
1403
|
+
// distinto. Medido: la mitad de los avisos eran esto.
|
|
1404
|
+
.filter((m) => {
|
|
1405
|
+
if (!modulosAvisados.has(m.module))
|
|
1406
|
+
return true;
|
|
1407
|
+
algunoYaAvisado = true;
|
|
1408
|
+
return false;
|
|
1409
|
+
})
|
|
1266
1410
|
.filter(valeLaPena)
|
|
1267
1411
|
// Por PELIGRO, no por lo más reciente. Solo caben MAX_MODULES y el orden
|
|
1268
1412
|
// decide qué se ve: con el orden por recencia, el módulo cuya nota decía
|
|
@@ -1280,8 +1424,12 @@ marca = { contado: false }) {
|
|
|
1280
1424
|
if (impactos.length === 0)
|
|
1281
1425
|
continue;
|
|
1282
1426
|
lineas.push(...lineasDeImpacto(file, impactos));
|
|
1427
|
+
// Se recuerdan los MÓDULOS servidos, no el fichero. Y sólo los que de verdad
|
|
1428
|
+
// caben (`MAX_MODULES`): marcar como avisado uno que se quedó en el «+N
|
|
1429
|
+
// more» lo silenciaría sin haberlo dicho nunca.
|
|
1283
1430
|
avisadas.push(file);
|
|
1284
1431
|
for (const m of impactos.slice(0, MAX_MODULES)) {
|
|
1432
|
+
modulosServidos.push(m.module);
|
|
1285
1433
|
idsServidos.push(...m.alerts.flatMap((a) => a.alertIds));
|
|
1286
1434
|
}
|
|
1287
1435
|
}
|
|
@@ -1294,13 +1442,20 @@ marca = { contado: false }) {
|
|
|
1294
1442
|
// Solo se marca como avisado lo que de verdad se dijo: si el archivo no tenía
|
|
1295
1443
|
// nada hoy pero mañana sale una alerta suya, el aviso tiene que poder salir.
|
|
1296
1444
|
if (avisadas.length > 0) {
|
|
1297
|
-
escribirJson(seenFile, estadoSiguiente(leerJson(seenFile), sessionId,
|
|
1445
|
+
escribirJson(seenFile, estadoSiguiente(leerJson(seenFile), sessionId, modulosServidos, ahora));
|
|
1298
1446
|
}
|
|
1299
1447
|
// Los dos silencios, separados por `conModulo`. Sin esa distinción la tasa
|
|
1300
1448
|
// sube sola cada vez que alguien edita un README y no dice nada de la
|
|
1301
1449
|
// puntería del hook, que es lo único que se quería medir.
|
|
1302
1450
|
if (lineas.length === 0) {
|
|
1303
|
-
|
|
1451
|
+
if (conModulo === 0)
|
|
1452
|
+
return callar("fuera-de-modulo");
|
|
1453
|
+
// `ya-avisado` ANTES de `sin-lineas`, y el orden es el punto: si el silencio
|
|
1454
|
+
// vino de que sus módulos ya se dijeron, ése es el motivo. Al pasar el dedup
|
|
1455
|
+
// de fichero a módulo, el filtro dejó de estar donde se emitía este motivo, y
|
|
1456
|
+
// sin esta rama los dos silencios se contaban igual — la tasa perdía la
|
|
1457
|
+
// única distinción que sabe explicar POR QUÉ calló.
|
|
1458
|
+
return callar(algunoYaAvisado ? "ya-avisado" : "sin-lineas");
|
|
1304
1459
|
}
|
|
1305
1460
|
// EL TECHO, sobre el total y no por fichero: una edición que toca cuatro
|
|
1306
1461
|
// archivos servía cuatro bloques enteros, y el coste del canal se multiplicaba
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,8 @@ import { hookStatus, installHook, uninstallHook } from "./hook.js";
|
|
|
22
22
|
import { importHistory, planDeImport } from "./import.js";
|
|
23
23
|
import { registerAgents } from "./init.js";
|
|
24
24
|
import { contextHookInstalled, installContextHook, installSettingsHook, printContext, settingsHookInstalled, uninstallContextHook, uninstallSettingsHook, } from "./context.js";
|
|
25
|
-
import { IMPACT_HOOK, informeDeSilencio, printImpact, silenciosAcumulados, warmImpactCache, } from "./impact.js";
|
|
25
|
+
import { IMPACT_HOOK, informeDeSilencio, printImpact, resolutorDeModulos, silenciosAcumulados, tasaDeSilencio, warmImpactCache, } from "./impact.js";
|
|
26
|
+
import { diagnosticoDeFriccion, dirDeTranscripciones, procesarFriccion, rankingDeFriccion, } from "./friction.js";
|
|
26
27
|
import { login } from "./login.js";
|
|
27
28
|
import { confirmar, hayTerminal, preguntar } from "./pregunta.js";
|
|
28
29
|
import { argumentosDeScan, contarCommits, correrScan, informeParaInit, informeDeRepo, } from "./scan.js";
|
|
@@ -66,6 +67,9 @@ Usage:
|
|
|
66
67
|
blocks an edit; silent when there is nothing to say
|
|
67
68
|
changebook silence [dir] How often that hook stays quiet, with both raw
|
|
68
69
|
numbers. Healthy is 85-95%: below 80% it is noise
|
|
70
|
+
changebook friction [dir] Where the agent's work gets redone in this repo,
|
|
71
|
+
read from the local Claude Code transcripts. Says
|
|
72
|
+
MUERTO if the repo has edits and it read nothing
|
|
69
73
|
changebook scan [dir] [--json|--card|--badge]
|
|
70
74
|
Coupling report for any repo, from its git history
|
|
71
75
|
alone. No account, no network, writes nothing — run it
|
|
@@ -333,6 +337,32 @@ async function main() {
|
|
|
333
337
|
await printImpact(new Supabase());
|
|
334
338
|
return;
|
|
335
339
|
}
|
|
340
|
+
case "friction": {
|
|
341
|
+
// Sin credenciales y sin red, como `silence`: la fricción se calcula en
|
|
342
|
+
// local y esta orden sólo la enseña. Lo que sube es cosa del `--warm`.
|
|
343
|
+
const dir = process.argv[3] ?? process.cwd();
|
|
344
|
+
// El MISMO resolutor que usa `--warm`. Esta orden también ENCOLA (es un
|
|
345
|
+
// efecto de `procesarFriccion`) y además avanza la marca de agua, así que
|
|
346
|
+
// si resolviera distinto envenenaría lo que se sube: pasó: 1.505 filas en
|
|
347
|
+
// prod sin módulo, y el bloque del brief filtra por módulo no nulo.
|
|
348
|
+
const { moduloDe, hayCache } = await resolutorDeModulos(dir);
|
|
349
|
+
const marca = await procesarFriccion(dir, {
|
|
350
|
+
ahoraMs: Date.now(),
|
|
351
|
+
dirTranscripciones: dirDeTranscripciones(dir),
|
|
352
|
+
moduloDe,
|
|
353
|
+
});
|
|
354
|
+
if (!hayCache) {
|
|
355
|
+
console.log("⚠ Sin caché del atlas: los eventos se encolan SIN módulo. Edita algo y vuelve a probar.");
|
|
356
|
+
}
|
|
357
|
+
// El denominador del canal de vitalidad: cuántas ediciones vio el hook.
|
|
358
|
+
// Si hubo ediciones y el lector leyó cero bytes, está muerto — y eso es
|
|
359
|
+
// lo único que un repo tranquilo no puede parecer.
|
|
360
|
+
const { total: edicionesVistas } = tasaDeSilencio(await silenciosAcumulados(dir));
|
|
361
|
+
console.log(diagnosticoDeFriccion(marca, edicionesVistas));
|
|
362
|
+
console.log("");
|
|
363
|
+
console.log(rankingDeFriccion(dir));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
336
366
|
case "silence": {
|
|
337
367
|
// Sin credenciales y sin red: el contador es local por diseño (ver
|
|
338
368
|
// tallyPath). Se lee del repo, no del servidor, y por eso contesta el
|