@ingeniomaps/cauce 0.72.0 → 0.74.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/CHANGELOG.md +62 -0
- package/engine/cli/catalog.js +23 -0
- package/engine/core/evidence.js +6 -2
- package/engine/hooks/shell.js +92 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,68 @@ desde este repositorio no va, porque el que lee no puede actuar sobre eso. Cuand
|
|
|
14
14
|
unas pocas líneas casi siempre es porque cuenta cómo se descubrió el problema o por qué se eligió el
|
|
15
15
|
diseño — eso vive en el commit y en el código.
|
|
16
16
|
|
|
17
|
+
## [0.74.0] - 2026-09-10
|
|
18
|
+
|
|
19
|
+
### Corregido
|
|
20
|
+
|
|
21
|
+
- **Un gate ya no pisa lo que vos construiste.** Al medir el índice, lo ignorado se enlazaba al árbol
|
|
22
|
+
real: el gate corría sobre lo staged y te dejaba la salida de build con **esa** versión, mientras tu
|
|
23
|
+
fuente en disco tenía otra y nada lo decía. Si corrías la app después de commitear, corrías algo que
|
|
24
|
+
no era lo que estabas mirando. Ahora lo que un gate puede fabricar —`dist`, `build`, `out`,
|
|
25
|
+
`coverage`, `.next`, `.nuxt`, `.svelte-kit`, `.turbo`, `.output`, `.parcel-cache`, `__pycache__`,
|
|
26
|
+
`.pytest_cache`— se construye adentro de la copia y se descarta con ella. Lo que no puede fabricar
|
|
27
|
+
—`node_modules`, un `.env`— se le sigue enlazando.
|
|
28
|
+
|
|
29
|
+
Arregla también algo del propio gate: construía sobre restos de tu corrida anterior, así que su
|
|
30
|
+
veredicto dependía de un estado que nadie declaró.
|
|
31
|
+
|
|
32
|
+
**Lo que te pide algo**: el build del gate deja de ser incremental, así que ese commit tarda más. Y si
|
|
33
|
+
tu proyecto genera en un directorio ignorado que no está en esa lista, seguí reportándolo: el nombre
|
|
34
|
+
se agrega.
|
|
35
|
+
|
|
36
|
+
- **Los gates corren sobre una copia que se declara no interactiva, y un gate que falla dice qué dijo.**
|
|
37
|
+
`verify` mide el índice en un temporal y enlaza ahí lo ignorado, `node_modules` incluido, apuntando al
|
|
38
|
+
original. En un proyecto pnpm eso no corre: el gestor ve que el árbol enlazado no fue instalado ahí y
|
|
39
|
+
su reacción es reinstalar, que empieza borrando el `node_modules` **del proyecto**. Lo único que lo
|
|
40
|
+
detenía es que el hijo no ve una terminal. Ahora la copia lleva `CI=true`, que es la variable que el
|
|
41
|
+
propio pnpm nombra — y sólo la copia: si árbol e índice coinciden, los gates corren en tu directorio y
|
|
42
|
+
ahí no se te cambia nada.
|
|
43
|
+
|
|
44
|
+
Y el bloqueo dejaba de decir por qué: `test (exit 1)` era el mismo texto para una suite en rojo y para
|
|
45
|
+
un gestor que se negó a arrancar el script, así que empujaba a aprobar el commit como «rojo conocido»
|
|
46
|
+
sin que nada se hubiera medido. Ahora llega con la duración y con la primera línea de error de la
|
|
47
|
+
herramienta, y cuando todos los gates fallan por debajo de dos segundos lo dice — sin afirmar que no
|
|
48
|
+
corrieron, que es algo que no se puede saber desde acá.
|
|
49
|
+
|
|
50
|
+
**Lo que te pide algo**: si tenías un gate que se comportaba distinto bajo `CI`, ahora lo va a hacer
|
|
51
|
+
al commitear con algo sin stagear o sin trackear.
|
|
52
|
+
|
|
53
|
+
## [0.73.0] - 2026-09-09
|
|
54
|
+
|
|
55
|
+
### Corregido
|
|
56
|
+
|
|
57
|
+
- **Los guards dejan de frenar trabajo legítimo escrito en varias líneas.** Siete reglas acotaban
|
|
58
|
+
«dentro de este comando» con `[^;&|]`, y el salto de línea —que también separa comandos— no estaba en
|
|
59
|
+
esa lista: cualquier bandera de una línea posterior se leía como parte del comando de arriba. `git
|
|
60
|
+
commit -m "x"` seguido de `ls -a` se bloqueaba como si stageara al commitear, y `git push origin main`
|
|
61
|
+
seguido de `rm -f /tmp/x` como un force push, que además nombraba una violación que no estaba.
|
|
62
|
+
|
|
63
|
+
Lo que frena sigue frenando: `commit -am`, `add -A`, `push --force`, `--amend` e `install -g` en una
|
|
64
|
+
línea siguen bloqueados, y un `git push` seguido de otra cosa sigue pidiendo la acción humana que R10
|
|
65
|
+
exige — sólo que ya no lo llama reescritura de historia.
|
|
66
|
+
|
|
67
|
+
- **Un banco de evaluación que no se puede rehacer lo dice, en vez de seguir sobre uno que no es nuevo.**
|
|
68
|
+
El borrado ahora se comprueba, y si algo sobrevive la corrida corta nombrando qué. Hasta acá el mismo
|
|
69
|
+
hecho se venía rodeando por síntoma —reintentos en el borrado, `force` en el andamiaje, un `rm` antes
|
|
70
|
+
del enlace—, y cada rodeo dejaba la medición siguiente corriendo sobre restos de la anterior.
|
|
71
|
+
|
|
72
|
+
La prueba que lo destapaba, además, **descartaba el resultado de la corrida que fallaba**: cualquier
|
|
73
|
+
causa terminaba en el mismo `true !== false` con el `stderr` tirado, y por eso tres investigaciones
|
|
74
|
+
dejaron escrito «no está establecido por qué». Ahora la falla llega con su código y su ruta.
|
|
75
|
+
|
|
76
|
+
**Lo que te pide algo**: si rehacer un banco te corta con «no se pudo borrar entero», borralo a mano y
|
|
77
|
+
volvé a correr. Antes eso seguía en silencio y fallaba más adelante, en otro lado.
|
|
78
|
+
|
|
17
79
|
## [0.72.0] - 2026-09-09
|
|
18
80
|
|
|
19
81
|
### Corregido
|
package/engine/cli/catalog.js
CHANGED
|
@@ -102,6 +102,29 @@ function evaluationBench(root, agent, caso, force, kind) {
|
|
|
102
102
|
// reintentos, rehacer un banco es una operación que falla de vez en cuando y deja la corrida sin
|
|
103
103
|
// empezar.
|
|
104
104
|
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 })
|
|
105
|
+
// Y se comprueba que haya borrado. `rmSync` puede volver sin lanzar y dejar cosas —pasó en CI y no
|
|
106
|
+
// está establecido por qué—, y hasta acá cada síntoma se rodeaba por separado: `force` en el
|
|
107
|
+
// andamiaje, un `rm` antes del enlace. Rodearlo deja la corrida siguiendo sobre un banco que no es
|
|
108
|
+
// nuevo, y lo que falla después no dice nada del borrado: el test que lo destapó reportaba
|
|
109
|
+
// `true !== false` sobre un archivo de la corrida anterior, sin nombrar de dónde salía.
|
|
110
|
+
//
|
|
111
|
+
// Falla en vez de seguir, porque un banco a medio borrar contamina la medición que viene, que es lo
|
|
112
|
+
// que la recreación existe para evitar. Y nombra lo que sobrevivió: es lo único que va a permitir
|
|
113
|
+
// establecer la causa la próxima vez que ocurra.
|
|
114
|
+
if (fs.existsSync(dir)) {
|
|
115
|
+
const sobreviven = []
|
|
116
|
+
const recorrer = (base, relative = '') => {
|
|
117
|
+
for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
|
|
118
|
+
if (sobreviven.length >= 5) return
|
|
119
|
+
const next = relative ? `${relative}/${entry.name}` : entry.name
|
|
120
|
+
if (entry.isDirectory()) recorrer(path.join(base, entry.name), next)
|
|
121
|
+
else sobreviven.push(next)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
try { recorrer(dir) } catch { /* el listado es la explicación, no la comprobación */ }
|
|
125
|
+
fail(`${dir} no se pudo borrar entero y el banco tiene que ser nuevo. Sobrevivieron al borrado: `
|
|
126
|
+
+ `${sobreviven.join(', ') || '(sólo directorios)'}. Borralo a mano y volvé a correr.`, 2)
|
|
127
|
+
}
|
|
105
128
|
// Con `force`: el banco es desechable y se acaba de borrar, así que lo que sobreviva al `rmSync` se
|
|
106
129
|
// pisa en vez de cortar la corrida. Sin esto, `copyTemplate` se niega ante cualquier archivo que
|
|
107
130
|
// quede —«El destino contiene …/AGENTS.md»— y el mismo test falló así tres veces en un día, en las
|
package/engine/core/evidence.js
CHANGED
|
@@ -31,12 +31,16 @@ function logPath(root) {
|
|
|
31
31
|
|
|
32
32
|
// Una línea por gate corrido. Nunca lanza: es un efecto de borde de un guard, y un registro que no se
|
|
33
33
|
// puede escribir no puede impedir el commit que estaba juzgando.
|
|
34
|
-
|
|
34
|
+
// `ms` es cuánto tardó el gate, y se guarda porque es lo que separa una suite que falló de una que
|
|
35
|
+
// nunca arrancó. Antes había que restar los `at` de dos líneas seguidas para estimarlo, y esa resta
|
|
36
|
+
// incluye lo que pasó entre gate y gate; el número propio no. Fue lo que costó diagnosticar el caso 068:
|
|
37
|
+
// tres gates «en rojo» a un segundo uno de otro, cuando la corrida real de ese proyecto tarda trece.
|
|
38
|
+
function record(root, gate, status, ms) {
|
|
35
39
|
if (!root) return
|
|
36
40
|
try {
|
|
37
41
|
const file = logPath(root)
|
|
38
42
|
const previous = fs.existsSync(file) ? fs.readFileSync(file, 'utf8').split('\n').filter(Boolean) : []
|
|
39
|
-
const entry = JSON.stringify({ at: new Date().toISOString(), gate, status })
|
|
43
|
+
const entry = JSON.stringify({ at: new Date().toISOString(), gate, status, ...(ms >= 0 ? { ms } : {}) })
|
|
40
44
|
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
41
45
|
fs.writeFileSync(file, `${[...previous, entry].slice(-MAX_RUNS).join('\n')}\n`)
|
|
42
46
|
} catch { /* el registro es evidencia, no una puerta */ }
|
package/engine/hooks/shell.js
CHANGED
|
@@ -31,6 +31,13 @@ const EV = require('../core/evidence')
|
|
|
31
31
|
const ANTES = String.raw`(?:^|[\s;&|('"\`])`
|
|
32
32
|
const PALABRA = String.raw`$|[\s;&|)'"\`]`
|
|
33
33
|
const COMANDO = String.raw`$|[;&|)'"\`]`
|
|
34
|
+
// Lo que puede haber **entre** un comando y su bandera sin salir de ese comando. El salto de línea va
|
|
35
|
+
// adentro de lo excluido porque también separa comandos, exactamente igual que `;`, `&` y `|`: sin él,
|
|
36
|
+
// una bandera escrita en una línea posterior se leía como parte del primer comando. Es la causa de los
|
|
37
|
+
// falsos positivos del caso 067, en siete reglas a la vez — `git push origin main` seguido de
|
|
38
|
+
// `rm -f /tmp/x` se bloqueaba anunciando «`git push --force` reescribe historia publicada», que además
|
|
39
|
+
// de frenar trabajo legítimo nombraba una violación que no estaba.
|
|
40
|
+
const MISMO = String.raw`[^;&|\n]`
|
|
34
41
|
|
|
35
42
|
// Un mensaje de commit es dato, no código. `git commit -m "fix: bloquear git push --force"` disparaba
|
|
36
43
|
// el guard de publicación, y lo mismo `rm -rf /` nombrado en una explicación; con el heredoc que se usa
|
|
@@ -62,7 +69,8 @@ function destructive(input) {
|
|
|
62
69
|
// matchea igual las dos formas, así que `allowPush` habilitaba el force-push sin que nadie lo decidiera
|
|
63
70
|
// y el párrafo de autonomía de `AGENTS.md` tenía que confesarlo. R8 prohíbe `force` sin excepción
|
|
64
71
|
// configurable, así que esta rama va antes del permiso y no lo consulta.
|
|
65
|
-
if (
|
|
72
|
+
if (new RegExp(String.raw`\bgit\s+push\b${MISMO}*\s(?:-f|--force(?:-with-lease|-if-includes)?)\b`)
|
|
73
|
+
.test(command)) {
|
|
66
74
|
block("'git push --force' reescribe historia ya publicada. R8 lo prohíbe y runner.allowPush no lo "
|
|
67
75
|
+ 'habilita: publicá con un push normal, o registrá una acción humana.')
|
|
68
76
|
}
|
|
@@ -75,7 +83,7 @@ function destructive(input) {
|
|
|
75
83
|
// no devolvía una línea. Se bloquea por política y no por daño —un `--amend` sobre algo que nadie vio
|
|
76
84
|
// no rompe nada—, así que el mensaje manda a lo que sí corresponde: otro commit.
|
|
77
85
|
[
|
|
78
|
-
|
|
86
|
+
new RegExp(String.raw`\bgit\s+commit\b${MISMO}*\s--amend\b`),
|
|
79
87
|
"'git commit --amend' reescribe un commit ya creado. R8 pide uno nuevo en su lugar.",
|
|
80
88
|
],
|
|
81
89
|
[/\bgit\s+clean\s+-[^\s]*f/, "'git clean -f' borra archivos sin seguimiento."],
|
|
@@ -89,7 +97,7 @@ function destructive(input) {
|
|
|
89
97
|
[
|
|
90
98
|
// `git restore .` no lleva `--` y destruye igual: comprobado en `git restore --help` (git 2.43.0),
|
|
91
99
|
// que restaura el working tree por defecto y toma el pathspec sin separador.
|
|
92
|
-
new RegExp(String.raw`\bgit\s+(?:checkout|restore)\s+(
|
|
100
|
+
new RegExp(String.raw`\bgit\s+(?:checkout|restore)\s+(?:${MISMO}*?\s)?`
|
|
93
101
|
+ String.raw`(?:--(?=\s*(?:${COMANDO}))|(?:--\s+)?(?:\.|\*|:\/)(?=\s*(?:${COMANDO})))`),
|
|
94
102
|
"'git checkout -- .' revierte todo lo no commiteado del directorio, no sólo lo que estás mirando. "
|
|
95
103
|
+ 'Nombrá el archivo, o commiteá lo que quieras conservar antes.',
|
|
@@ -104,7 +112,7 @@ function destructive(input) {
|
|
|
104
112
|
],
|
|
105
113
|
[
|
|
106
114
|
new RegExp(ANTES + String.raw`(?:mkfs\S*|shred)\s`
|
|
107
|
-
+ String.raw`|\bdd\s
|
|
115
|
+
+ String.raw`|\bdd\s+${MISMO}*\bof=\/dev\/|>\s*\/dev\/(?:sd|nvme|disk)`),
|
|
108
116
|
'Operación destructiva sobre disco o dispositivo.',
|
|
109
117
|
],
|
|
110
118
|
[
|
|
@@ -124,14 +132,14 @@ function gitAdd(input) {
|
|
|
124
132
|
// Dónde termina la palabra lo decide PALABRA y no un espacio: `bash -c "git add -A"` y
|
|
125
133
|
// `eval 'git add -A'` pasaban porque después de la bandera venía una comilla. Es el hueco que 028
|
|
126
134
|
// cerró en las reglas de `destructive`, y esta regla se quedó afuera de aquel arreglo.
|
|
127
|
-
if (new RegExp(String.raw`\bgit\s+add\s+(
|
|
135
|
+
if (new RegExp(String.raw`\bgit\s+add\s+(?:${MISMO}*\s)?(?:-A|--all|\.)(?=${PALABRA})`)
|
|
128
136
|
.test(command)) {
|
|
129
137
|
block("'git add -A/--all/.' está prohibido. Stagea rutas explícitas.")
|
|
130
138
|
}
|
|
131
139
|
// La misma regla con otra ortografía: `-a` stagea todo lo seguido sin nombrar una ruta, y encima lo
|
|
132
140
|
// hace al commitear —después de este hook—, así que los guards que leen el índice tampoco lo ven.
|
|
133
141
|
// `--amend` queda afuera: empieza con dos guiones y lo frena `destructive`, por otra razón.
|
|
134
|
-
if (
|
|
142
|
+
if (new RegExp(String.raw`\bgit\s+commit\b${MISMO}*\s(?:-[a-z]*a[a-z]*|--all)\b`).test(command)) {
|
|
135
143
|
block("'git commit -a' stagea al commitear, después de este guard: nadie llega a revisar el diff "
|
|
136
144
|
+ 'staged, ni vos ni los guards que lo miran. Stageá las rutas por nombre y commiteá aparte.')
|
|
137
145
|
}
|
|
@@ -141,7 +149,8 @@ function dependencies(input) {
|
|
|
141
149
|
if (process.env.OPS_DEPENDENCIES_OVERRIDE === '1') return
|
|
142
150
|
const command = commandOf(input)
|
|
143
151
|
const unsafePackageCommand = /\b(?:npm|pnpm|yarn|bun)\s+publish\b/.test(command)
|
|
144
|
-
||
|
|
152
|
+
|| new RegExp(String.raw`\b(?:npm|pnpm|yarn)\s+(?:install|add)\b${MISMO}*(?:\s-g\b|\s--global\b)`)
|
|
153
|
+
.test(command)
|
|
145
154
|
if (unsafePackageCommand) {
|
|
146
155
|
block('Publicar paquetes o instalar dependencias globales requiere una acción humana explícita.')
|
|
147
156
|
}
|
|
@@ -346,14 +355,22 @@ function governance(input) {
|
|
|
346
355
|
function run(program, args, cwd, extra = {}) {
|
|
347
356
|
const env = { ...process.env, ...extra }
|
|
348
357
|
delete env.NODE_TEST_CONTEXT
|
|
358
|
+
const started = Date.now()
|
|
349
359
|
const result = spawnSync(program, args, { cwd, encoding: 'utf8', stdio: 'pipe', env })
|
|
350
360
|
return {
|
|
351
361
|
ok: result.status === 0,
|
|
352
362
|
status: result.status,
|
|
363
|
+
ms: Date.now() - started,
|
|
353
364
|
output: `${result.stdout || ''}${result.stderr || ''}`.trim(),
|
|
354
365
|
}
|
|
355
366
|
}
|
|
356
367
|
|
|
368
|
+
// Salidas de build y cachés que cualquier gate rehace solo. Se comparan contra el nombre entero de la
|
|
369
|
+
// entrada para que valga también anidado —`packages/app/dist`—, y con el separador de `git status`, que
|
|
370
|
+
// siempre usa `/`.
|
|
371
|
+
const RECREABLE = new RegExp('(^|/)(?:dist|build|out|coverage|__pycache__'
|
|
372
|
+
+ '|\\.next|\\.nuxt|\\.svelte-kit|\\.turbo|\\.output|\\.parcel-cache|\\.pytest_cache)$')
|
|
373
|
+
|
|
357
374
|
// Dónde tiene que correr un gate: sobre lo que el commit va a grabar, que es el índice y no el árbol.
|
|
358
375
|
// El árbol se le parece casi siempre y por eso el error no se veía — puede tener encima otra versión de
|
|
359
376
|
// un archivo staged, y puede tener uno sin trackear que el commit no lleva, que es el olvido de
|
|
@@ -390,6 +407,19 @@ function commitTree(dir) {
|
|
|
390
407
|
for (const line of lines) {
|
|
391
408
|
if (!line.startsWith('!! ')) continue
|
|
392
409
|
const name = line.slice(3).trim().replace(/\/$/, '')
|
|
410
|
+
// Lo que el gate puede fabricar no se le enlaza: lo construye adentro de la copia y se descarta con
|
|
411
|
+
// ella. Enlazarlo hacía dos daños a la vez. Uno es del usuario: el gate corre sobre el índice, así
|
|
412
|
+
// que le dejaba la salida de build con la versión **staged** mientras su fuente en disco tenía otra,
|
|
413
|
+
// y nada lo decía —medido con un `dist/` que pasó de «lo-que-estoy-editando» a «staged» (caso 069)—.
|
|
414
|
+
// El otro es del propio gate: construía sobre restos de la corrida anterior del usuario, así que su
|
|
415
|
+
// veredicto dependía de un estado que nadie declaró.
|
|
416
|
+
//
|
|
417
|
+
// La lista envejece y eso pesa menos de lo que parece, porque sólo se aplica a rutas que git ya
|
|
418
|
+
// marcó como ignoradas: un `dist/` ignorado es generado por definición. Errarle por defecto —que
|
|
419
|
+
// falte un nombre— deja el comportamiento de antes; errarle por exceso hace que un gate reconstruya,
|
|
420
|
+
// que es más lento y no incorrecto. Lo que **sí** se enlaza es lo que un gate no puede fabricar:
|
|
421
|
+
// `node_modules`, un `.env`, las credenciales de una herramienta.
|
|
422
|
+
if (RECREABLE.test(name)) continue
|
|
393
423
|
const link = path.join(temp, name)
|
|
394
424
|
if (fs.existsSync(link)) continue
|
|
395
425
|
fs.mkdirSync(path.dirname(link), { recursive: true })
|
|
@@ -412,7 +442,17 @@ function commitTree(dir) {
|
|
|
412
442
|
// quien commitea; un proyecto con un gate así tiene que sacar esa escritura del gate.
|
|
413
443
|
const started = run('git', ['init', '--quiet'], temp)
|
|
414
444
|
if (started.ok) run('git', ['add', '--all'], temp)
|
|
415
|
-
|
|
445
|
+
// Un gate no sólo lee su entorno: escribe en él. Lo ignorado se enlaza al original —eso es a
|
|
446
|
+
// propósito y está arriba—, así que lo que el gate escriba cae en el árbol de quien commitea. Un
|
|
447
|
+
// gestor de paquetes que se sincroniza antes de correr un script lo lleva al extremo: pnpm 11 ve que
|
|
448
|
+
// el árbol enlazado no fue instalado acá y su reacción es reinstalar, que empieza borrando el
|
|
449
|
+
// `node_modules` **del proyecto**. Lo único que hoy lo detiene es que `run` lanza con `stdio: 'pipe'`
|
|
450
|
+
// y el hijo no ve una terminal (caso 068).
|
|
451
|
+
//
|
|
452
|
+
// `CI` es la variable que el propio pnpm nombra para no preguntar, y la que cualquier gate razonable
|
|
453
|
+
// ya espera. Va sólo acá: por el `return` de arriba los gates corren en el directorio del usuario, y
|
|
454
|
+
// ahí cambiarle el entorno no tiene ninguna razón.
|
|
455
|
+
return { root: temp, temp, env: { CI: 'true' } }
|
|
416
456
|
}
|
|
417
457
|
|
|
418
458
|
function verify(input) {
|
|
@@ -453,6 +493,41 @@ function verify(input) {
|
|
|
453
493
|
// Cada gate deja su rastro en `ops`; para qué sirve ese registro lo dice `core/evidence.js`. Lo que se
|
|
454
494
|
// decide acá es que el rojo se anota igual que el verde: un gate que falló y se commiteó con
|
|
455
495
|
// aprobación es exactamente lo que alguien va a querer ver después.
|
|
496
|
+
// Lo que se sabe de un gate que falló, en la forma en que se va a leer. El mensaje decía sólo
|
|
497
|
+
// `test (exit 1)` y tiraba la salida de la herramienta: cualquier causa —una suite en rojo, un gestor
|
|
498
|
+
// que se negó a arrancar el script, un binario que no está— llegaba con el mismo texto. Es la misma
|
|
499
|
+
// forma de fallar que el caso 066 encontró en una prueba, acá en el mensaje que lee una persona.
|
|
500
|
+
//
|
|
501
|
+
// Se muestra **una** línea y acotada: la salida de un gate puede traer cualquier cosa del entorno, y lo
|
|
502
|
+
// que hace falta para diagnosticar es la primera línea de error, no el volcado.
|
|
503
|
+
const ERROR_LINE = /error|err[_!]|fail|abort|not found|cannot|no such/i
|
|
504
|
+
const MAX_LINE = 160
|
|
505
|
+
function fallo(gate, result) {
|
|
506
|
+
// La línea que empieza con `>` es el eco del script que npm y pnpm imprimen antes de correrlo, así
|
|
507
|
+
// que lleva el comando entero y no dice nada de qué falló. Descartarla es lo que hace que la primera
|
|
508
|
+
// coincidencia sea el error y no el comando — con el eco adentro, un script que **menciona** una
|
|
509
|
+
// palabra de error gana siempre.
|
|
510
|
+
const lines = (result.output || '').split('\n').map((one) => one.trim())
|
|
511
|
+
.filter((one) => one && !one.startsWith('>'))
|
|
512
|
+
const line = lines.find((one) => ERROR_LINE.test(one)) || lines[0] || ''
|
|
513
|
+
return { gate, status: result.status, ms: result.ms, line: line.slice(0, MAX_LINE) }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Un gate que vuelve en menos de esto no corrió una suite. No se afirma que **no** haya corrido —un
|
|
517
|
+
// lint puede fallar rápido y de verdad— y por eso lo que se agrega es el número, no un veredicto: los
|
|
518
|
+
// tres gates del caso 068 volvieron a un segundo uno de otro contra los trece de la corrida real.
|
|
519
|
+
const DEMASIADO_RAPIDO = 2000
|
|
520
|
+
function comoSeLee(failures) {
|
|
521
|
+
const texto = failures
|
|
522
|
+
.map((one) => `${one.gate} (exit ${one.status}, ${(one.ms / 1000).toFixed(1)} s)`
|
|
523
|
+
+ `${one.line ? `: ${one.line}` : ''}`)
|
|
524
|
+
.join('; ')
|
|
525
|
+
if (!failures.every((one) => one.ms < DEMASIADO_RAPIDO)) return texto
|
|
526
|
+
const cuantos = failures.length === 1 ? 'Volvió' : `Los ${failures.length} volvieron`
|
|
527
|
+
return `${texto}\n${cuantos} en menos de ${DEMASIADO_RAPIDO / 1000} s: eso no alcanza para correr `
|
|
528
|
+
+ 'una suite, así que mirá si llegaron a ejecutarse antes de aprobar esto como un rojo conocido.'
|
|
529
|
+
}
|
|
530
|
+
|
|
456
531
|
function verifyGates(root, dir, aprobado, env, ops) {
|
|
457
532
|
const failures = []
|
|
458
533
|
if (fs.existsSync(path.join(root, 'package.json'))) {
|
|
@@ -463,28 +538,28 @@ function verifyGates(root, dir, aprobado, env, ops) {
|
|
|
463
538
|
for (const script of ['test', 'lint', 'typecheck', 'build']) {
|
|
464
539
|
if (!pkg.scripts || !pkg.scripts[script]) continue
|
|
465
540
|
const result = run(pm, ['run', script], root, env)
|
|
466
|
-
EV.record(ops, script, result.status)
|
|
467
|
-
if (!result.ok) failures.push(
|
|
541
|
+
EV.record(ops, script, result.status, result.ms)
|
|
542
|
+
if (!result.ok) failures.push(fallo(script, result))
|
|
468
543
|
}
|
|
469
544
|
} else if (fs.existsSync(path.join(root, 'go.mod'))) {
|
|
470
545
|
const makefile = path.join(root, 'Makefile')
|
|
471
546
|
if (fs.existsSync(makefile) && /^ci:/m.test(fs.readFileSync(makefile, 'utf8'))) {
|
|
472
547
|
const result = run('make', ['ci'], root, env)
|
|
473
|
-
EV.record(ops, 'make ci', result.status)
|
|
474
|
-
if (!result.ok) failures.push(
|
|
548
|
+
EV.record(ops, 'make ci', result.status, result.ms)
|
|
549
|
+
if (!result.ok) failures.push(fallo('make ci', result))
|
|
475
550
|
} else {
|
|
476
551
|
for (const args of [['test', './...'], ['build', './...']]) {
|
|
477
552
|
const result = run('go', args, root, env)
|
|
478
|
-
EV.record(ops, `go ${args[0]}`, result.status)
|
|
479
|
-
if (!result.ok) failures.push(`go ${args[0]}
|
|
553
|
+
EV.record(ops, `go ${args[0]}`, result.status, result.ms)
|
|
554
|
+
if (!result.ok) failures.push(fallo(`go ${args[0]}`, result))
|
|
480
555
|
}
|
|
481
556
|
}
|
|
482
557
|
} else if (fs.existsSync(path.join(root, 'pyproject.toml')) || fs.existsSync(path.join(root, 'requirements.txt'))) {
|
|
483
558
|
const makefile = path.join(root, 'Makefile')
|
|
484
559
|
if (fs.existsSync(makefile) && /^test:/m.test(fs.readFileSync(makefile, 'utf8'))) {
|
|
485
560
|
const result = run('make', ['test'], root, env)
|
|
486
|
-
EV.record(ops, 'make test', result.status)
|
|
487
|
-
if (!result.ok) failures.push(
|
|
561
|
+
EV.record(ops, 'make test', result.status, result.ms)
|
|
562
|
+
if (!result.ok) failures.push(fallo('make test', result))
|
|
488
563
|
}
|
|
489
564
|
}
|
|
490
565
|
if (!failures.length || aprobado) return
|
|
@@ -492,7 +567,7 @@ function verifyGates(root, dir, aprobado, env, ops) {
|
|
|
492
567
|
// comando a mano se lee como que el guard miente, y lo que pasó es que midió lo que se va a grabar.
|
|
493
568
|
const donde = root === dir ? '' : '\nCorrió sobre el índice, que es lo que el commit graba: si en tu '
|
|
494
569
|
+ 'directorio pasa, es que en disco tenés algo que no está staged.'
|
|
495
|
-
block(`Verify falló en ${path.basename(dir)}: ${failures
|
|
570
|
+
block(`Verify falló en ${path.basename(dir)}: ${comoSeLee(failures)}\nNo se commitea en rojo.${donde}\n`
|
|
496
571
|
+ AP.HOW('OPS_SKIP_VERIFY'))
|
|
497
572
|
}
|
|
498
573
|
|