@ingeniomaps/cauce 0.87.0 → 0.89.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 +179 -0
- package/agents/roles/system/database-administrator/learning/HISTORY.md +1 -0
- package/agents/roles/system/finops-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/frontend-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/kyc-aml-specialist/learning/HISTORY.md +1 -0
- package/agents/roles/system/qa-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/release-manager/learning/HISTORY.md +1 -0
- package/agents/roles/system/security-engineer/learning/HISTORY.md +1 -0
- package/automatization/shared/eval-measured.js +1 -1
- package/automatization/shared/workflow-root.js +15 -4
- package/automatization/workflows/autobuild.js +9 -6
- package/engine/agents/learning-files.js +47 -1
- package/engine/agents/learning-seal.js +25 -9
- package/engine/agents/learning-sources.js +1 -12
- package/engine/agents/learning.js +20 -8
- package/engine/automation/index.js +16 -0
- package/engine/automation/rules.js +99 -5
- package/engine/automation/runners.js +7 -3
- package/engine/cli/catalog.js +15 -1
- package/engine/cli/claims.js +9 -3
- package/engine/cli/io.js +7 -1
- package/engine/cli/ops.js +3 -2
- package/engine/cli/planning.js +21 -193
- package/engine/cli/validate.js +205 -0
- package/engine/planning/contracts.js +54 -0
- package/engine/planning/state.js +14 -4
- package/package.json +1 -1
- package/template/planning/PROTOCOL.md +5 -0
- package/template/planning/rules/README.md +31 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// instalar. Y como `upgrade` no reinstala, `check` y `doctor` comparan lo instalado con lo vigente.
|
|
7
7
|
|
|
8
8
|
const fs = require('node:fs')
|
|
9
|
+
const path = require('node:path')
|
|
9
10
|
const F = require('../core/files')
|
|
10
11
|
const O = require('../core/ownership')
|
|
11
12
|
|
|
@@ -20,14 +21,100 @@ const START_AT = '<!-- cauce:reglas inicio'
|
|
|
20
21
|
// Lo que un `CLAUDE.md` o un `GEMINI.md` instalado antes de 0.82.0 trae en el lugar del bloque.
|
|
21
22
|
const LEGACY_IMPORT = /^@\S*planning\/rules\/\S+\.md\s*$/
|
|
22
23
|
|
|
24
|
+
// Una regla que declara `aplica:` rige igual, y se carga sólo cuando se toca esa superficie. Lo que se
|
|
25
|
+
// ahorra está medido: el contexto de arranque de **cada** agente son ~16 K tokens sólo de lo que Cauce
|
|
26
|
+
// pone, y una regla que importa en una tarea de cada cien se leía cien veces (caso 141).
|
|
27
|
+
//
|
|
28
|
+
// El default es lo que vuelve seguro esto: sin el campo, la regla se carga como siempre. Al revés —pedir
|
|
29
|
+
// que declare `siempre` para seguir cargándose— cada instancia habría perdido sus reglas propias en el
|
|
30
|
+
// próximo `upgrade`, una por una y sin que nada lo dijera.
|
|
31
|
+
//
|
|
32
|
+
// `effectiveRules` no cambia: sigue devolviendo todo lo que rige, que es lo que `ops context` le entrega
|
|
33
|
+
// al recorrido. Acá sólo se decide qué se **carga**, y por eso la partición vive de este lado.
|
|
34
|
+
const SURFACE = /^aplica:[ \t]*(\S.*?)[ \t]*$/m
|
|
35
|
+
function surfaceOf(root, file) {
|
|
36
|
+
try {
|
|
37
|
+
const text = fs.readFileSync(path.join(root, file), 'utf8')
|
|
38
|
+
// Sólo en el frontmatter: un `aplica:` en la prosa de una regla es una frase, no una declaración.
|
|
39
|
+
if (!text.startsWith('---')) return ''
|
|
40
|
+
const end = text.indexOf('\n---', 3)
|
|
41
|
+
return end === -1 ? '' : ((text.slice(0, end).match(SURFACE) || [])[1] || '')
|
|
42
|
+
} catch { return '' }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Las que se cargan y las que sólo se nombran. Las dos listas salen de la misma lectura para que el
|
|
46
|
+
// bloque que se escribe y el aviso que lo audita no puedan discrepar: con dos recorridos, `doctor`
|
|
47
|
+
// reportaría como faltante la que a propósito no se carga, para siempre y sin forma de callarlo.
|
|
48
|
+
function split(root) {
|
|
49
|
+
const loaded = []
|
|
50
|
+
const named = []
|
|
51
|
+
for (const file of O.effectiveRules(root)) {
|
|
52
|
+
const surface = surfaceOf(root, file)
|
|
53
|
+
if (surface) named.push({ file, surface })
|
|
54
|
+
else loaded.push(file)
|
|
55
|
+
}
|
|
56
|
+
return { loaded, named }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Cuánto pesa lo que el bloque carga, en bytes. En bytes y no en tokens a propósito: los bytes los mide
|
|
60
|
+
// el motor y se pueden comprobar, mientras que la equivalencia en tokens depende del modelo y del
|
|
61
|
+
// tokenizador. Un número estimado en una salida que alguien cita para decidir vale menos que uno exacto.
|
|
62
|
+
//
|
|
63
|
+
// Sólo `loaded`: una regla declarada por superficie no viaja en el arranque, y contarla haría que
|
|
64
|
+
// declararla no sirviera de nada. Vive acá —y no en quien lo imprime— porque lo miran dos, `install` al
|
|
65
|
+
// instalar y `check` en cada corrida, y con dos cuentas separadas se contradicen el día que alguien toque
|
|
66
|
+
// una sola.
|
|
67
|
+
function weight(root) {
|
|
68
|
+
const { loaded } = split(root)
|
|
69
|
+
let bytes = 0
|
|
70
|
+
const files = []
|
|
71
|
+
for (const file of loaded) {
|
|
72
|
+
try {
|
|
73
|
+
const size = fs.statSync(path.join(root, file)).size
|
|
74
|
+
bytes += size
|
|
75
|
+
files.push({ file, size })
|
|
76
|
+
} catch { /* la que no está en disco ya la reporta `check` por su lado */ }
|
|
77
|
+
}
|
|
78
|
+
return { count: loaded.length, bytes, files: files.sort((a, b) => b.size - a.size) }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const KB = (bytes) => `${(bytes / 1024).toFixed(1)} KB`
|
|
82
|
+
|
|
83
|
+
// A partir de dónde el peso deja de ser el costo de arrancar y pasa a ser una decisión que conviene mirar.
|
|
84
|
+
// El piso que Cauce impone —las cuatro reglas del sistema— son 38,3 KB, así que un umbral por debajo de
|
|
85
|
+
// eso avisaría en toda instancia recién creada y se apagaría por ruido el primer día: eso descartó los
|
|
86
|
+
// 60 KB que el caso 141 proponía. 64 KB deja ~26 KB para lo propio, que son varias reglas de tamaño
|
|
87
|
+
// normal, antes de que el aviso hable.
|
|
88
|
+
const HEAVY = 64 * 1024
|
|
89
|
+
|
|
90
|
+
// La línea que declara el peso, para que la digan igual `install` y `check`. Nombra las dos más grandes
|
|
91
|
+
// porque es lo accionable: saber que el bloque pesa no dice cuál conviene declarar por superficie.
|
|
92
|
+
function weightLine(root) {
|
|
93
|
+
const { count, bytes, files } = weight(root)
|
|
94
|
+
const top = files.slice(0, 2).map((one) => path.basename(one.file)).join(', ')
|
|
95
|
+
return `el bloque de reglas carga ${count} archivo(s), ${KB(bytes)} en cada agente`
|
|
96
|
+
+ (top ? ` (las más grandes: ${top})` : '')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Sólo cuando pasó el umbral. Devuelve lista porque es lo que `check` empalma con el resto de avisos.
|
|
100
|
+
function heavyRules(root) {
|
|
101
|
+
return weight(root).bytes > HEAVY ? [weightLine(root)] : []
|
|
102
|
+
}
|
|
103
|
+
|
|
23
104
|
// Sin raíz el marcador queda como está —así lo leen las pruebas que revisan el texto de un adaptador—: un
|
|
24
105
|
// bloque vacío se leería igual que un proyecto sin reglas, y un marcador sin resolver se ve.
|
|
25
106
|
function fill(text, root) {
|
|
26
107
|
if (!root || !text.includes('{{RULES:')) return text
|
|
27
|
-
const
|
|
28
|
-
return text.replace(MARKER, (_, format) => [
|
|
29
|
-
|
|
30
|
-
: `- \`{{OPS_DIR}}${file}\``)),
|
|
108
|
+
const { loaded, named } = split(root)
|
|
109
|
+
return text.replace(MARKER, (_, format) => [
|
|
110
|
+
START,
|
|
111
|
+
...loaded.map((file) => (format === 'imports' ? `@{{OPS_DIR}}${file}` : `- \`{{OPS_DIR}}${file}\``)),
|
|
112
|
+
// Nombrada y no cargada: pesa una línea en vez de su archivo entero, y quien la lea sabe qué la
|
|
113
|
+
// dispara. Va en el mismo bloque a propósito — fuera de él, una sesión que no corre un recorrido no
|
|
114
|
+
// se enteraría de que existe, y una regla que nadie sabe que existe es una que no rige.
|
|
115
|
+
...named.map((one) => `- \`{{OPS_DIR}}${one.file}\` (aplica: ${one.surface})`),
|
|
116
|
+
END,
|
|
117
|
+
].join('\n'))
|
|
31
118
|
}
|
|
32
119
|
|
|
33
120
|
function blockOf(text) {
|
|
@@ -69,6 +156,13 @@ function drift(root, name) {
|
|
|
69
156
|
const { runnerManifest, runnerPaths, resolveItem } = require('./runners')
|
|
70
157
|
const runner = runnerManifest(root, name)
|
|
71
158
|
const paths = runnerPaths(root, name, runner)
|
|
159
|
+
// Contra todo lo que rige, y no contra la partición: una regla declarada por superficie igual aparece
|
|
160
|
+
// en el bloque —nombrada en vez de importada—, y `listed` extrae la ruta de las dos formas. Así que el
|
|
161
|
+
// audit la ve y no la extraña, sin que este lado tenga que saber que la partición existe.
|
|
162
|
+
//
|
|
163
|
+
// Se intentó filtrar acá por simetría con `fill` y no cambiaba nada: la mutación que lo revertía dejaba
|
|
164
|
+
// las pruebas en verde. Queda dicho porque la simetría es tentadora y el código que no altera ninguna
|
|
165
|
+
// conducta se lee como si sostuviera algo.
|
|
72
166
|
const expected = O.effectiveRules(root)
|
|
73
167
|
const found = []
|
|
74
168
|
for (const item of [...(runner.instructions || []), ...(runner.artifacts || [])]) {
|
|
@@ -119,4 +213,4 @@ function staleLines(root) {
|
|
|
119
213
|
return lines
|
|
120
214
|
}
|
|
121
215
|
|
|
122
|
-
module.exports = { fill, refresh, drift, driftLine, staleLines }
|
|
216
|
+
module.exports = { fill, refresh, drift, driftLine, staleLines, split, weightLine, heavyRules }
|
|
@@ -122,9 +122,13 @@ function inline(text, automationRoot) {
|
|
|
122
122
|
})
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
// `{{OPS_ROOT}}` es la raíz absoluta
|
|
126
|
-
//
|
|
127
|
-
//
|
|
125
|
+
// `{{OPS_ROOT}}` es la raíz absoluta, y la lleva quien no puede deducirla de dónde lo ejecutaron: el
|
|
126
|
+
// puente de Antigravity, los hooks de Codex y —desde 0.89.0— los recorridos, que dictan comandos a un
|
|
127
|
+
// agente cuyo directorio nadie promete (caso 139).
|
|
128
|
+
//
|
|
129
|
+
// No reemplaza a `{{OPS_DIR}}` en general, y el costo dice por qué: un archivo que lleva la raíz escrita
|
|
130
|
+
// se rompe si el proyecto se mueve de carpeta, y se repara reinstalando el adaptador. Lo paga el que no
|
|
131
|
+
// tiene alternativa; lo que se resuelve contra la carpeta donde se abre la herramienta sigue con el otro.
|
|
128
132
|
const OPS_ROOT = '{{OPS_ROOT}}'
|
|
129
133
|
|
|
130
134
|
// `{{RULES:…}}` va antes que `{{OPS_DIR}}` por lo mismo que el include: las rutas que escribe llevan el prefijo.
|
package/engine/cli/catalog.js
CHANGED
|
@@ -7,6 +7,7 @@ const fs = require('node:fs')
|
|
|
7
7
|
const path = require('node:path')
|
|
8
8
|
const { spawnSync } = require('node:child_process')
|
|
9
9
|
const L = require('../agents/learning')
|
|
10
|
+
const LF = require('../agents/learning-files')
|
|
10
11
|
const AG = require('../agents/catalog')
|
|
11
12
|
const EV = require('../agents/evaluations')
|
|
12
13
|
const T = require('../flows/registry')
|
|
@@ -242,9 +243,15 @@ function learn(agent, cli) {
|
|
|
242
243
|
if (cli.has('--archived')) {
|
|
243
244
|
const result = L.archive(opsRoot(), agent, cli.value('--period'), kind)
|
|
244
245
|
const relative = path.relative(opsRoot(), result.file)
|
|
246
|
+
// Los dos archivados no son lo mismo y el mensaje lo dice: uno es una decisión —se miró y no
|
|
247
|
+
// cambia nada— y el otro es tirar un andamio que nadie llegó a llenar. Afirmar el primero sobre
|
|
248
|
+
// el segundo le cuenta a quien archiva que hubo una revisión que no hubo.
|
|
249
|
+
const porque = result.blank
|
|
250
|
+
? 'nadie decidió el cambio y el documento quedó con el molde'
|
|
251
|
+
: 'se miró y no cambia nada'
|
|
245
252
|
return console.log(result.already
|
|
246
253
|
? `= ${relative} ya estaba archivada`
|
|
247
|
-
: `✓ ${relative} queda archivada:
|
|
254
|
+
: `✓ ${relative} queda archivada: ${porque}`)
|
|
248
255
|
}
|
|
249
256
|
if (cli.has('--applied')) {
|
|
250
257
|
const result = L.seal(opsRoot(), agent, cli.value('--period'), kind)
|
|
@@ -282,6 +289,13 @@ function learn(agent, cli) {
|
|
|
282
289
|
? ` ${result.reports} corrida(s) consolidada(s), ${result.findings} hallazgo(s)`
|
|
283
290
|
: ` ${result.reports} informe(s) semanal(es) incluidos`)
|
|
284
291
|
}
|
|
292
|
+
// Lo lee quien automatiza el ciclo para no pedir una firma por un documento que no decide nada, y
|
|
293
|
+
// también quien lo corre a mano: sin esta línea el archivo se ve terminado y no lo está. Se pregunta
|
|
294
|
+
// sobre el documento y no sobre lo que devolvió el motor, porque `prepareProposal` sale por cinco
|
|
295
|
+
// lugares y sólo uno compone: puesto en ése, el dato falta en los otros cuatro.
|
|
296
|
+
if (LF.blankProposal(result.file)) {
|
|
297
|
+
console.log(' sin cambio decidido: falta correr agent-propose antes de que esto se pueda firmar')
|
|
298
|
+
}
|
|
285
299
|
} catch (error) { fail(error.message, 2) }
|
|
286
300
|
}
|
|
287
301
|
|
package/engine/cli/claims.js
CHANGED
|
@@ -76,9 +76,15 @@ function claim(dir, slug, cli) {
|
|
|
76
76
|
+ `${CL.DIR}/${slug}.md a mano: soltar lo de otro es una decisión, no un comando.`)
|
|
77
77
|
}
|
|
78
78
|
console.log(`✓ ${slug} tomada por ${me}`)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
if (!cli.has('--json')) {
|
|
80
|
+
// Un reclamo sin empujar no protege de nada: el otro runner lee lo que hay en su copia. Decirlo acá
|
|
81
|
+
// es lo único que separa «tomé la tarea» de «creí que la había tomado».
|
|
82
|
+
console.log(` commiteá y empujá ${CL.DIR}/${slug}.md para que el equipo lo vea`)
|
|
83
|
+
// Y con qué id volver. Este dato lo imprimía `ops worktree` al crear el árbol; en sidecar no se crea
|
|
84
|
+
// ninguno, así que ese comando no corre nunca y el id no quedaba escrito en el único momento en que
|
|
85
|
+
// alguien lo tiene delante: ahora, que se acaba de guardar en el reclamo.
|
|
86
|
+
console.log(` export CAUCE_RUNNER=${from}`)
|
|
87
|
+
}
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
function release(dir, slug) {
|
package/engine/cli/io.js
CHANGED
|
@@ -10,6 +10,12 @@ function fail(message, code = 1) {
|
|
|
10
10
|
process.exit(code)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// La fecha de hoy, en un solo lugar: los comandos que la usan tienen que estar mirando el mismo día, y
|
|
14
|
+
// el módulo que calcula vencimientos la recibe en vez de preguntarla. Vive acá desde que la puerta de
|
|
15
|
+
// planning se separó de los comandos que la leen — quedaba en el archivo que se partió, y dejar una copia
|
|
16
|
+
// a cada lado habría roto en silencio lo único que esta función promete.
|
|
17
|
+
const TODAY = () => new Date().toISOString().slice(0, 10)
|
|
18
|
+
|
|
13
19
|
// La raíz ops de un comando que no la recibe. El shim `tools/ops.js` la exporta porque sabe dónde
|
|
14
20
|
// vive: sin eso, invocarlo desde otra carpeta —lo normal en sidecar— la resolvía contra el cwd.
|
|
15
21
|
function opsRoot(dir) {
|
|
@@ -44,4 +50,4 @@ function planningRoot(dir) {
|
|
|
44
50
|
return root
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
module.exports = { fail, opsRoot, planningRoot }
|
|
53
|
+
module.exports = { fail, opsRoot, planningRoot, TODAY }
|
package/engine/cli/ops.js
CHANGED
|
@@ -9,6 +9,7 @@ const { FLAGS, parse } = require('./args')
|
|
|
9
9
|
const { fail } = require('./io')
|
|
10
10
|
const IN = require('./instance')
|
|
11
11
|
const PL = require('./planning')
|
|
12
|
+
const VA = require('./validate')
|
|
12
13
|
const AR = require('./archive')
|
|
13
14
|
const CLM = require('./claims')
|
|
14
15
|
const WT = require('./worktree')
|
|
@@ -124,7 +125,7 @@ async function init(target, cli) {
|
|
|
124
125
|
})
|
|
125
126
|
} catch (error) { fail(error.message, 2) }
|
|
126
127
|
|
|
127
|
-
if (result.installed)
|
|
128
|
+
if (result.installed) VA.check(path.join(root, 'planning'), NO_FLAGS)
|
|
128
129
|
|
|
129
130
|
// Una instancia recién instalada funciona y no sabe nada de este proyecto: `organization/` es el molde
|
|
130
131
|
// y el roadmap está vacío. Llenarlo exige leer el repositorio y decidir qué es cada cosa, que es justo
|
|
@@ -204,7 +205,7 @@ async function run(cli) {
|
|
|
204
205
|
if (command === 'init') await init(arg[1], cli)
|
|
205
206
|
else if (command === 'scan') W.scan(arg[1], cli)
|
|
206
207
|
else if (command === 'onboard') W.onboard(arg[1], cli)
|
|
207
|
-
else if (command === 'check')
|
|
208
|
+
else if (command === 'check') VA.check(arg[1], cli)
|
|
208
209
|
else if (command === 'tree') PL.tree(arg[1], cli)
|
|
209
210
|
else if (command === 'context') PL.context(arg[1], cli)
|
|
210
211
|
else if (command === 'recurring') PL.recurring(arg[1], cli)
|
package/engine/cli/planning.js
CHANGED
|
@@ -1,32 +1,20 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
// Los comandos que leen
|
|
4
|
-
//
|
|
3
|
+
// Los comandos que leen un planning y contestan sobre él: qué hay, qué toca ahora, qué vence y con qué
|
|
4
|
+
// se cerró una tarea. Todos trabajan sobre el mismo estado, que `planning/state.js` compone una vez.
|
|
5
|
+
//
|
|
6
|
+
// La puerta no está acá: vive en `validate.js` porque decide en vez de informar, y con ella se fueron
|
|
7
|
+
// quince de los veintiún módulos que este archivo importaba.
|
|
5
8
|
|
|
6
9
|
const fs = require('node:fs')
|
|
7
10
|
const path = require('node:path')
|
|
8
11
|
const P = require('../planning/parser')
|
|
9
|
-
const B = require('../planning/business-rules')
|
|
10
|
-
const PC = require('../planning/contracts')
|
|
11
|
-
const SR = require('../planning/structure')
|
|
12
|
-
const SZ = require('../planning/sizing')
|
|
13
12
|
const RC = require('../planning/recurring')
|
|
14
|
-
const IB = require('../planning/inbox')
|
|
15
13
|
const CL = require('../planning/claims')
|
|
16
|
-
const R = require('../core/repos')
|
|
17
14
|
const ST = require('../planning/state')
|
|
18
|
-
const AD = require('../planning/adoption')
|
|
19
|
-
const AP = require('../hooks/approval')
|
|
20
|
-
const I = require('../integrations/registry')
|
|
21
15
|
const O = require('../core/ownership')
|
|
22
16
|
const EV = require('../core/evidence')
|
|
23
|
-
const
|
|
24
|
-
const OB = require('../core/onboarding')
|
|
25
|
-
const C = require('../config/validate')
|
|
26
|
-
const CP = require('../config/paths')
|
|
27
|
-
const AG = require('../agents/catalog')
|
|
28
|
-
const RL = require('../automation/rules')
|
|
29
|
-
const { fail, planningRoot } = require('./io')
|
|
17
|
+
const { fail, planningRoot, TODAY } = require('./io')
|
|
30
18
|
|
|
31
19
|
// Qué dimensiones enumera el molde de `organization/` y cuáles dejaron de estar. Un agente que reescribe
|
|
32
20
|
// esos archivos tiende a quedarse con el contenido y perder la estructura: el resultado se lee entero y
|
|
@@ -74,178 +62,6 @@ function evidence(dir, cli) {
|
|
|
74
62
|
+ 'que la prueba nombrada haya corrido: eso depende del runner, y varios no la nombran al pasar.')
|
|
75
63
|
}
|
|
76
64
|
|
|
77
|
-
// La fecha de hoy, en un solo lugar: los comandos que la usan tienen que estar mirando el mismo día, y
|
|
78
|
-
// el módulo que calcula vencimientos la recibe en vez de preguntarla.
|
|
79
|
-
const TODAY = () => new Date().toISOString().slice(0, 10)
|
|
80
|
-
|
|
81
|
-
function check(dir, cli) {
|
|
82
|
-
const root = planningRoot(dir)
|
|
83
|
-
const errors = []
|
|
84
|
-
const warnings = []
|
|
85
|
-
// El plan no está: `wip/` es local y gitignoreado, así que un clon nuevo no lo trae y eso no es un
|
|
86
|
-
// error. Ausente se lee como IDLE, que es lo que significa.
|
|
87
|
-
const required = ['BACKLOG.md', 'INBOX.md', 'HUMAN_ACTIONS.md', 'PROTOCOL.md']
|
|
88
|
-
// `DONE.md` se retiró: la evidencia vive en un archivo por tarea. Un `DONE.md` que quede en disco
|
|
89
|
-
// ya no lo lee nadie, y eso no se nota — las épicas dejan de poder cerrar y sus historias figuran
|
|
90
|
-
// sin evidencia, que es lo mismo que se vería si nunca se hubieran hecho.
|
|
91
|
-
// `WIP.md` se retiró por lo mismo que `DONE.md`: era uno solo y lo escribían todos los que corren sobre
|
|
92
|
-
// una instancia sidecar. Uno que quede en disco ya no lo lee nadie, y su plan a medias se pierde sin
|
|
93
|
-
// que nada lo diga.
|
|
94
|
-
if (fs.existsSync(path.join(root, 'WIP.md'))) {
|
|
95
|
-
errors.push('WIP.md ya no se lee: el plan de cada runner vive en `wip/<runner>.md`; movelo y borralo')
|
|
96
|
-
}
|
|
97
|
-
if (fs.existsSync(path.join(root, 'DONE.md'))) {
|
|
98
|
-
errors.push('DONE.md ya no se lee: pasá cada entrada a su propio `done/<slug>.md` y borralo')
|
|
99
|
-
}
|
|
100
|
-
for (const file of required) if (!fs.existsSync(path.join(root, file))) errors.push(`falta ${file}`)
|
|
101
|
-
|
|
102
|
-
const configPath = path.join(root, '..', 'ops.config.json')
|
|
103
|
-
let config = null
|
|
104
|
-
if (fs.existsSync(configPath)) {
|
|
105
|
-
try {
|
|
106
|
-
const raw = fs.readFileSync(configPath, 'utf8')
|
|
107
|
-
config = JSON.parse(raw)
|
|
108
|
-
if (!raw.includes('{{')) {
|
|
109
|
-
errors.push(...C.validateOpsConfig(config))
|
|
110
|
-
warnings.push(...C.configWarnings(config))
|
|
111
|
-
if (Array.isArray(config.workspaceRoots)) {
|
|
112
|
-
for (const workspace of config.workspaceRoots) {
|
|
113
|
-
if (workspace && workspace.name && workspace.path
|
|
114
|
-
&& !fs.existsSync(path.resolve(path.dirname(configPath), workspace.path))) {
|
|
115
|
-
errors.push(`ops.config.json: no existe la raíz ${workspace.name} (${workspace.path})`)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
// Es la única parte de la configuración que le levanta el límite a un guard, y quien la escribió
|
|
120
|
-
// no es quien la lee dentro de seis meses: va como advertencia permanente, igual que un override.
|
|
121
|
-
// Y se muestra resuelta porque resuelta es como la compara el guard — un `~` escrito solo exenta
|
|
122
|
-
// la casa entera, y escrito no se nota.
|
|
123
|
-
for (const exempt of CP.writableOutsideRoots(path.dirname(configPath), config)) {
|
|
124
|
-
warnings.push(`ops.config.json: ${exempt.declared} está exenta del límite `
|
|
125
|
-
+ `de raíces (${exempt.path})`)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} catch (error) {
|
|
129
|
-
errors.push(`ops.config.json: JSON inválido (${error.message})`)
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
warnings.push(`no existe ${path.relative(root, configPath)}`)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const epics = P.readEpics(root)
|
|
136
|
-
const milestones = P.readBacklog(root)
|
|
137
|
-
const done = P.readDone(root)
|
|
138
|
-
errors.push(...B.validate(path.join(root, 'business-rules')))
|
|
139
|
-
errors.push(...SR.validateRoadmapStructure(root))
|
|
140
|
-
errors.push(...SR.validateBacklogStructure(root))
|
|
141
|
-
errors.push(...SR.validateRules(root))
|
|
142
|
-
errors.push(...SR.validateAdr(root))
|
|
143
|
-
const backlog = milestones.flatMap((milestone) => milestone.tasks)
|
|
144
|
-
const backlogSlugs = new Set(backlog.map((task) => task.slug))
|
|
145
|
-
const epicNums = new Set()
|
|
146
|
-
const storySlugs = new Set()
|
|
147
|
-
|
|
148
|
-
const roles = new Set(AG.list(path.resolve(root, '..')).map((role) => role.slug))
|
|
149
|
-
const wips = P.readWips(root)
|
|
150
|
-
const adopted = AD.read(root)
|
|
151
|
-
errors.push(...SZ.oversizedUnits({ epics, milestones }))
|
|
152
|
-
errors.push(...PC.validateState({
|
|
153
|
-
epics, milestones, done, wips, roles, humanActions: P.readHumanActions(root), adopted: new Set(adopted),
|
|
154
|
-
}))
|
|
155
|
-
warnings.push(...AD.report({ done, epics, adopted }))
|
|
156
|
-
warnings.push(...PC.doneCeremonyWarnings(done, new Set(adopted)))
|
|
157
|
-
warnings.push(...R.coverageWarnings(path.resolve(root, '..'), done))
|
|
158
|
-
// Sin `RECURRING.md` no dice una palabra: una instancia que actualiza y no declara trabajo recurrente
|
|
159
|
-
// no tiene por qué enterarse de que el contrato existe. Vencida avisa y no frena — lo que frena vive
|
|
160
|
-
// en `HUMAN_ACTIONS.md`, y un aviso que salta siempre se termina apagando.
|
|
161
|
-
// Un reclamo que nombra una tarea que no existe bloquea la cola sin que nada lo explique, y uno viejo
|
|
162
|
-
// la bloquea para siempre. Lo primero es error; lo segundo avisa, porque abandonar no es un defecto.
|
|
163
|
-
const claims = CL.read(root)
|
|
164
|
-
errors.push(...CL.validate({ claims, milestones, done }))
|
|
165
|
-
// Si la rama de cada tarea tomada se movió, que es lo único barato que distingue una tarea larga de
|
|
166
|
-
// una abandonada. Sin repositorio resoluble el mapa queda vacío y el aviso vuelve a mirar sólo la
|
|
167
|
-
// fecha, que es lo que había antes: degrada, no rompe.
|
|
168
|
-
const activity = new Map()
|
|
169
|
-
for (const claim of claims.filter((one) => !done.set.has(one.slug))) {
|
|
170
|
-
const at = R.lastCommit(R.repoOf(path.join(root, '..'), claim.service), CL.branchOf(claim.slug))
|
|
171
|
-
if (at) activity.set(claim.slug, at)
|
|
172
|
-
}
|
|
173
|
-
warnings.push(...CL.warnings({ claims, done, today: TODAY(), activity }))
|
|
174
|
-
const recurring = RC.read(root)
|
|
175
|
-
errors.push(...RC.validate(recurring))
|
|
176
|
-
warnings.push(...RC.warnings(RC.status({ ...recurring, done, today: TODAY() })))
|
|
177
|
-
warnings.push(...IB.warnings(root, done, config))
|
|
178
|
-
warnings.push(...AD.sealWarnings(root))
|
|
179
|
-
warnings.push(...R.unrecordedHumanActions(path.resolve(root, '..'), P.readHumanActions(root)))
|
|
180
|
-
warnings.push(...AP.warnings(path.resolve(root, '..')))
|
|
181
|
-
warnings.push(...TR.warnings(path.resolve(root, '..')))
|
|
182
|
-
|
|
183
|
-
// Lo que `upgrade` conserva por estar editado deja de recibir mejoras, y eso es una deuda que no
|
|
184
|
-
// avisa sola: la instancia queda con medio molde viejo y todo se ve normal. Sale acá para que se vea
|
|
185
|
-
// en cada corrida y no sólo el día que alguien actualiza.
|
|
186
|
-
const congelados = O.localChanges(path.resolve(root, '..'))
|
|
187
|
-
if (congelados.length) {
|
|
188
|
-
warnings.push(`${congelados.length} archivo(s) del molde congelados por edición local; `
|
|
189
|
-
+ '`upgrade` los conserva y no les trae mejoras')
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Y lo que `upgrade` no retiró porque no pudo demostrar que fuera suyo: queda ahí, sin colgar de
|
|
193
|
-
// ningún mecanismo, hasta que alguien lo mueva o lo borre. Se cuenta por lo mismo que los congelados
|
|
194
|
-
// — un resto que no se ve se vuelve permanente.
|
|
195
|
-
const restos = O.RETIRED_COMPARTIDO.filter((relative) => fs.existsSync(path.join(root, '..', relative)))
|
|
196
|
-
if (restos.length) {
|
|
197
|
-
warnings.push(`${restos.length} ruta(s) retiradas siguen en disco con contenido tuyo `
|
|
198
|
-
+ `(${restos.join(', ')}); Cauce ya no las distribuye ni las toca`)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const integration = I.validate(path.resolve(root, '..'))
|
|
202
|
-
errors.push(...integration.errors)
|
|
203
|
-
warnings.push(...integration.warnings)
|
|
204
|
-
|
|
205
|
-
warnings.push(...SR.competingSections(root))
|
|
206
|
-
// Sobrescribir una entrada de system/ es legítimo y esperado; lo que no puede pasar es que
|
|
207
|
-
// ocurra en silencio, porque esa entrada deja de recibir las mejoras del toolkit.
|
|
208
|
-
for (const override of O.overrides(path.resolve(root, '..'))) {
|
|
209
|
-
// Y con qué se queda el proyecto: un override sano redefine lo que reemplaza, y el que deja IDs
|
|
210
|
-
// afuera los retira sin decirlo. Nombrarlos es lo único que separa una decisión de un descuido.
|
|
211
|
-
const retired = override.collection === 'planning/rules'
|
|
212
|
-
? SR.retiredByOverride(root, override.project)
|
|
213
|
-
: []
|
|
214
|
-
warnings.push(`${override.collection}/${override.project} sobrescribe ${override.system} `
|
|
215
|
-
+ `(override explícito)${retired.length ? `; deja de regir ${retired.join(', ')}` : ''}`)
|
|
216
|
-
}
|
|
217
|
-
// Y lo que instaló cada runner, contra esas mismas reglas (caso 099).
|
|
218
|
-
warnings.push(...RL.staleLines(path.resolve(root, '..')))
|
|
219
|
-
// Misma regla para los cargos, que es donde más caro sale: un fork se hace una vez y se olvida.
|
|
220
|
-
const FK = require('../agents/fork')
|
|
221
|
-
for (const entry of FK.drift(path.resolve(root, '..'))) warnings.push(FK.driftLine(entry))
|
|
222
|
-
|
|
223
|
-
warnings.push(...OB.missingSections(path.resolve(root, '..')))
|
|
224
|
-
warnings.push(...OB.orphanCredentials(path.resolve(root, '..')))
|
|
225
|
-
|
|
226
|
-
if (cli.has('--json')) {
|
|
227
|
-
console.log(JSON.stringify({
|
|
228
|
-
ok: !errors.length,
|
|
229
|
-
epics: epics.length,
|
|
230
|
-
queued: backlog.length,
|
|
231
|
-
done: done.entries.length,
|
|
232
|
-
wips: wips.map((one) => one.task),
|
|
233
|
-
errors,
|
|
234
|
-
warnings,
|
|
235
|
-
}))
|
|
236
|
-
if (errors.length) process.exit(1)
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
for (const warning of warnings) console.warn(`⚠ ${warning}`)
|
|
241
|
-
for (const error of errors) console.error(`✗ ${error}`)
|
|
242
|
-
if (errors.length) fail(`\n${errors.length} error(es), ${warnings.length} advertencia(s)`)
|
|
243
|
-
console.log(
|
|
244
|
-
`✓ planning válido: ${epics.length} épica(s), ${backlog.length} tarea(s) en cola, ` +
|
|
245
|
-
`${done.entries.length} terminada(s)`,
|
|
246
|
-
)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
65
|
// Estado observable de planning sin mutar nada; base común de `tree` y de sus salidas.
|
|
250
66
|
function treeJson({ epics, milestones, done, wips, inbox, queued, claims }) {
|
|
251
67
|
const state = (slug) => done.set.has(slug) ? 'done' : queued.has(slug) ? 'queued' : 'pending'
|
|
@@ -407,6 +223,18 @@ function context(dir, cli) {
|
|
|
407
223
|
// Tu propio nombre en una tarea «ajena» es la señal de que sos vos desde otro runner, y sin decirlo se
|
|
408
224
|
// lee como que alguien te ganó la tarea.
|
|
409
225
|
const dueño = (one) => (one.owner === report.owner ? `${one.owner} — vos, desde otro runner` : one.owner)
|
|
226
|
+
// Que el reclamo sea tuyo desde otro id ya se decía; que además haya un **plan escrito** bajo ese id, no.
|
|
227
|
+
// Esa es la mitad que cuesta la sesión: los pasos ya hechos están en un archivo que nadie nombra, y el id
|
|
228
|
+
// que lo recupera es justo el que esta sesión no supo deducir. Los dos datos están en el reclamo.
|
|
229
|
+
const tomadas = () => {
|
|
230
|
+
for (const one of report.taken) {
|
|
231
|
+
console.log(`TAKEN ${one.slug} (${dueño(one)})`)
|
|
232
|
+
if (one.owner === report.owner && one.wip) {
|
|
233
|
+
console.log(`PLAN ${one.slug}: su plan está en wip/${one.wip} — retomalo con `
|
|
234
|
+
+ `\`export CAUCE_RUNNER=${one.runner}\``)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
410
238
|
const espera = () => {
|
|
411
239
|
for (const one of report.waiting) {
|
|
412
240
|
console.log(`WAIT ${one.slug}: espera a ${one.dep}${one.owner ? ` (${one.owner})` : ''}`)
|
|
@@ -424,7 +252,7 @@ function context(dir, cli) {
|
|
|
424
252
|
if (next) console.log(`EPIC ${next.num}: ${next.title} — sin promover`)
|
|
425
253
|
// Mismo motivo que `blocked` arriba, con otra causa: acá la cola no la traba una persona, la tiene
|
|
426
254
|
// el equipo, y lo que corresponde es hablar con quien la tiene.
|
|
427
|
-
|
|
255
|
+
tomadas()
|
|
428
256
|
espera()
|
|
429
257
|
for (const action of report.humanActions) console.log(`HUMAN ${action.task}: ${action.action}`)
|
|
430
258
|
due()
|
|
@@ -452,7 +280,7 @@ function context(dir, cli) {
|
|
|
452
280
|
console.log(report.claimed
|
|
453
281
|
? `CLAIM tuya desde el reclamo (${report.owner})`
|
|
454
282
|
: `CLAIM libre — tomala con \`ops claim <planning> ${report.task.slug}\``)
|
|
455
|
-
|
|
283
|
+
tomadas()
|
|
456
284
|
espera()
|
|
457
285
|
if (report.blockedTasks.length) console.log(`SKIP ${report.blockedTasks.join(', ')} (acción humana abierta)`)
|
|
458
286
|
for (const action of report.humanActions) console.log(`HUMAN ${action.task}: ${action.action}`)
|
|
@@ -489,4 +317,4 @@ function recurring(dir, cli) {
|
|
|
489
317
|
}
|
|
490
318
|
}
|
|
491
319
|
|
|
492
|
-
module.exports = {
|
|
320
|
+
module.exports = { evidence, tree, context, recurring }
|