@ingeniomaps/cauce 0.31.0 → 0.32.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,27 @@ 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.32.0] - 2026-08-19
18
+
19
+ ### Corregido
20
+
21
+ - **Las credenciales de un multirepo no existían para el arranque.** `/onboard` leía un solo
22
+ `.env.example`, el de la raíz del workspace: en un monolito eso es todo, entre repositorios hermanos no
23
+ es nada. Una corrida real sobre tres repos dejó filas que decían «la credencial del proveedor» sin
24
+ nombrar una sola variable, y un externo que sólo ese archivo mencionaba —un certificado de organismo
25
+ fiscal— no apareció en ningún lado.
26
+
27
+ Ahora `ops scan` reporta, por servicio, los nombres que declara su ejemplo y de qué archivo salieron, y
28
+ el arranque los usa para nombrar cada fila. **Sólo los nombres**: el valor es de una persona, y el
29
+ ejemplo es la mitad pública del par. Se leen `.env.example`, `.env.sample`, `.env.template` y
30
+ `.env.dist`; un ejemplo de más de cuarenta variables es un archivo generado y no un contrato, así que
31
+ se corta ahí y lo dice.
32
+
33
+ - **`ops scan <ruta>` dejaba afuera al proyecto de la raíz.** Un monolito que declara sus comandos en el
34
+ nivel de arriba volvía listando todo menos a sí mismo, mientras que desde adentro de la instancia el
35
+ mismo árbol sí lo listaba: dos respuestas distintas sobre el mismo directorio. Los dos caminos comparten
36
+ una sola función y el proyecto de la raíz va primero.
37
+
17
38
  ## [0.31.0] - 2026-08-18
18
39
 
19
40
  ### Añadido
@@ -167,8 +167,9 @@ const drafted = await agent(
167
167
  'completa.\n'}` +
168
168
  `4. ${HUMAN}: una fila por cada cosa que necesita a una persona, con la tarea, el estado pendiente, el ` +
169
169
  `origen "onboard" y la acción concreta que la desbloquea. Como mínimo, una por cada credencial que el ` +
170
- `proyecto espera —dónde se cargan y quién lo hace, sin proponer ningún valor—, una por cada sistema ` +
171
- `externo o MCP a conectar, y una por la autoridad del runner, que hoy declara runner.allowPush=false.\n` +
170
+ `inventario nombra, diciendo la variable y el servicio que la espera —dónde se carga y quién lo hace, ` +
171
+ `sin proponer ningún valor—, una por cada sistema externo o MCP a conectar, y una por la autoridad del ` +
172
+ `runner, que hoy declara runner.allowPush=false.\n` +
172
173
  `5. Las preguntas que queden abiertas, en la sección Ideas de ${INBOX}, sin promover.\n` +
173
174
  `Devolvé en files cada archivo que tocaste y en assumptions cada supuesto que dejaste marcado.`,
174
175
  { schema: WRITTEN, label: 'contexto' },
package/engine/cli/ops.js CHANGED
@@ -353,29 +353,32 @@ function workspaceRoots(root) {
353
353
 
354
354
  // Qué hay en las raíces declaradas, antes de que nadie razone sobre ello. La raíz ops se saltea: no es
355
355
  // un servicio del proyecto, y su `package.json` sólo declara el motor.
356
+ // Los candidatos de una raíz, con el proyecto que vive en ella misma primero: un monolito declara sus
357
+ // comandos en el nivel de arriba, y dejarlo afuera hacía desaparecer justo al proyecto principal.
358
+ function candidates(workspace, skip = '') {
359
+ const result = SC.scan(workspace, skip)
360
+ const found = result.rootManifests.length
361
+ ? [{ path: '.', root: workspace, runtimes: ['raíz'], commands: result.rootCommands, env: result.rootEnv }]
362
+ : []
363
+ return [...found, ...result.services.map((service) => ({ ...service, root: workspace }))]
364
+ }
365
+
356
366
  function inventory(root) {
357
- const found = []
358
- for (const workspace of workspaceRoots(root)) {
359
- const result = SC.scan(workspace, root)
360
- if (result.rootManifests.length) {
361
- found.push({ path: '.', root: workspace, runtimes: ['raíz'], commands: result.rootCommands })
362
- }
363
- for (const service of result.services) found.push({ ...service, root: workspace })
364
- }
365
- return found
367
+ return workspaceRoots(root).flatMap((workspace) => candidates(workspace, root))
366
368
  }
367
369
 
368
370
  function scan(target, cli) {
369
371
  const root = path.resolve(target || '.')
370
- const result = target
371
- ? { root: path.resolve(target), services: SC.scan(path.resolve(target)).services }
372
- : { root, services: inventory(root) }
372
+ const result = { root, services: target ? candidates(root) : inventory(root) }
373
373
  if (cli.has('--json')) return console.log(JSON.stringify(result, null, 2))
374
374
  // Un monorepo de sesenta paquetes no se lee en pantalla. Se recorta, y se dice cuánto: un corte que no
375
375
  // se anuncia hace pasar lo listado por todo lo que hay.
376
376
  for (const service of result.services.slice(0, LISTA)) {
377
377
  const donde = service.root && service.root !== result.root ? `${path.basename(service.root)}/` : ''
378
- console.log(`${donde}${service.path} [${(service.runtimes || []).join(', ')}]${comandos(service.commands)}`)
378
+ const espera = service.env ? `\n espera ${service.env.names.join(', ')} (${service.env.file})` : ''
379
+ console.log(
380
+ `${donde}${service.path} [${(service.runtimes || []).join(', ')}]${comandos(service.commands)}${espera}`,
381
+ )
379
382
  }
380
383
  if (result.services.length > LISTA) {
381
384
  console.log(`… y ${result.services.length - LISTA} más, todos en --json`)
@@ -98,6 +98,35 @@ function manifestsOf(dir) {
98
98
  return MANIFESTS.filter((entry) => fs.existsSync(path.join(dir, entry.file)))
99
99
  }
100
100
 
101
+ // Qué credenciales espera un servicio, por su nombre y nada más. El ejemplo es público y versionado —el
102
+ // `.env` de verdad no se toca nunca—, y los nombres son justo lo que hace falta para dejar una fila que
103
+ // diga qué cargar; el valor no le corresponde a nadie más que a una persona.
104
+ //
105
+ // Se lee por servicio y no sólo en la raíz porque en un multirepo cada repositorio trae el suyo: leyendo
106
+ // una sola raíz, las credenciales de tres repos se volvían invisibles y las acciones humanas terminaban
107
+ // diciendo «la credencial del proveedor» en vez de nombrarla.
108
+ const ENV_EXAMPLES = ['.env.example', '.env.sample', '.env.template', '.env.dist']
109
+
110
+ // Un ejemplo con cientos de variables es un archivo generado, no un contrato: se corta y se dice.
111
+ const ENV_MAX = 40
112
+
113
+ function expectedEnv(dir) {
114
+ for (const name of ENV_EXAMPLES) {
115
+ const file = path.join(dir, name)
116
+ if (!fs.existsSync(file)) continue
117
+ let text = ''
118
+ try { text = fs.readFileSync(file, 'utf8') } catch { return null }
119
+ const names = text.split('\n')
120
+ .map((line) => line.trim())
121
+ .filter((line) => line && !line.startsWith('#'))
122
+ .map((line) => line.replace(/^export\s+/, '').split('=')[0].trim())
123
+ .filter((key) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
124
+ if (!names.length) return null
125
+ return { file: name, names: names.slice(0, ENV_MAX), truncated: Math.max(0, names.length - ENV_MAX) }
126
+ }
127
+ return null
128
+ }
129
+
101
130
  // Servicios candidatos bajo `root`, sin entrar en `skip` —típicamente la raíz ops, que no es un
102
131
  // servicio del proyecto—. El resultado es una lista, no un veredicto: decidir cuál es el producto y
103
132
  // cuál quedó muerto sigue siendo trabajo de una persona o de un cargo.
@@ -113,6 +142,7 @@ function services(root, skip = '') {
113
142
  runtimes: manifests.map((entry) => entry.runtime),
114
143
  manifests: manifests.map((entry) => entry.file),
115
144
  commands: commandsOf(dir),
145
+ env: expectedEnv(dir),
116
146
  })
117
147
  }
118
148
  if (depth >= DEPTH) return
@@ -136,8 +166,9 @@ function scan(root, skip = '') {
136
166
  root: path.resolve(root),
137
167
  rootManifests: manifestsOf(root).map((entry) => entry.file),
138
168
  rootCommands: commandsOf(root),
169
+ rootEnv: expectedEnv(root),
139
170
  services: services(root, skip),
140
171
  }
141
172
  }
142
173
 
143
- module.exports = { scan, services, IGNORED, DEPTH }
174
+ module.exports = { scan, services, expectedEnv, IGNORED, DEPTH, ENV_MAX }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",