@ingeniomaps/cauce 0.90.0 → 0.92.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.
@@ -14,7 +14,7 @@ const {
14
14
  const AP = require('./approval')
15
15
  const CHAT = require('./chat')
16
16
  const { selfApproval } = require('./self-approval')
17
- const { readWip } = require('../planning/parser')
17
+ const { readWip, readWips } = require('../planning/parser')
18
18
  const { runner } = require('../planning/claims')
19
19
  const { hasTasks } = require('../planning/state')
20
20
  const { TEMPLATE_PREFIXES } = require('../core/ownership')
@@ -235,14 +235,33 @@ function planFirst(input) {
235
235
  // Que el guard quede inerte lo dice `automation check`, porque una condición invisible es peor que
236
236
  // no tenerla.
237
237
  if (!hasTasks(planning)) return
238
+ // No tener plan y no ver el propio piden cosas opuestas: escribirlo, o volver al id desde el que ya se
239
+ // escribió. `runner()` sale del árbol donde corre el proceso, así que con una instancia al lado de dos
240
+ // repositorios el mismo cambio cae de un lado o del otro según el directorio, y el mensaje mandaba a
241
+ // escribir un plan que estaba a la vista (caso 152).
242
+ //
243
+ // Se lista recién acá, después de las dos salidas de arriba: quien tiene su plan sale por la primera y
244
+ // no paga esta lectura, que es la misma razón por la que `hasTasks` se pregunta donde se pregunta.
245
+ const ajenos = readWips(planning).filter((one) => one.complete + one.pending > 0)
238
246
  const estado = wip ? `WIP tiene la tarea ${wip.task} y ningún paso` : 'WIP está en IDLE'
239
- const why = `${estado}, así que el plan todavía no está escrito.\n`
240
- + 'Escribí en tu planning/wip/<runner>.md la tarea y su plan aprobado —pasos numerados, cada uno con un estado '
241
- + 'verificable— y volvé al cambio. Si esto no es trabajo de una tarea, aprobá la ruta.\n'
247
+ const why = ajenos.length
248
+ ? `hay plan escrito, pero bajo otro id: ${ajenos.map((one) => `${one.runner} (${one.task})`).join(', ')}.\n`
249
+ + 'Si ese plan es tuyo, volvé a su id con `export CAUCE_RUNNER=<id>` —`ops runners <planning>` los lista '
250
+ + 'con su tarea y su avance— y repetí el cambio. Si vas a trabajar en paralelo, montá tu propio árbol '
251
+ + 'con `ops worktree <planning> <tarea>`, que te devuelve el id hecho.\n'
252
+ : `${estado}, así que el plan todavía no está escrito.\n`
253
+ + 'Escribí en tu planning/wip/<runner>.md la tarea y su plan aprobado —pasos numerados, cada uno con '
254
+ + 'un estado verificable— y volvé al cambio. Si esto no es trabajo de una tarea, aprobá la ruta.\n'
242
255
  for (const raw of filesOf(input)) {
243
256
  if (!isProduct(root, path.resolve(cwdOf(input), raw))) continue
244
257
  if (approved(input, raw)) continue
245
- block(`${raw} cambia el producto sin plan. ${why}${AP.HOW('OPS_PLAN_FIRST_OVERRIDE', [raw], input)}`)
258
+ // Con un plan a la vista no se ofrece ninguna de las dos salidas: aprobar la ruta escribe «esto no es
259
+ // trabajo de una tarea», que ahí es falso, y anunciar la variable ofrece el permiso más ancho cuando
260
+ // la acción correcta es angosta y concreta —el mismo criterio con que `HOW` decide no nombrarla—.
261
+ const how = ajenos.length
262
+ ? AP.HOW(null, [], input, [])
263
+ : AP.HOW('OPS_PLAN_FIRST_OVERRIDE', [raw], input)
264
+ block(`${raw} cambia el producto sin plan. ${why}${how}`)
246
265
  }
247
266
  }
248
267
 
@@ -10,13 +10,14 @@ const path = require('node:path')
10
10
  const { spawnSync } = require('node:child_process')
11
11
  const {
12
12
  commandOf, cwdOf, block, isCommit, stagedForCommit,
13
- writableRoots, outsideRoots, DECLARE_IT, unquoted, opsRoot, withoutGitGlobals,
13
+ writableRoots, outsideRoots, DECLARE_IT, unquoted, opsRoot, withoutGitGlobals, configOf,
14
14
  } = require('./input')
15
15
  const AP = require('./approval')
16
16
  const CHAT = require('./chat')
17
17
  const { publish } = require('./push')
18
18
  const { selfApprovalShell } = require('./self-approval')
19
19
  const EV = require('../core/evidence')
20
+ const SC = require('../core/scope')
20
21
 
21
22
  // Dónde empieza y dónde termina una palabra dentro de un comando. Tres reglas de la tabla de abajo lo
22
23
  // decidían por su cuenta admitiendo sólo un espacio, el principio o el fin, y en un shell una palabra
@@ -418,13 +419,25 @@ const RECREABLE = new RegExp('(^|/)(?:dist|build|out|coverage|__pycache__'
418
419
  //
419
420
  // No se usa `git stash --keep-index`, que sería más corto: toca el árbol de quien está trabajando, y un
420
421
  // gate que muere a la mitad le deja el stash puesto.
421
- function commitTree(dir) {
422
+ function commitTree(dir, input) {
422
423
  const status = run('git', ['-C', dir, 'status', '--porcelain', '--ignored'], dir)
423
424
  if (!status.ok) {
424
425
  block(`no se pudo leer el estado de ${dir}, así que no hay cómo saber qué va a grabar el commit.`)
425
426
  }
426
427
  const lines = status.output.split('\n').filter(Boolean)
427
- if (!lines.some((line) => !line.startsWith('!!') && line[1] !== ' ')) {
428
+ const delta = lines.filter((line) => !line.startsWith('!!') && line[1] !== ' ')
429
+ if (!delta.length) {
430
+ return { root: dir, temp: null, env: {} }
431
+ }
432
+ // La de arriba pregunta *si hay* delta; ésta, *qué* delta: lo que ninguna puerta lee no puede cambiar
433
+ // su veredicto, y ahí el árbol vuelve a servir. Las reglas y el porqué viven en `core/scope.js`; sin
434
+ // una raíz que declare su alcance esto no cambia nada (caso 156).
435
+ //
436
+ // La raíz ops no es `dir` —una instancia sidecar las tiene separadas—, así que se resuelve como en el
437
+ // resto de los guards en vez de suponer un `ops.config.json` colgando del repositorio: deducir el
438
+ // layout en lugar de leer el declarado es lo que costó el caso 158.
439
+ const ops = opsRoot(input)
440
+ if (SC.staysInTree(ops ? configOf(ops).workspaceRoots : [], dir, delta)) {
428
441
  return { root: dir, temp: null, env: {} }
429
442
  }
430
443
 
@@ -488,11 +501,17 @@ function commitTree(dir) {
488
501
  // el lockfile de la copia y reinstala, lo que **empieza borrando** el `node_modules` del proyecto.
489
502
  //
490
503
  // Lo que se apaga es esa comprobación previa, que es el motivo por el que quiere tocar nada.
491
- // `verify-deps-before-run` la gobierna y tiene tres valores: `install` reinstala solo —el que trae
492
- // pnpm 11 y el que hace el daño—, `error` se niega y frena el gate cuando el lockfile de la copia
493
- // difiere de lo instalado, que es justo lo que pasa al commitear un cambio de lockfile por partes, y
494
- // `false` corre el script sin mirar. La copia no tiene que sincronizar nada: tiene que medir el
495
- // código.
504
+ // `verify-deps-before-run` la gobierna y tiene cinco valores —`install`, `warn`, `prompt`, `error` y
505
+ // `false`—. El que hace el daño es `install`, que reinstala solo y es el default desde pnpm 11. `error`
506
+ // tampoco serviría: frena el gate cuando el lockfile de la copia difiere de lo instalado, que es justo
507
+ // lo que pasa al commitear un cambio de lockfile por partes. La copia no tiene que sincronizar nada:
508
+ // tiene que medir el código.
509
+ //
510
+ // El prefijo es `pnpm_config_` y no `npm_config_`, y esa sola palabra es la diferencia entre apagar la
511
+ // comprobación y no apagar nada: pnpm lee sus ajustes del entorno con su propio prefijo, así que con el
512
+ // de npm la variable llega igual y se ignora en silencio. Acá estuvo `npm_config_` desde el arreglo del
513
+ // 070 y no surtió efecto nunca (caso 151), con lo cual la protección que ese arreglo creyó poner no
514
+ // estuvo puesta. Medido sobre pnpm 10.30.2 y 11.20.0, iguales las dos.
496
515
  //
497
516
  // Acá estuvo `CI: 'true'` y fue una regresión (caso 070). Resolvía el síntoma del 068 —pnpm dejaba de
498
517
  // preguntar antes de purgar— desarmando la confirmación en vez de quitarle el motivo, y esa
@@ -502,7 +521,7 @@ function commitTree(dir) {
502
521
  //
503
522
  // La regla que queda: no se desarma la confirmación de una herramienta, se le quita el motivo de
504
523
  // preguntar. Una confirmación que estorba casi siempre está cuidando algo.
505
- return { root: temp, temp, env: { npm_config_verify_deps_before_run: 'false' } }
524
+ return { root: temp, temp, env: { pnpm_config_verify_deps_before_run: 'false' } }
506
525
  }
507
526
 
508
527
  function verify(input) {
@@ -529,7 +548,7 @@ function verify(input) {
529
548
  + AP.HOW('OPS_SKIP_VERIFY', sinAprobar, input))
530
549
  }
531
550
  if (!staged.some((file) => /\.(?:ts|tsx|js|jsx|mjs|cjs|go|py|html|css|scss|prisma)$/.test(file))) return
532
- const { root, temp, env } = commitTree(dir)
551
+ const { root, temp, env } = commitTree(dir, input)
533
552
  try {
534
553
  verifyGates(root, dir, sinAprobar, env, input)
535
554
  } finally {
@@ -49,6 +49,15 @@
49
49
  "verify": {
50
50
  "type": "string",
51
51
  "description": "Comando que corre la puerta de esta raíz —pruebas, lint, typecheck, build—, tal como se invoca desde ella. Sin él, quien verifica tiene que descubrirlo leyendo el repositorio en cada tarea, y eso es trabajo de modelo repetido para siempre sobre una respuesta que no cambia."
52
+ },
53
+ "scope": {
54
+ "type": "array",
55
+ "minItems": 1,
56
+ "items": {
57
+ "type": "string",
58
+ "minLength": 1
59
+ },
60
+ "description": "Qué rutas lee la puerta de esta raíz, relativas a ella, con * ? y **. Sirve para que un archivo sucio que el gate no va a abrir no fuerce la copia del índice. Sin él, cualquier diferencia entre árbol e índice la fuerza, que es el comportamiento de siempre."
52
61
  }
53
62
  },
54
63
  "additionalProperties": false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.90.0",
3
+ "version": "0.92.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",
@@ -32,3 +32,15 @@ migraciones, un repo donde sí puede commitear directo, un entorno de pruebas qu
32
32
 
33
33
  Lo que no se puede ampliar acá: promover trabajo propio, prometer fechas, inventar evidencia o exceder
34
34
  la autoridad de un cargo. Eso no depende del proyecto.
35
+
36
+ ### Límites
37
+
38
+ Una línea por límite, y cada una tiene que poder obedecerse sola: es lo que viaja al preámbulo de cada
39
+ agente, sin el párrafo que la rodea. El ejemplo va comentado a propósito — un ejemplo que se obedece es
40
+ peor que ninguno, así que descomentalo recién cuando sea tuyo de verdad.
41
+
42
+ <!-- - En `api/` no se tocan migraciones sin aprobación de quien administra la base. -->
43
+
44
+ Fuera de esta lista también cuenta lo que arranque con «El runner», «Debe» o «Nunca», que es como estaba
45
+ escrito antes de que esta sección existiera. `ops check` avisa de cualquier párrafo que no entre por
46
+ ninguno de los dos caminos, porque un límite perdido no se ve: la lista sale más corta y se lee igual.