@ingeniomaps/cauce 0.73.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 CHANGED
@@ -14,6 +14,42 @@ 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
+
17
53
  ## [0.73.0] - 2026-09-09
18
54
 
19
55
  ### Corregido
@@ -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
- function record(root, gate, status) {
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 */ }
@@ -355,14 +355,22 @@ function governance(input) {
355
355
  function run(program, args, cwd, extra = {}) {
356
356
  const env = { ...process.env, ...extra }
357
357
  delete env.NODE_TEST_CONTEXT
358
+ const started = Date.now()
358
359
  const result = spawnSync(program, args, { cwd, encoding: 'utf8', stdio: 'pipe', env })
359
360
  return {
360
361
  ok: result.status === 0,
361
362
  status: result.status,
363
+ ms: Date.now() - started,
362
364
  output: `${result.stdout || ''}${result.stderr || ''}`.trim(),
363
365
  }
364
366
  }
365
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
+
366
374
  // Dónde tiene que correr un gate: sobre lo que el commit va a grabar, que es el índice y no el árbol.
367
375
  // El árbol se le parece casi siempre y por eso el error no se veía — puede tener encima otra versión de
368
376
  // un archivo staged, y puede tener uno sin trackear que el commit no lleva, que es el olvido de
@@ -399,6 +407,19 @@ function commitTree(dir) {
399
407
  for (const line of lines) {
400
408
  if (!line.startsWith('!! ')) continue
401
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
402
423
  const link = path.join(temp, name)
403
424
  if (fs.existsSync(link)) continue
404
425
  fs.mkdirSync(path.dirname(link), { recursive: true })
@@ -421,7 +442,17 @@ function commitTree(dir) {
421
442
  // quien commitea; un proyecto con un gate así tiene que sacar esa escritura del gate.
422
443
  const started = run('git', ['init', '--quiet'], temp)
423
444
  if (started.ok) run('git', ['add', '--all'], temp)
424
- return { root: temp, temp, env: {} }
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' } }
425
456
  }
426
457
 
427
458
  function verify(input) {
@@ -462,6 +493,41 @@ function verify(input) {
462
493
  // Cada gate deja su rastro en `ops`; para qué sirve ese registro lo dice `core/evidence.js`. Lo que se
463
494
  // decide acá es que el rojo se anota igual que el verde: un gate que falló y se commiteó con
464
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
+
465
531
  function verifyGates(root, dir, aprobado, env, ops) {
466
532
  const failures = []
467
533
  if (fs.existsSync(path.join(root, 'package.json'))) {
@@ -472,28 +538,28 @@ function verifyGates(root, dir, aprobado, env, ops) {
472
538
  for (const script of ['test', 'lint', 'typecheck', 'build']) {
473
539
  if (!pkg.scripts || !pkg.scripts[script]) continue
474
540
  const result = run(pm, ['run', script], root, env)
475
- EV.record(ops, script, result.status)
476
- if (!result.ok) failures.push(`${script} (exit ${result.status})`)
541
+ EV.record(ops, script, result.status, result.ms)
542
+ if (!result.ok) failures.push(fallo(script, result))
477
543
  }
478
544
  } else if (fs.existsSync(path.join(root, 'go.mod'))) {
479
545
  const makefile = path.join(root, 'Makefile')
480
546
  if (fs.existsSync(makefile) && /^ci:/m.test(fs.readFileSync(makefile, 'utf8'))) {
481
547
  const result = run('make', ['ci'], root, env)
482
- EV.record(ops, 'make ci', result.status)
483
- if (!result.ok) failures.push(`make ci (exit ${result.status})`)
548
+ EV.record(ops, 'make ci', result.status, result.ms)
549
+ if (!result.ok) failures.push(fallo('make ci', result))
484
550
  } else {
485
551
  for (const args of [['test', './...'], ['build', './...']]) {
486
552
  const result = run('go', args, root, env)
487
- EV.record(ops, `go ${args[0]}`, result.status)
488
- if (!result.ok) failures.push(`go ${args[0]} (exit ${result.status})`)
553
+ EV.record(ops, `go ${args[0]}`, result.status, result.ms)
554
+ if (!result.ok) failures.push(fallo(`go ${args[0]}`, result))
489
555
  }
490
556
  }
491
557
  } else if (fs.existsSync(path.join(root, 'pyproject.toml')) || fs.existsSync(path.join(root, 'requirements.txt'))) {
492
558
  const makefile = path.join(root, 'Makefile')
493
559
  if (fs.existsSync(makefile) && /^test:/m.test(fs.readFileSync(makefile, 'utf8'))) {
494
560
  const result = run('make', ['test'], root, env)
495
- EV.record(ops, 'make test', result.status)
496
- if (!result.ok) failures.push(`make test (exit ${result.status})`)
561
+ EV.record(ops, 'make test', result.status, result.ms)
562
+ if (!result.ok) failures.push(fallo('make test', result))
497
563
  }
498
564
  }
499
565
  if (!failures.length || aprobado) return
@@ -501,7 +567,7 @@ function verifyGates(root, dir, aprobado, env, ops) {
501
567
  // comando a mano se lee como que el guard miente, y lo que pasó es que midió lo que se va a grabar.
502
568
  const donde = root === dir ? '' : '\nCorrió sobre el índice, que es lo que el commit graba: si en tu '
503
569
  + 'directorio pasa, es que en disco tenés algo que no está staged.'
504
- block(`Verify falló en ${path.basename(dir)}: ${failures.join(', ')}. No se commitea en rojo.${donde}\n`
570
+ block(`Verify falló en ${path.basename(dir)}: ${comoSeLee(failures)}\nNo se commitea en rojo.${donde}\n`
505
571
  + AP.HOW('OPS_SKIP_VERIFY'))
506
572
  }
507
573
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.73.0",
3
+ "version": "0.74.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",