@ingeniomaps/cauce 0.83.0 → 0.84.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,48 @@ 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.84.0] - 2026-09-12
18
+
19
+ ### Corregido
20
+
21
+ - **Una frase en inglés que prohíbe ya no autoriza.** Cauce reconoce si le negás algo antes de dejarlo
22
+ pasar, y en inglés sólo entendía `don't`: «the tool doesn't read the .env» o «we can't read the .env»
23
+ nombraban el archivo, traían un verbo y ninguna negación reconocida, así que **una frase que prohíbe
24
+ terminaba autorizando**. Entran ahora las contraídas con auxiliar —`doesn't`, `isn't`, `can't`,
25
+ `won't`, `wasn't`, `wouldn't`, `shouldn't`…—, con o sin apóstrofo.
26
+
27
+ **Qué cambia para vos**: nada que hacer. Si trabajás en inglés, negar algo ahora se respeta; lo que antes
28
+ pasaba por no entenderse, se frena. Una frase como «I can't tell if the .env is right» deja de autorizar
29
+ y cuesta un «dale», que es el mismo costo que el español ya tenía.
30
+
31
+ - **La entrada de DONE dice contra qué reglas se revisó.** `autobuild` exige que Review nombre las reglas
32
+ contra las que revisó —si no las nombra, la corrida para—, pero esa lista quedaba sólo en el registro de
33
+ la corrida, que no sobrevive. Ahora va también en la línea `review=` de `DONE.md`, que es lo que queda.
34
+
35
+ **Qué cambia para vos**: nada que hacer. Al auditar una entrega vieja vas a poder decir contra qué se
36
+ revisó sin depender del registro de aquella corrida.
37
+
38
+ - **Lo que pedís vos ya no se frena sin ofrecerte ninguna salida.** Siete de los guards que pueden frenar
39
+ nunca consultaban si lo habías pedido: no tenían variable, ni línea que pegar en
40
+ `planning/.ops-approval`, ni «dale». Entre ellos estaban las reglas que cubren trabajo de todos los días
41
+ —`git reset --hard`, `git clean -f`, `git checkout -- .`, `docker compose down`, `docker system prune`—
42
+ y los que frenan escribir una credencial, un archivo generado, un snapshot del sincronizador o el motor
43
+ instalado. Pedirlos con todas las letras no servía de nada: se frenaban igual que si los hubiera
44
+ decidido el agente por su cuenta.
45
+
46
+ Ahora preguntan lo mismo que el resto de los guards: si lo pediste en el chat pasa, y si no, el bloqueo
47
+ te dice qué se frenó y un «dale» lo aprueba. De paso se cierra una asimetría que nadie había decidido:
48
+ **leer** una credencial se podía aprobar desde 0.80.0 y **escribirla** no.
49
+
50
+ **Lo que no cambia, y es a propósito**: reescribir historia publicada —`git push --force`,
51
+ `git commit --amend`—, el borrado de disco, `rm -r` sobre la raíz, el home o el directorio padre, y
52
+ `git add -A`. Ésos no se abren ni pidiéndolos, porque R8 y R23 no admiten excepción configurable.
53
+
54
+ **Qué cambia para vos**: nada que hacer ni que reinstalar. Lo que antes te obligaba a exportar una
55
+ variable para poder trabajar —o directamente no tenía salida— ahora se destraba diciéndolo. El permiso
56
+ es angosto a propósito: vale para el comando entero tal como lo escribiste, así que otra bandera es otro
57
+ comando y se vuelve a preguntar.
58
+
17
59
  ## [0.83.0] - 2026-09-11
18
60
 
19
61
  ### Agregado
@@ -790,7 +790,11 @@ while (rounds++ < MAX_TASKS) {
790
790
  if (governing.length) log(`Review contra: ${review.rules.join(', ')}`)
791
791
  // Aprobar sin declarar qué se abrió no se arregla mandando a tocar código: falló quien revisó.
792
792
  if (!review.consulted.length) return stop('review-unbacked', 'aprobó el diff sin declarar qué inspeccionó')
793
+ // Contra qué reglas se revisó va también a la entrada de DONE, y no sólo al journal: el journal muere
794
+ // con la corrida y la entrada queda, así que sin esto no había cómo reconstruir contra cuáles se
795
+ // revisó cuando alguien audita la entrega meses después (caso 122).
793
796
  reviewFact = `${review.verdict} por ${cast.review}, sobre ${review.consulted.join(', ')}`
797
+ + ((review.rules || []).length ? ` · reglas: ${review.rules.join(', ')}` : '')
794
798
  // Lo que no impide entregar no manda a tocar código, y tampoco desaparece: la mejora opinable que se
795
799
  // corrige a las apuradas cuesta una vuelta y un riesgo que nadie pidió. Va a Propuestas y no a
796
800
  // Lecciones porque lo que la revisión anotó es un cambio del producto —su evidencia es la de la
@@ -111,9 +111,14 @@ function HOW(variable, lines, input, pasteable = lines) {
111
111
  + 'sin resolver, y la aprobación compara texto, así que esa línea sólo valdría para un comando escrito '
112
112
  + 'igual. Volvé a correrlo con la ruta escrita y el bloqueo va a decir qué pegar. '
113
113
  : ''
114
- return ask + paste + unresolved
115
- + `La variable ${variable}=1 sigue existiendo y apaga el guard para toda la sesión, que es por lo que no `
116
- + 'es la vía recomendada.'
114
+ // Sin variable no se nombra ninguna. Un guard que no tiene apagado por sesión no debería anunciar uno, y
115
+ // los que estrenan salida angosta con el 117 no lo tienen a propósito: ofrecer el permiso más ancho
116
+ // cuando alcanza el más angosto es lo que hizo que la variable fuera la vía que quedaba a mano (caso 089).
117
+ const off = variable
118
+ ? `La variable ${variable}=1 sigue existiendo y apaga el guard para toda la sesión, que es por lo que no `
119
+ + 'es la vía recomendada.'
120
+ : ''
121
+ return ask + paste + unresolved + off
117
122
  }
118
123
 
119
124
  module.exports = { APPROVAL, lines, read, pending, pendingNow, where, HOW }
@@ -44,7 +44,14 @@ function load(session) {
44
44
 
45
45
  // Nombrar no es pedir: «no toques el .env» nombra el .env. Cuenta la negación que está en la misma frase y
46
46
  // antes del nombre; la coma corta, porque «leé el config, no el .env» son dos pedidos.
47
- const NEGATION = /(?:^|[^\p{L}])(?:no|nunca|jam[aá]s|ni|sin|not|never|don'?t)(?![\p{L}])/iu
47
+ // En inglés la negación se escribe contraída, y de todas se conocía una sola: `don't`. «the tool doesn't
48
+ // read the .env» nombraba el archivo, traía un verbo de la lista y ninguna negación reconocida, así que una
49
+ // frase que **prohíbe** autorizaba (caso 120). Es el error en la dirección peligrosa, al revés que el 109.
50
+ // El auxiliar entra en la alternancia con el apóstrofo opcional, que es lo que `don'?t` ya hacía para uno;
51
+ // `don't` sale de acá —`do` + `n't`—, así que no quedan dos reglas para lo mismo.
52
+ const CONTRACTED = String.raw`(?:do|does|did|is|are|was|were|ca|wo|would|should)n'?t`
53
+ const NEGATION = new RegExp(
54
+ String.raw`(?:^|[^\p{L}])(?:no|nunca|jam[aá]s|ni|sin|not|never|${CONTRACTED})(?![\p{L}])`, 'iu')
48
55
  // Un punto corta sólo si cierra la oración: el de `x.js` es parte del nombre, y cortar ahí dejaba el verbo de
49
56
  // «agregá src/x.js a .ops-approval» en otra frase que la del archivo.
50
57
  const CLAUSE = /[,;:!?\n]|\.(?=\s|$)/
@@ -76,10 +76,14 @@ function credential(input, raw) {
76
76
  : ''
77
77
  }
78
78
 
79
+ // Escribir una credencial se autoriza igual que leerla. `secrets-read` consulta la aprobación desde 0.80.0
80
+ // y éste frenaba sin ofrecer nada, así que la misma persona podía aprobar leer su `.env` y no podía aprobar
81
+ // escribirlo. La asimetría no la decidió nadie (caso 117).
79
82
  function secrets(input) {
80
83
  for (const file of filesOf(input)) {
81
84
  const reason = credential(input, file)
82
- if (reason) block(`${file} ${reason}`)
85
+ if (!reason || approved(input, file)) continue
86
+ block(`${file} ${reason}\n${AP.HOW(null, [file], input)}`)
83
87
  }
84
88
  }
85
89
 
@@ -105,9 +109,10 @@ function secretsRead(input) {
105
109
  function integrationSnapshot(input) {
106
110
  for (const raw of filesOf(input)) {
107
111
  const file = raw.replace(/\\/g, '/')
108
- if (/(?:^|\/)integrations\/[^/]+\/staging\/(?:.+\/remote\.json|sync-state\.json)$/.test(file)) {
109
- block(`${file} pertenece al sincronizador. Cura draft.md; no edites snapshots a mano.`)
110
- }
112
+ if (!/(?:^|\/)integrations\/[^/]+\/staging\/(?:.+\/remote\.json|sync-state\.json)$/.test(file)) continue
113
+ if (approved(input, raw)) continue
114
+ block(`${file} pertenece al sincronizador. Cura draft.md; no edites snapshots a mano.\n`
115
+ + AP.HOW(null, [raw], input))
111
116
  }
112
117
  }
113
118
 
@@ -115,9 +120,10 @@ function generated(input) {
115
120
  for (const raw of filesOf(input)) {
116
121
  const file = raw.replace(/\\/g, '/')
117
122
  const base = path.basename(file)
118
- if (/(?:^|[._-])generated\.[^.]+$/i.test(base) || /(?:^|[._-])gen\.(?:go|ts|js|py)$/i.test(base)) {
119
- block(`${file} parece código generado. Modifica su fuente y ejecuta el generador; no lo edites a mano.`)
120
- }
123
+ if (!/(?:^|[._-])generated\.[^.]+$/i.test(base) && !/(?:^|[._-])gen\.(?:go|ts|js|py)$/i.test(base)) continue
124
+ if (approved(input, raw)) continue
125
+ block(`${file} parece código generado. Modifica su fuente y ejecuta el generador; no lo edites a mano.\n`
126
+ + AP.HOW(null, [raw], input))
121
127
  }
122
128
  }
123
129
 
@@ -342,6 +348,7 @@ function engineWrites(input) {
342
348
  for (const raw of filesOf(input)) {
343
349
  const file = path.resolve(cwdOf(input), raw)
344
350
  if (file !== pkg && !file.startsWith(`${pkg}${path.sep}`)) continue
351
+ if (approved(input, raw)) continue
345
352
  block(`${raw} pertenece al motor de Cauce, que llega por npm.\n` +
346
353
  'Un cambio acá lo borra el próximo install y mientras tanto corrés un motor que no coincide ' +
347
354
  'con la versión que declarás. Para traer una versión nueva son dos pasos —el motor y después ' +
@@ -75,7 +75,7 @@ function destructive(input) {
75
75
  }
76
76
  publish(input, command)
77
77
  const rules = [
78
- [/\bgit\s+reset\s+--hard\b/, "'git reset --hard' destruye cambios locales."],
78
+ [/\bgit\s+reset\s+--hard\b/, "'git reset --hard' destruye cambios locales.", true],
79
79
  // R8 lo prohíbe sin excepción configurable y ningún guard lo miraba: `grep -rn amend engine/hooks/`
80
80
  // no devolvía una línea. Se bloquea por política y no por daño —un `--amend` sobre algo que nadie vio
81
81
  // no rompe nada—, así que el mensaje manda a lo que sí corresponde: otro commit.
@@ -83,7 +83,7 @@ function destructive(input) {
83
83
  new RegExp(String.raw`\bgit\s+commit\b${MISMO}*\s--amend\b`),
84
84
  "'git commit --amend' reescribe un commit ya creado. R8 pide uno nuevo en su lugar.",
85
85
  ],
86
- [/\bgit\s+clean\s+-[^\s]*f/, "'git clean -f' borra archivos sin seguimiento."],
86
+ [/\bgit\s+clean\s+-[^\s]*f/, "'git clean -f' borra archivos sin seguimiento.", true],
87
87
  // `git checkout -- .` destruye lo mismo que `reset --hard` y sin recuperación, pero se escribe como
88
88
  // una limpieza. Se bloquea sólo la forma ancha —`.`, `*`, `:/`, o sin ruta—: revertir un archivo
89
89
  // nombrado es trabajo corriente y no se toca.
@@ -98,14 +98,17 @@ function destructive(input) {
98
98
  + String.raw`(?:--(?=\s*(?:${COMANDO}))|(?:--\s+)?(?:\.|\*|:\/)(?=\s*(?:${COMANDO})))`),
99
99
  "'git checkout -- .' revierte todo lo no commiteado del directorio, no sólo lo que estás mirando. "
100
100
  + 'Nombrá el archivo, o commiteá lo que quieras conservar antes.',
101
+ true,
101
102
  ],
102
103
  [
103
104
  /\bdocker(?:\s+\w+)*\s+(?:volume\s+(?:rm|prune)|system\s+prune|network\s+prune)\b/,
104
105
  'La limpieza global de Docker puede borrar datos compartidos.',
106
+ true,
105
107
  ],
106
108
  [
107
109
  /\bdocker(?:\s+compose|-compose)\s+(?:\S+\s+)*(?:down|stop|kill|rm)\b/,
108
110
  'Detener un stack Compose puede interrumpir servicios compartidos.',
111
+ true,
109
112
  ],
110
113
  [
111
114
  new RegExp(ANTES + String.raw`(?:mkfs\S*|shred)\s`
@@ -117,7 +120,22 @@ function destructive(input) {
117
120
  "'rm -r' sobre /, home o el directorio padre es catastrófico.",
118
121
  ],
119
122
  ]
120
- for (const [pattern, message] of rules) if (pattern.test(command)) block(message)
123
+ // El tercer elemento dice si la regla tiene salida. Las que no la tienen son las que una persona tampoco
124
+ // debería poder abrir pidiéndolo: `--amend` y el force-push de arriba, que R8 prohíbe sin excepción
125
+ // configurable; el borrado de disco; y `rm -r` sobre raíz, home o el padre, que es la clase que gobierna
126
+ // R23. El resto no tenía salida por omisión y no por decisión: son anteriores al canal de chat de 0.81.0
127
+ // —por qué ese canal existe y qué distingue, en `chat.js`— y nadie volvió a pasarles por encima (117).
128
+ //
129
+ // El ítem que se aprueba es el comando entero tal como llegó, no el pedazo que matcheó: `git clean -f`
130
+ // no aparece entero dentro de «corré git clean -fd», así que aprobar el fragmento no destrabaría lo que
131
+ // la persona escribió. Entero es además lo que ella pegaría en el archivo, y lo más angosto: cualquier
132
+ // otra bandera es otro comando y vuelve a preguntarse.
133
+ for (const [pattern, message, open] of rules) {
134
+ if (!pattern.test(command)) continue
135
+ const item = String(raw).trim()
136
+ if (open && !AP.pending(opsRoot(input), [item], input).length) continue
137
+ block(open ? `${message}\n${AP.HOW(null, [item], input)}` : message)
138
+ }
121
139
  }
122
140
 
123
141
  function gitAdd(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.83.0",
3
+ "version": "0.84.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",