@ingeniomaps/cauce 0.63.0 → 0.64.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,29 @@ 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.64.0] - 2026-09-06
18
+
19
+ ### Agregado
20
+
21
+ - **Las salidas de los guards están documentadas, y con su alcance real.** Cinco guards se pueden abrir
22
+ y ninguna de las cinco llaves estaba escrita en un archivo que alguien fuera a abrir: el bloqueo te
23
+ nombraba una variable y no había dónde leer dónde va. `AGENTS.md` ahora las lista en «Qué se puede
24
+ editar y qué no», con lo incómodo dicho: el guard lee la variable de **su propio proceso**, así que
25
+ escribirla delante del comando no llega, y la forma que sí funciona —exportarla en el entorno desde
26
+ el que arranca tu runner— deja el guard apagado hasta que cierres la sesión. La única con alcance de
27
+ operación es la aprobación de gobernanza.
28
+
29
+ - **Un commit de gobernanza se aprueba por operación, no por sesión.** El guard ofrecía como salida
30
+ `OPS_GOVERNANCE_OVERRIDE=1`, que se lee del entorno del proceso: prendida antes de lanzar tu runner
31
+ deja el guard apagado hasta que la sesión cierre. Eso convierte «aprobado este commit» en «apagado
32
+ hasta que me vaya», y el `Makefile` de este proyecto ya decía cuál es el alcance correcto — «la
33
+ autorización de R10 es por operación y humana». **Qué cambia para vos**: escribís las rutas
34
+ autorizadas en `planning/.governance-approval`, una por línea, y el commit pasa. Vale para ese
35
+ conjunto y para ningún otro: si después sumás un archivo, ese archivo no está aprobado. No se consume
36
+ ni se borra sola —así un commit frenado por otra razón no te obliga a rehacerla—, así que `check` te
37
+ avisa mientras exista para que la borres. La variable sigue funcionando y el mensaje del guard ahora
38
+ dice por qué no es la vía recomendada.
39
+
17
40
  ## [0.63.0] - 2026-09-06
18
41
 
19
42
  ### Corregido
@@ -11,6 +11,7 @@ const PC = require('../planning/contracts')
11
11
  const SZ = require('../planning/sizing')
12
12
  const ST = require('../planning/state')
13
13
  const AD = require('../planning/adoption')
14
+ const AP = require('../hooks/approval')
14
15
  const I = require('../integrations/registry')
15
16
  const O = require('../core/ownership')
16
17
  const OB = require('../core/onboarding')
@@ -85,6 +86,14 @@ function check(dir, cli) {
85
86
  epics, milestones, done, wip, roles, humanActions: P.readHumanActions(root), adopted: new Set(adopted),
86
87
  }))
87
88
  warnings.push(...AD.report({ done, epics, adopted }))
89
+ // Una aprobación de gobernanza vale para el conjunto que nombra, así que olvidada sigue autorizando
90
+ // esas mismas rutas la próxima vez que alguien las stagee. No caduca sola: lo que la cierra es que se
91
+ // vea en cada corrida y alguien la borre.
92
+ const aprobadas = AP.read(path.resolve(root, '..'))
93
+ if (aprobadas.length) {
94
+ warnings.push(`planning/${AP.APPROVAL}: ${aprobadas.length} ruta(s) de gobernanza aprobadas y sin `
95
+ + 'borrar; el archivo sigue autorizándolas')
96
+ }
88
97
 
89
98
  const integration = I.validate(path.resolve(root, '..'))
90
99
  errors.push(...integration.errors)
@@ -0,0 +1,31 @@
1
+ 'use strict'
2
+
3
+ // La aprobación de un commit de gobernanza: una lista de rutas que una persona escribió a mano para
4
+ // autorizar exactamente ese cambio. Existe porque la vía que había era una variable de entorno, y una
5
+ // variable es por sesión: prendida antes de lanzar el runner deja el guard apagado hasta que la sesión
6
+ // cierre. El `Makefile` de este repositorio ya dice cuál es el alcance correcto —«la autorización de R10
7
+ // es por operación y humana»—, y una llave que dura toda la sesión no lo es.
8
+ //
9
+ // **Se coteja, no se consume.** Borrar el archivo al leerlo daría el mismo alcance y traería dos cosas
10
+ // que no queremos: hoy ningún guard escribe en el repositorio, y `governance` corre antes que `verify`,
11
+ // así que un commit frenado por otra razón se habría llevado puesta la aprobación y habría que
12
+ // rehacerla. Cotejando, la aprobación vale para el conjunto que nombra y para ningún otro: en cuanto
13
+ // cambia lo que está en el índice deja de servir, que es «por operación» sin fecha ni contador.
14
+ //
15
+ // Queda a la vista porque `check` avisa mientras exista. Sin eso, un archivo olvidado sigue autorizando
16
+ // esas mismas rutas la próxima vez que alguien las stagee, que es la puerta abierta que esto evitaba.
17
+
18
+ const path = require('node:path')
19
+ const fs = require('node:fs')
20
+
21
+ const APPROVAL = '.governance-approval'
22
+
23
+ // Una ruta por línea, `#` para lo demás. El archivo ausente y el vacío son lo mismo: no hay nada
24
+ // aprobado, que es el estado normal.
25
+ function read(root) {
26
+ let text = ''
27
+ try { text = fs.readFileSync(path.join(root, 'planning', APPROVAL), 'utf8') } catch { return [] }
28
+ return text.split('\n').map((line) => line.replace(/#.*$/, '').trim()).filter(Boolean)
29
+ }
30
+
31
+ module.exports = { APPROVAL, read }
@@ -10,8 +10,9 @@ const path = require('node:path')
10
10
  const { spawnSync } = require('node:child_process')
11
11
  const {
12
12
  commandOf, cwdOf, block, gitDirectory, isCommit, stagedFiles, pushAllowed,
13
- writableRoots, outsideRoots, DECLARE_IT, unquoted,
13
+ writableRoots, outsideRoots, DECLARE_IT, unquoted, findOpsRoot,
14
14
  } = require('./input')
15
+ const AP = require('./approval')
15
16
 
16
17
  // Dónde empieza y dónde termina una palabra dentro de un comando. Tres reglas de la tabla de abajo lo
17
18
  // decidían por su cuenta admitiendo sólo un espacio, el principio o el fin, y en un shell una palabra
@@ -250,12 +251,19 @@ function governance(input) {
250
251
  String.raw`|evaluations\/(?:cases\/|expected-behaviors\.yaml)|learning\/proposals\/))`,
251
252
  )
252
253
  const governed = stagedFiles(dir).filter((file) => governedPattern.test(file))
253
- if (governed.length) {
254
- const files = governed.map((file) => ` - ${file}`).join('\n')
255
- block(`El commit toca gobernanza protegida:\n${files}\n` +
256
- 'Usa OPS_GOVERNANCE_OVERRIDE=1 solo con aprobación, en el entorno del guard: escrita delante '
257
- + 'del comando no llega hasta acá.')
258
- }
254
+ if (!governed.length) return
255
+ // La aprobación vale para lo que nombra y para nada más: lo que quede sin cubrir es lo que se
256
+ // reporta. Así una aprobación vieja no autoriza el archivo que se sumó después, que es la diferencia
257
+ // entre una llave por operación y una puerta que quedó abierta.
258
+ const root = findOpsRoot(process.env.OPS_ROOT || process.env.CLAUDE_PROJECT_DIR || cwdOf(input))
259
+ const aprobados = new Set(root ? AP.read(root) : [])
260
+ const pendientes = governed.filter((file) => !aprobados.has(file))
261
+ if (!pendientes.length) return
262
+ const files = pendientes.map((file) => ` - ${file}`).join('\n')
263
+ block(`El commit toca gobernanza protegida:\n${files}\n`
264
+ + `Aprobalo escribiendo esas rutas en planning/${AP.APPROVAL}, una por línea: vale para ese `
265
+ + 'conjunto y deja de valer en cuanto cambie. La variable OPS_GOVERNANCE_OVERRIDE=1 sigue '
266
+ + 'existiendo y apaga el guard para toda la sesión, que es por lo que no es la vía recomendada.')
259
267
  }
260
268
 
261
269
  function run(program, args, cwd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.63.0",
3
+ "version": "0.64.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",
@@ -98,6 +98,34 @@ siquiera están acá — los lee el motor desde el paquete.
98
98
  Un guard existente **no se edita**: `upgrade` detecta el cambio y se detiene antes de pisarlo, y con
99
99
  `--force` deja registrado qué descartó.
100
100
 
101
+ ### Cuando un guard te frena con razón
102
+
103
+ Algunos bloqueos tienen salida, y conviene saber cuál antes de necesitarla — el momento en que un guard
104
+ te frena es el peor para elegir bien.
105
+
106
+ **Un commit que toca gobernanza** —reglas, ADRs, el contrato de un cargo o lo que lo mide— se autoriza
107
+ escribiendo las rutas en `planning/.governance-approval`, una por línea, con `#` para lo que no sea una
108
+ ruta. Vale para ese conjunto y para ningún otro: si después sumás un archivo, ese archivo no está
109
+ aprobado y el guard lo nombra. No se borra sola —así un commit frenado por otra cosa no te obliga a
110
+ rehacerla—, así que `check` te avisa mientras exista, y borrarla es parte de terminar.
111
+
112
+ **Las demás salidas son variables de entorno**, y hay que decir su alcance
113
+ porque no es el que uno espera: el guard la lee de **su propio proceso**, no del comando. Escribirla
114
+ delante —`VAR=1 git commit`— no llega. La forma que sí funciona es exportarla en el entorno desde el que
115
+ arranca tu runner, y eso deja el guard apagado **hasta que cierres la sesión**, no para un comando.
116
+
117
+ | variable | qué abre |
118
+ |---|---|
119
+ | `OPS_GOVERNANCE_OVERRIDE=1` | lo mismo que la aprobación de arriba, pero para toda la sesión |
120
+ | `OPS_MIGRATIONS_OVERRIDE=1` | escribir SQL destructivo en una migración |
121
+ | `OPS_TEST_EVIDENCE_OVERRIDE=1` | borrar o apagar una prueba |
122
+ | `OPS_DEPENDENCIES_OVERRIDE=1` | tocar manifiestos y lockfiles, publicar o instalar global |
123
+ | `OPS_SKIP_VERIFY=1` | saltear los gates del stack antes de un commit |
124
+
125
+ Son de sesión y no de operación, que es exactamente lo que la aprobación de gobernanza vino a corregir.
126
+ Mientras sigan así, lo que corresponde es prenderlas para lo que hacía falta y apagarlas después — y que
127
+ la razón quede escrita donde alguien la lea, no sólo en la memoria de quien la prendió.
128
+
101
129
  ## Cómo leer el estado
102
130
 
103
131
  Antes de abrir un archivo de `planning/`, preguntarle al CLI: es determinista, no gasta contexto y no