@ingeniomaps/cauce 0.48.0 → 0.49.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,52 @@ 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.49.0] - 2026-08-26
18
+
19
+ ### Agregado
20
+
21
+ - **`R22` — lo que se mide no se toca mientras se mide.** Mientras una medición corre, el sujeto y todo
22
+ aquello contra lo que resuelve se quedan quietos. Lo difícil es que no avisa: la corrida termina y su
23
+ resultado se lee igual que uno limpio, así que quien lo reciba decide sin saber que se movió el piso.
24
+ Y entra por donde no se mira — un banco desechable puede resolver la herramienta por un enlace al
25
+ repositorio vivo, así que editar ahí cambia lo que la corrida lee sin tocar el banco. Si hace falta
26
+ trabajar igual, se trabaja donde la medición no mira; y si ya pasó, se dice qué cambió y cuándo: un
27
+ resultado cuyo entorno se movió es una hipótesis, no un veredicto.
28
+
29
+ ### Corregido
30
+
31
+ - **Un recorrido que frena da igual la salida que su contrato reserva para no poder.** `change-review`
32
+ enumera tres veredictos y el tercero es «no poder aprobar»; bloqueado, su informe parcial escribía
33
+ «No hay veredicto», que no es ninguno de los tres. Con el diff, los importadores y las pruebas a la
34
+ vista, un revisor **sí** puede firmar que no aprueba: eso es un veredicto, no su ausencia. Vale para
35
+ cualquier recorrido cuyo contrato tenga esa salida — un destino de «nada que hacer», una
36
+ recomendación de investigar.
37
+ - **`dependsOn` dejó de ser decorativo: un recorrido corre sus etapas como su contrato las declara.**
38
+ Hasta ahora el motor las corría en fila y le pasaba a cada una los handoffs de **todas** las
39
+ anteriores, sin mirar de qué decía depender. `technical-design` existe para tener tres lecturas
40
+ independientes de un mismo encuadre y no las tenía: su propio guardrail dice que las tres «no negocian
41
+ entre sí ni ajustan su hallazgo para que cierre con el de otra», y una que ya leyó a la primera no
42
+ puede cumplirlo. Ahora las etapas se agrupan por nivel de dependencia, las de un mismo nivel corren a
43
+ la vez, y cada una ve **sólo** los handoffs de aquello de lo que declara depender. Cinco de los siete
44
+ recorridos del catálogo tienen un nivel con paralelismo real y lo estrenan con esto. Una etapa que
45
+ **no** declara `dependsOn` sigue viendo todo lo anterior, igual que antes: independencia es una
46
+ afirmación, y una afirmación se declara. Si una etapa no cumple su gate, el corte es por nivel — las
47
+ que corrieron con ella entran igual al handoff, y lo que se abandona son los niveles siguientes.
48
+ - **Cambiar el `flow.json` de un recorrido ahora envejece sus veredictos.** El aviso de «el contrato
49
+ cambió y la última corrida es anterior» miraba sólo el `SKILL.md`, que un recorrido no tiene, así que
50
+ no se disparaba nunca: se le podía agregar una dimensión a un gate y sus casos aprobados seguían
51
+ leyéndose vigentes. Qué archivo **es** el contrato depende del sujeto — el de un cargo es su
52
+ `SKILL.md`, el de un recorrido su `flow.json`— y ahora se mira el que corresponde.
53
+ ## [0.49.0] - 2026-08-27
54
+
55
+ ### Corregido
56
+
57
+ - **`ops context` ya no se traga las acciones humanas cuando no hay tarea en cola.** Se imprimían
58
+ después del `return` de «sin tarea disponible», así que una instancia recién arrancada —`onboard`
59
+ deja filas pendientes y ninguna tarea todavía— preguntaba qué toca ahora y recibía «nada», cuando lo
60
+ que tocaba era que una persona desbloqueara siete cosas. Lo destapó una corrida de un recorrido sobre
61
+ su banco.
62
+
17
63
  ## [0.48.0] - 2026-08-26
18
64
 
19
65
  ### Corregido
@@ -206,16 +206,59 @@ const blocked = []
206
206
  // corre `autobuild`, y sólo después de que una persona promueva la épica al BACKLOG.
207
207
  const discovery = contract.stages.filter((stage) => stage.phase === 'discovery')
208
208
  if (!discovery.length) return stop('sin-descubrimiento', `${FLOW} no declara etapas de discovery`)
209
- for (const stage of discovery) {
210
- const previous = handoffs.length
211
- ? `Handoffs previos:\n${handoffs.map((entry) => `- ${entry.id}: ${entry.summary}`).join('\n')}`
212
- + (openConditions(handoffs).length
209
+
210
+ // Qué handoffs ve una etapa: los de aquello de lo que declara depender, y lo que aquéllos dependían.
211
+ // No los de sus hermanas. Una etapa que **no** declara `dependsOn` depende de todo lo anterior, que es
212
+ // como se comportaba esto antes: independencia es una afirmación y una afirmación se declara.
213
+ function ancestors(stage, index) {
214
+ if (!stage.dependsOn) return discovery.slice(0, index).map((one) => one.id)
215
+ const out = new Set()
216
+ const pending = [...stage.dependsOn]
217
+ while (pending.length) {
218
+ const id = pending.pop()
219
+ if (out.has(id)) continue
220
+ out.add(id)
221
+ const found = discovery.find((one) => one.id === id)
222
+ const arriba = found && found.dependsOn ? found.dependsOn : []
223
+ for (const up of arriba) pending.push(up)
224
+ }
225
+ return [...out]
226
+ }
227
+
228
+ // Los niveles del grafo: cada uno son las etapas cuyas dependencias ya cerraron, y corren a la vez.
229
+ // `technical-design` existe para tener tres lecturas independientes de un mismo encuadre, y corriéndolas
230
+ // en fila con el handoff de la anterior adentro no las tiene: su propio guardrail dice que las tres «no
231
+ // negocian entre sí ni ajustan su hallazgo para que cierre con el de otra», y una que ya leyó a la
232
+ // primera no puede cumplirlo. Lo destapó una corrida: `interface` escribió «Coincido» sobre un supuesto
233
+ // de `service` y armó su hallazgo principal sobre el K6 de `service`. El contrato lo declaraba desde el
234
+ // principio en `dependsOn` y el motor lo ignoraba.
235
+ function levels(stages) {
236
+ const out = []
237
+ const done = new Set()
238
+ let rest = stages.map((stage, index) => ({ stage, index, needs: ancestors(stage, index) }))
239
+ while (rest.length) {
240
+ const level = rest.filter((one) => one.needs.every((id) => done.has(id)))
241
+ // Una dependencia que no cierra nunca deja el resto afuera. No puede pasar —`flow check` rechaza
242
+ // una dependencia inexistente o posterior— pero un bucle que no avanza cuelga la corrida entera.
243
+ if (!level.length) { out.push(rest); break }
244
+ out.push(level)
245
+ for (const one of level) done.add(one.stage.id)
246
+ rest = rest.filter((one) => !level.includes(one))
247
+ }
248
+ return out
249
+ }
250
+
251
+ const runStage = (stage, index) => {
252
+ const visible = handoffs.filter((entry) => ancestors(stage, index).includes(entry.id))
253
+ const abiertas = openConditions(visible)
254
+ const previous = visible.length
255
+ ? `Handoffs previos:\n${visible.map((entry) => `- ${entry.id}: ${entry.summary}`).join('\n')}`
256
+ + (abiertas.length
213
257
  ? '\n\nCondiciones que dejaron las etapas anteriores y tenés que respetar:\n'
214
- + openConditions(handoffs).map((one) => `- ${one}`).join('\n')
258
+ + abiertas.map((one) => `- ${one}`).join('\n')
215
259
  : '')
216
260
  : 'Sos la primera etapa: no hay handoff previo.'
217
-
218
- const result = await agent(
261
+ return agent(
219
262
  `${RULES}\n\n${previous}\n\nActuá como ${stage.agent}, respetando su contrato en ` +
220
263
  `${stage.skill || `${WORKDIR}/agents/roles/${stage.agent}/SKILL.md`} y sus límites. ` +
221
264
  `Etapa "${stage.id}": producí ` +
@@ -235,16 +278,27 @@ for (const stage of discovery) {
235
278
  `En summary va, en 150 ` +
236
279
  `palabras o menos, lo que la etapa siguiente necesita para decidir —no un resumen de tu análisis, ` +
237
280
  `sino lo que le cambia el trabajo—, porque eso se le reenvía a cada etapa posterior.`,
238
- { schema: STAGE, label: `stage:${stage.id}` },
239
- )
240
- if (!result) return stop('stage-unavailable', `la etapa ${stage.id} no devolvió resultado`)
281
+ { schema: STAGE, label: `stage:${stage.id}` })
282
+ }
241
283
 
242
- handoffs.push({ id: stage.id, agent: stage.agent, ...result })
243
- if (result.gate === 'no-cumplido') {
244
- blocked.push({ stage: stage.id, missing: result.missing || '', action: result.humanAction || '' })
245
- log(`Gate no cumplido en ${stage.id}: ${result.missing || 'sin detalle'}`)
246
- break
284
+ // Nivel por nivel, y las de un mismo nivel a la vez. Si una de ellas no cumple su gate, el recorrido
285
+ // para: las hermanas que corrieron con ella entran igual al handoff —su trabajo está hecho y pagado— y
286
+ // lo que se abandona son los niveles siguientes.
287
+ for (const level of levels(discovery)) {
288
+ const results = await parallel(level.map((one) =>
289
+ () => runStage(one.stage, one.index).then((result) => ({ stage: one.stage, result }))))
290
+ for (const one of results) {
291
+ if (!one || !one.result) return stop('stage-unavailable', 'una etapa del nivel no devolvió resultado')
292
+ handoffs.push({ id: one.stage.id, agent: one.stage.agent, ...one.result })
293
+ }
294
+ for (const one of results) {
295
+ if (one.result.gate !== 'no-cumplido') continue
296
+ blocked.push({
297
+ stage: one.stage.id, missing: one.result.missing || '', action: one.result.humanAction || '',
298
+ })
299
+ log(`Gate no cumplido en ${one.stage.id}: ${one.result.missing || 'sin detalle'}`)
247
300
  }
301
+ if (blocked.length) break
248
302
  }
249
303
 
250
304
  if (blocked.length) {
@@ -291,6 +345,11 @@ if (blocked.length) {
291
345
  `**entrega parcial** desde el título: qué quedó establecido y con qué evidencia, qué no se pudo ` +
292
346
  `y por qué, y qué haría falta para completarlo —"${blocked[0].missing}"—. No completes con ` +
293
347
  `supuestos lo que la etapa bloqueada iba a resolver: lo que falta se nombra, no se rellena.\n\n` +
348
+ `Y si el recorrido enumera una salida para «no se pudo» —un veredicto de no poder aprobar, un ` +
349
+ `destino de «nada que hacer», una recomendación de investigar—, ésa es la respuesta y hay que ` +
350
+ `darla. Frenar no exime de la salida que el contrato reserva justo para esto: «no hay veredicto» ` +
351
+ `no es ninguna de las que el contrato enumera, y quien pidió la revisión se queda sin la única que ` +
352
+ `sí podías firmar con lo que tenías.\n\n` +
294
353
  `Eso vale para lo que necesita el insumo que falta, y sólo para eso. Lo que el material que ya ` +
295
354
  `tenés alcanza para establecer se entrega acá, aunque en el recorrido completo lo hubiera ` +
296
355
  `producido una etapa posterior: esa etapa no va a correr, así que reservárselo no se lo guarda ` +
@@ -125,8 +125,15 @@ function behaviors(root, agent, kind) {
125
125
  // La comparación es por día y no por instante porque el registro guarda fecha y no hora: un contrato
126
126
  // que cambia y se vuelve a medir el mismo día no dispara el aviso. Es el caso que menos importa
127
127
  // —quien lo cambió hoy sabe que lo cambió—, y afinar más pediría una hora que el registro no tiene.
128
- function contractChangedAt(dir) {
129
- const git = spawnSync('git', ['-C', dir, 'log', '-1', '--format=%cs', '--', 'SKILL.md'], { encoding: 'utf8' })
128
+ //
129
+ // Y qué archivo **es** el contrato depende del sujeto: el de un cargo es su `SKILL.md` y el de un
130
+ // recorrido es su `flow.json`. Mirando sólo el primero, un recorrido no disparaba el aviso nunca —no
131
+ // tiene `SKILL.md`— así que se le podía agregar una dimensión al gate y sus veredictos anteriores
132
+ // seguían leyéndose vigentes. Pasó el mismo día que esto se escribió: `change-review` ganó la pregunta
133
+ // por las superficies críticas y sus tres casos aprobados no dijeron una palabra.
134
+ function contractChangedAt(dir, kind) {
135
+ const file = kind === 'flow' ? 'flow.json' : 'SKILL.md'
136
+ const git = spawnSync('git', ['-C', dir, 'log', '-1', '--format=%cs', '--', file], { encoding: 'utf8' })
130
137
  return git.status === 0 ? (git.stdout || '').trim() : ''
131
138
  }
132
139
 
@@ -255,7 +262,7 @@ function validate(root, agent, kind) {
255
262
  if (state.failed.length) {
256
263
  warnings.push(`${state.failed.length} caso(s) no pasan: ${state.failed.join(', ')}`)
257
264
  }
258
- const cambio = contractChangedAt(subject(root, agent, kind))
265
+ const cambio = contractChangedAt(subject(root, agent, kind), kind)
259
266
  if (cambio && cambio > state.oldest) {
260
267
  const parte = state.oldest === state.newest ? 'la última corrida es' : 'el veredicto más viejo es'
261
268
  warnings.push(`el contrato cambió el ${cambio} y ${parte} del ${state.oldest}: `
@@ -205,7 +205,15 @@ function context(dir, cli) {
205
205
  const row = humanActions.find((action) => skipped.includes(action.task)) || humanActions[0]
206
206
  return console.log(`BLOCKED blocked-on-human — ${row.task}: ${row.action}`)
207
207
  }
208
- if (!report.task) return console.log('TASK (sin tarea disponible)')
208
+ // Sin tarea no se corta la salida: las acciones humanas van igual. Estaban después del `return`, así
209
+ // que una instancia recién arrancada —`onboard` deja filas pendientes y ninguna tarea todavía—
210
+ // respondía «sin tarea disponible» y se tragaba las siete cosas que una persona tenía que desbloquear.
211
+ // Es el comando que existe para decir qué toca ahora, contestando «nada» cuando lo que toca es eso.
212
+ if (!report.task) {
213
+ console.log('TASK (sin tarea disponible)')
214
+ for (const action of report.humanActions) console.log(`HUMAN ${action.task}: ${action.action}`)
215
+ return
216
+ }
209
217
  console.log(`TASK ${report.task.slug}${report.task.tier ? ` [${report.task.tier}]` : ''}` +
210
218
  `${report.task.service ? ` service: ${report.task.service}` : ''}` +
211
219
  `${report.task.hito ? ` hito: ${report.task.hito}` : ''}`)
@@ -36,10 +36,7 @@ Test Suites: 1 passed, 1 total
36
36
 
37
37
  `format.ts` no tiene test propio. `order-payload.ts` tampoco. Los seis que pasaron son del listado.
38
38
 
39
- ## De la instancia organization/company.md, «Qué no se puede romper»
39
+ ## Lo que no consta en este documento
40
40
 
41
- | Superficie | Qué se detiene si falla | A quién alcanza | Dónde vive |
42
- |---|---|---|---|
43
- | Alta de pedido | No entra ninguna venta | Todos los compradores | `src/checkout/` |
44
- | Acceso a la plataforma | Nadie puede entrar | Todos los usuarios | `src/auth/` |
45
- | Listado y reportes | Nadie consulta lo ya vendido | Operación interna | `src/pages/orders/`, `src/reports/` |
41
+ Si alguna de estas rutas está declarada como crítica, no lo dice el PR: eso vive en la instancia, en
42
+ `organization/company.md`. Quien revise tiene que ir a mirarlo — acá no está.
@@ -10,7 +10,9 @@ Te dejo el diff y el contexto de la instancia.
10
10
 
11
11
  - Notar que el helper que se toca no lo usa sólo el listado: lo importan también el resumen del pedido
12
12
  y el componente que arma el cuerpo de la orden antes de enviarla.
13
- - Nombrar qué se detiene si eso falla, con lo que la empresa ya declaró, en vez de estimarlo.
13
+ - Ir a buscar a la instancia si alguna de esas rutas está declarada como crítica, en vez de estimarlo
14
+ desde el documento del PR o darlo por sabido. Si la declaración está sin llenar, decirlo: no saberlo
15
+ no es lo mismo que no serlo.
14
16
  - Decir qué verificación pide un cambio de una línea cuando cae donde cae, y qué de eso no se hizo:
15
17
  los tests que pasaron son los del listado.
16
18
  - Cerrar con uno de los tres veredictos, sin confundir «el diff es chico» con «el riesgo es chico».
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",
@@ -2,8 +2,8 @@
2
2
 
3
3
  `system/` pertenece a Cauce y se reemplaza completo en cada actualización:
4
4
 
5
- - `system/process.md` — R1..R4, R16, R17, R20, R21: planificación, alcance, review, sincronización de estado,
6
- medición y retomar lo interrumpido.
5
+ - `system/process.md` — R1..R4, R16, R17, R20..R22: planificación, alcance, review, sincronización de
6
+ estado, medición y retomar lo interrumpido.
7
7
  - `system/code-shape.md` — R5..R7, R11, R18: simplicidad y forma del cambio.
8
8
  - `system/commits.md` — R8..R10: historia versionada y entrega.
9
9
  - `system/conduct.md` — R12..R15, R19: trato con sistemas externos, lo que llega de ellos, y la
@@ -155,3 +155,24 @@ aparecieron, cuánto se gastó. La sesión que originó esta regla creyó estar
155
155
  volvió a correr entero dos veces: siete millones de tokens para un solo veredicto, con los archivos de
156
156
  las etapas ya cumplidas a la vista en el directorio de trabajo, y con el filtro que lo evitaba escrito
157
157
  por quien reanudaba tres horas antes.
158
+
159
+ ## R22 — Lo que se mide no se toca mientras se mide
160
+
161
+ Mientras una medición corre, el sujeto y todo aquello contra lo que resuelve se quedan quietos. No se
162
+ edita el contrato que se está midiendo, ni el motor que la corrida usa, ni el entorno del que lee.
163
+
164
+ Lo que lo vuelve difícil de ver es que no avisa. La corrida termina, entrega su resultado y **ese
165
+ resultado se lee exactamente igual que uno limpio**: no hay señal que diga «esto midió dos versiones».
166
+ Quien lo reciba va a decidir sobre él sin saber que se movió el piso.
167
+
168
+ Y el camino por el que entra casi nunca es el archivo obvio. Un banco desechable puede resolver la
169
+ herramienta por un enlace al repositorio vivo, así que editar ahí cambia lo que la corrida lee sin que
170
+ nada del banco se haya tocado. La pregunta no es «¿toqué el sujeto?» sino «¿toqué algo que el sujeto
171
+ alcanza?».
172
+
173
+ Si hace falta trabajar igual, se trabaja donde la medición no mira: otra copia, otra rama sin
174
+ materializar, o se espera. Esperar es más barato que descubrir que la tanda no vale.
175
+
176
+ Y si ya pasó, se dice: qué medición, qué cambió y cuándo. Un resultado cuyo entorno se movió es una
177
+ hipótesis, no un veredicto —lo mismo que R21 nombra para lo que quedó a medias—, y guardarlo sin esa
178
+ marca es la forma cara del error, porque el número sobrevive a la sesión que sabía.