@ingeniomaps/cauce 0.53.0 → 0.53.2
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 +35 -0
- package/automatization/hooks/guard-dependencies.sh +1 -1
- package/automatization/hooks/guard-destructive.sh +1 -1
- package/automatization/hooks/guard-engine.sh +1 -1
- package/automatization/hooks/guard-generated.sh +1 -1
- package/automatization/hooks/guard-git-add.sh +1 -1
- package/automatization/hooks/guard-governance.sh +1 -1
- package/automatization/hooks/guard-integration-snapshot.sh +1 -1
- package/automatization/hooks/guard-migrations.sh +1 -1
- package/automatization/hooks/guard-planning-drift.sh +1 -1
- package/automatization/hooks/guard-secrets.sh +1 -1
- package/automatization/hooks/guard-test-evidence.sh +1 -1
- package/automatization/hooks/guard-verify.sh +1 -1
- package/automatization/hooks/guard-workspace-boundary.sh +1 -1
- package/automatization/hooks/run-hook.sh +3 -2
- package/automatization/runners/antigravity/hook.js +13 -14
- package/automatization/shared/eval-only.js +3 -3
- package/automatization/workflows/agent-eval.js +3 -3
- package/automatization/workflows/agent-promote.js +4 -4
- package/automatization/workflows/autobuild.js +12 -12
- package/automatization/workflows/flow-eval.js +10 -7
- package/automatization/workflows/flow.js +9 -10
- package/automatization/workflows/onboard.js +4 -5
- package/engine/agents/evaluations.js +18 -16
- package/engine/agents/learning-files.js +111 -0
- package/engine/agents/learning-sources.js +166 -0
- package/engine/agents/learning.js +93 -300
- package/engine/automation/config.js +175 -0
- package/engine/automation/hooks.js +96 -0
- package/engine/automation/index.js +17 -431
- package/engine/automation/roles.js +72 -0
- package/engine/automation/runners.js +162 -0
- package/engine/cli/catalog.js +7 -12
- package/engine/cli/instance.js +11 -10
- package/engine/cli/io.js +1 -1
- package/engine/cli/ops.js +12 -10
- package/engine/cli/wiring.js +2 -4
- package/engine/config/validate.js +2 -1
- package/engine/core/frontmatter.js +2 -1
- package/engine/core/ownership.js +5 -4
- package/engine/core/scan.js +3 -0
- package/engine/flows/registry.js +2 -2
- package/engine/hooks/files.js +160 -0
- package/engine/hooks/input.js +129 -0
- package/engine/hooks/run.js +24 -449
- package/engine/hooks/shell.js +197 -0
- package/engine/integrations/registry.js +2 -0
- package/engine/planning/contracts.js +12 -10
- package/engine/planning/parser.js +4 -3
- package/engine/planning/state.js +2 -1
- package/package.json +5 -5
- package/template/tools/ops.js +2 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Los nombres y estados que el ciclo de aprendizaje escribe en disco: cómo se llama un informe y una
|
|
4
|
+
// propuesta, en qué orden se leen, en qué estado están y dónde se puede escribir. Vive aparte porque
|
|
5
|
+
// lo necesitan los dos lados —el ciclo que produce esos documentos y la validación que los juzga— y
|
|
6
|
+
// sin este corte cada uno tendría que requerir al otro.
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs')
|
|
9
|
+
const path = require('node:path')
|
|
10
|
+
const catalog = require('./catalog')
|
|
11
|
+
const ownership = require('../core/ownership')
|
|
12
|
+
const { section } = require('../planning/parser')
|
|
13
|
+
|
|
14
|
+
const REQUIRED_SECTIONS = [
|
|
15
|
+
'Hallazgos',
|
|
16
|
+
'Evidencia',
|
|
17
|
+
'Cambio propuesto',
|
|
18
|
+
'Riesgos y regresiones',
|
|
19
|
+
'Evaluación',
|
|
20
|
+
'Aprobación humana',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
// Un cargo del sistema vive dentro del paquete: escribir ahí perdería el informe en el próximo
|
|
24
|
+
// `npm ci`, y además duplicaría en cada empresa una investigación sobre la profesión que se hace
|
|
25
|
+
// mejor una sola vez. Lo que sí es de esta empresa es su contexto, y ese tiene otro lugar.
|
|
26
|
+
// Un recorrido no viene del paquete cuando lo mantiene este repositorio, así que la pregunta de
|
|
27
|
+
// dónde se puede escribir es la misma y la respuesta se resuelve igual: dentro del proyecto, sí.
|
|
28
|
+
function assertWritableTeam(root, slug) {
|
|
29
|
+
const dir = path.dirname(require('../flows/registry').read(root, slug).file)
|
|
30
|
+
const own = path.resolve(root, 'flows')
|
|
31
|
+
if (path.resolve(dir).startsWith(`${own}${path.sep}`)) return dir
|
|
32
|
+
throw new Error(
|
|
33
|
+
`${slug} es un recorrido que trae Cauce y su aprendizaje se hace en el toolkit, no acá.\n` +
|
|
34
|
+
` Para tener una versión propia, copialo a flows/${slug}/ y mantenelo vos.`,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertWritable(root, agent, kind = 'agent') {
|
|
39
|
+
if (kind === 'flow') return assertWritableTeam(root, agent)
|
|
40
|
+
const found = catalog.find(root, agent)
|
|
41
|
+
// Lo que decide no es si el cargo es del sistema, sino si vive dentro de este repositorio. En el
|
|
42
|
+
// toolkit los cargos del sistema son propios y se aprenden acá; en una instancia vienen del
|
|
43
|
+
// paquete, y escribir ahí se pierde en el próximo `npm ci` o en el próximo upgrade.
|
|
44
|
+
const own = path.resolve(catalog.projectCatalog(root))
|
|
45
|
+
if (path.resolve(found.dir).startsWith(`${own}${path.sep}`)) return found.dir
|
|
46
|
+
throw new Error(
|
|
47
|
+
`${agent} es un cargo que trae Cauce y su aprendizaje se hace en el toolkit, no acá.\n` +
|
|
48
|
+
` Lo que este cargo debe saber de esta empresa va en organization/roles/${agent}.md.\n` +
|
|
49
|
+
` Para tener una versión propia del cargo, adoptalo: ops agents fork ${agent}.`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isoDate(now = new Date()) { return now.toISOString().slice(0, 10) }
|
|
54
|
+
function month(now = new Date()) { return now.toISOString().slice(0, 7) }
|
|
55
|
+
|
|
56
|
+
// Una propuesta por período, y sus revisiones. La revisión existe porque aplicar no es el final del
|
|
57
|
+
// ciclo: la evaluación posterior es la que dice si el cambio sirvió, y cuando dice que no, el sello
|
|
58
|
+
// —que está para que nadie reaplique lo mismo y duplique cada viñeta— dejaba al cargo con un contrato
|
|
59
|
+
// que se sabe mal calibrado y sin camino para corregirlo hasta el mes siguiente. La corrección es un
|
|
60
|
+
// cambio distinto: documento propio, firma propia, y la aplicada queda sellada donde está.
|
|
61
|
+
const PROPOSAL_NAME = /^(\d{4}-\d{2})(?:-r(\d+))?\.md$/
|
|
62
|
+
|
|
63
|
+
// Mismo cuidado que con los registros de evaluación: `-` va antes que `.` en ASCII, así que ordenar
|
|
64
|
+
// nombres pondría `2026-08-r2.md` delante de `2026-08.md` y la revisión se leería como la más vieja.
|
|
65
|
+
function proposalOrder(name) {
|
|
66
|
+
const [, period, revision] = name.match(PROPOSAL_NAME)
|
|
67
|
+
return `${period}-${String(Number(revision || 1)).padStart(4, '0')}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// El tope de la línea de índice. No es estético: es una línea por cargo, todas a la vez, y una que se
|
|
71
|
+
// envuelve rompe la columna que hace posible el vistazo.
|
|
72
|
+
const SUMMARY_MAX = 120
|
|
73
|
+
|
|
74
|
+
function proposalFiles(dir) {
|
|
75
|
+
try {
|
|
76
|
+
return fs.readdirSync(dir)
|
|
77
|
+
.filter((name) => PROPOSAL_NAME.test(name))
|
|
78
|
+
.sort((one, other) => proposalOrder(one).localeCompare(proposalOrder(other)))
|
|
79
|
+
} catch { return [] }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function frontmatterState(text, fallback) {
|
|
83
|
+
return ((text.match(/^status:\s*(\S+)\s*$/m) || [])[1] || fallback).toLowerCase()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function proposalState(text) {
|
|
87
|
+
return frontmatterState(text, 'proposed')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// El sufijo `-N` es la segunda corrida del mismo día, y es la que trae el veredicto más nuevo. Sin él
|
|
91
|
+
// en el patrón, más de una cuarta parte de los registros que existían —de cargos y de recorridos—
|
|
92
|
+
// quedaban fuera del ciclo: no entraban a ninguna propuesta y nada lo delataba, que es el modo de fallo
|
|
93
|
+
// que el comentario de `markConsolidated` ya describía para el informe atrasado.
|
|
94
|
+
const REPORT_NAME = /^(\d{4}-\d{2}-\d{2})(?:-(\d+))?\.md$/
|
|
95
|
+
|
|
96
|
+
function reportFiles(dir) {
|
|
97
|
+
try {
|
|
98
|
+
return fs.readdirSync(dir)
|
|
99
|
+
.map((name) => [name, REPORT_NAME.exec(name)])
|
|
100
|
+
.filter(([, hit]) => hit)
|
|
101
|
+
.sort(([, a], [, b]) =>
|
|
102
|
+
(a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : Number(a[2] || 0) - Number(b[2] || 0)))
|
|
103
|
+
.map(([name]) => name)
|
|
104
|
+
} catch { return [] }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
REQUIRED_SECTIONS, SUMMARY_MAX, PROPOSAL_NAME, REPORT_NAME,
|
|
109
|
+
assertWritableTeam, assertWritable, isoDate, month,
|
|
110
|
+
proposalOrder, proposalFiles, frontmatterState, proposalState, reportFiles,
|
|
111
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// El contrato de fuentes de un cargo y la validación que lo mide: qué tipos hay, cada cuánto le toca
|
|
4
|
+
// investigar a quien las declara, y qué está mal escrito. Su reloj es el del contrato —un tipo nuevo,
|
|
5
|
+
// otra cadencia—, no el del ciclo que produce informes y propuestas.
|
|
6
|
+
|
|
7
|
+
const fs = require('node:fs')
|
|
8
|
+
const path = require('node:path')
|
|
9
|
+
const catalog = require('./catalog')
|
|
10
|
+
const evaluations = require('./evaluations')
|
|
11
|
+
const {
|
|
12
|
+
REQUIRED_SECTIONS, SUMMARY_MAX, frontmatterState, proposalFiles, proposalState, reportFiles,
|
|
13
|
+
} = require('./learning-files')
|
|
14
|
+
|
|
15
|
+
function evaluateTeam(root, slug) {
|
|
16
|
+
const dir = path.dirname(require('../flows/registry').read(root, slug).file)
|
|
17
|
+
const errors = []
|
|
18
|
+
const warnings = []
|
|
19
|
+
if (!fs.existsSync(path.join(dir, 'learning', 'HISTORY.md'))) {
|
|
20
|
+
warnings.push('sin learning/HISTORY.md: lo que se le cambie al recorrido no queda registrado')
|
|
21
|
+
}
|
|
22
|
+
const proposals = proposalFiles(path.join(dir, 'learning', 'proposals'))
|
|
23
|
+
let pending = 0
|
|
24
|
+
for (const name of proposals) {
|
|
25
|
+
const text = fs.readFileSync(path.join(dir, 'learning', 'proposals', name), 'utf8')
|
|
26
|
+
if (!/^automatic_apply:\s*false$/m.test(text)) errors.push(`${name}: automatic_apply debe ser false`)
|
|
27
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
28
|
+
if (!text.includes(`## ${section}`)) errors.push(`${name}: falta sección ${section}`)
|
|
29
|
+
}
|
|
30
|
+
if (proposalState(text) !== 'applied') pending += 1
|
|
31
|
+
}
|
|
32
|
+
return { errors, warnings, proposals: proposals.length, pending, cases: 0 }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Qué publica una fuente, que es lo único que decide cada cuánto vale la pena volver a mirarla. No
|
|
36
|
+
// dice si es primaria —eso lo exige `rules.require_primary_source`— ni si sigue vigente: lo que se
|
|
37
|
+
// aparta del default lo declara la fuente con `authority:` o `status:`, y por eso son dos campos y no
|
|
38
|
+
// un nombre compuesto. Cuando eran uno solo el catálogo llegó a 51 etiquetas para estas seis.
|
|
39
|
+
const SOURCE_TIERS = ['advisory', 'platform', 'project', 'regulation', 'standard', 'profession']
|
|
40
|
+
|
|
41
|
+
// Cada cuánto vale la pena volver a mirar cada tipo. Un aviso publica todos los días y llegar un mes
|
|
42
|
+
// tarde es llegar tarde; una norma se revisa por edición y mirarla cada lunes devuelve el mismo texto.
|
|
43
|
+
// La cadencia de un cargo la fija su fuente más rápida: basta una que corra para que la semana traiga
|
|
44
|
+
// algo, y ninguna otra pierde nada por mirarse antes.
|
|
45
|
+
const TIER_CADENCE = {
|
|
46
|
+
advisory: 'semanal', platform: 'semanal', project: 'semanal',
|
|
47
|
+
regulation: 'mensual', standard: 'mensual', profession: 'trimestral',
|
|
48
|
+
}
|
|
49
|
+
const CADENCES = ['semanal', 'mensual', 'trimestral']
|
|
50
|
+
|
|
51
|
+
// Sale del árbol y no de una lista escrita a mano, por la misma razón que la matriz del cron sale del
|
|
52
|
+
// árbol: una lista paralela se pudre el día que un cargo cambia sus fuentes y nadie la toca.
|
|
53
|
+
function cadence(root, agent) {
|
|
54
|
+
const file = path.join(catalog.resolve(root, agent), 'learning', 'sources.yaml')
|
|
55
|
+
if (!fs.existsSync(file)) return ''
|
|
56
|
+
const tiers = sourceTiers(fs.readFileSync(file, 'utf8')).filter((one) => TIER_CADENCE[one])
|
|
57
|
+
if (!tiers.length) return ''
|
|
58
|
+
return CADENCES[Math.min(...tiers.map((one) => CADENCES.indexOf(TIER_CADENCE[one])))]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Basta con las líneas `tier:`: el archivo es del catálogo, no de un tercero, y agregar un parser de
|
|
62
|
+
// YAML por un campo rompería la regla de cero dependencias.
|
|
63
|
+
function sourceTiers(text) {
|
|
64
|
+
const body = text.includes('sources:') ? text.slice(text.indexOf('sources:')) : ''
|
|
65
|
+
return [...body.matchAll(/tier:\s*([A-Za-z-]+)/g)].map((hit) => hit[1])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Las fuentes de un cargo, por su URL. La misma URL con dos nombres es una sola fuente contada dos
|
|
69
|
+
// veces: el catálogo llegó a tener la especificación OpenAPI bajo tres —`OpenAPI Specification`,
|
|
70
|
+
// `...latest published` y `...3.2.0`— así que arreglarle el `tier` a un cargo no se lo arreglaba a los
|
|
71
|
+
// otros, y quien leyera el informe vería la misma página citada como si fueran tres.
|
|
72
|
+
function sourceUrls(text) {
|
|
73
|
+
const body = text.includes('sources:') ? text.slice(text.indexOf('sources:')) : ''
|
|
74
|
+
const out = []
|
|
75
|
+
let name = ''
|
|
76
|
+
for (const line of body.split('\n')) {
|
|
77
|
+
const declared = line.match(/^\s*-\s*name:\s*(.+?)\s*$/)
|
|
78
|
+
if (declared) { name = declared[1].replace(/^['"]|['"]$/g, ''); continue }
|
|
79
|
+
const url = line.match(/^\s*url:\s*(\S+)/)
|
|
80
|
+
if (url && name) out.push({ name, url: url[1].replace(/\/+$/, '') })
|
|
81
|
+
}
|
|
82
|
+
return out
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function evaluate(root, agent) {
|
|
86
|
+
const target = catalog.resolve(root, agent)
|
|
87
|
+
const errors = []
|
|
88
|
+
const warnings = []
|
|
89
|
+
const requiredFiles = [
|
|
90
|
+
'learning/sources.yaml',
|
|
91
|
+
'learning/HISTORY.md',
|
|
92
|
+
'evaluations/expected-behaviors.yaml',
|
|
93
|
+
]
|
|
94
|
+
// `AUTOMATION.md` documenta cómo corre la automatización de aprendizaje del toolkit. Exigírselo
|
|
95
|
+
// a una empresa que escribe un cargo propio era pedirle contabilidad interna nuestra: su cargo debe
|
|
96
|
+
// tener contrato, fuentes e historia, no nuestro andamiaje.
|
|
97
|
+
if (catalog.find(root, agent).system) requiredFiles.push('learning/AUTOMATION.md')
|
|
98
|
+
for (const relative of requiredFiles) {
|
|
99
|
+
if (!fs.existsSync(path.join(target, relative))) errors.push(`falta ${relative}`)
|
|
100
|
+
}
|
|
101
|
+
const sourcesFile = path.join(target, 'learning', 'sources.yaml')
|
|
102
|
+
if (fs.existsSync(sourcesFile)) {
|
|
103
|
+
const tiers = sourceTiers(fs.readFileSync(sourcesFile, 'utf8'))
|
|
104
|
+
// Sin fuentes el ciclo semanal no tiene literatura que leer y devuelve un informe vacío cada
|
|
105
|
+
// semana. Avisa y no bloquea: un cargo que se está escribiendo todavía no las tiene.
|
|
106
|
+
if (!tiers.length) warnings.push('sources.yaml sin fuentes: la investigación no tiene qué leer')
|
|
107
|
+
for (const tier of tiers) {
|
|
108
|
+
if (!SOURCE_TIERS.includes(tier)) {
|
|
109
|
+
errors.push(`sources.yaml: tier "${tier}" fuera de ${SOURCE_TIERS.join(' | ')}`)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Dos nombres para una URL. Es error y no aviso: la cadencia sale del `tier` de cada entrada, así
|
|
113
|
+
// que dos copias de la misma fuente pueden decir cosas distintas sobre cada cuánto publica, y la
|
|
114
|
+
// más rápida gana sin que nadie lo haya decidido.
|
|
115
|
+
const byUrl = new Map()
|
|
116
|
+
for (const one of sourceUrls(fs.readFileSync(sourcesFile, 'utf8'))) {
|
|
117
|
+
const previous = byUrl.get(one.url)
|
|
118
|
+
if (previous && previous !== one.name) {
|
|
119
|
+
errors.push(`sources.yaml: ${one.url} está dos veces, como "${previous}" y como "${one.name}"`)
|
|
120
|
+
}
|
|
121
|
+
byUrl.set(one.url, one.name)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const skill = fs.readFileSync(path.join(target, 'SKILL.md'), 'utf8').toLowerCase()
|
|
125
|
+
for (const phrase of ['no inventar', 'autorización', 'evidencia observable']) {
|
|
126
|
+
if (!skill.includes(phrase)) errors.push(`SKILL.md no conserva el control: ${phrase}`)
|
|
127
|
+
}
|
|
128
|
+
// Sin su línea, el cargo existe pero no se encuentra: quien tiene una tarea tendría que abrir la
|
|
129
|
+
// carpeta para saber si es éste. Se exige acá y no como advertencia porque es estático y de una línea.
|
|
130
|
+
const summary = catalog.summary(target)
|
|
131
|
+
if (!summary) errors.push('SKILL.md no declara summary: la línea con la que se elige este cargo')
|
|
132
|
+
else if (summary.length > SUMMARY_MAX) {
|
|
133
|
+
errors.push(`summary tiene ${summary.length} caracteres y el máximo es ${SUMMARY_MAX}: `
|
|
134
|
+
+ 'si no entra en una línea, no sirve para elegir de un vistazo')
|
|
135
|
+
}
|
|
136
|
+
const proposals = proposalFiles(path.join(target, 'learning', 'proposals'))
|
|
137
|
+
let pending = 0
|
|
138
|
+
for (const name of proposals) {
|
|
139
|
+
const text = fs.readFileSync(path.join(target, 'learning', 'proposals', name), 'utf8')
|
|
140
|
+
if (!/^automatic_apply:\s*false$/m.test(text)) errors.push(`${name}: automatic_apply debe ser false`)
|
|
141
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
142
|
+
if (!text.includes(`## ${section}`)) errors.push(`${name}: falta sección ${section}`)
|
|
143
|
+
}
|
|
144
|
+
// Contar sólo las que esperan algo. Una propuesta aplicada contada como propuesta deja al cargo
|
|
145
|
+
// reportando trabajo pendiente para siempre, y es la misma confusión que permitía reaplicarla.
|
|
146
|
+
if (proposalState(text) !== 'applied') pending += 1
|
|
147
|
+
}
|
|
148
|
+
// Los hallazgos que todavía no llegaron al contrato. No es un error —la propuesta que los tome
|
|
149
|
+
// puede no haberse abierto aún—, pero sin decirlo un informe escrito y olvidado se ve igual que uno
|
|
150
|
+
// ya incorporado: los dos son un archivo en `reports/`.
|
|
151
|
+
const reportDir = path.join(target, 'learning', 'reports')
|
|
152
|
+
const unconsolidated = reportFiles(reportDir).filter((name) =>
|
|
153
|
+
frontmatterState(fs.readFileSync(path.join(reportDir, name), 'utf8'), 'draft') !== 'consolidated')
|
|
154
|
+
if (unconsolidated.length) {
|
|
155
|
+
warnings.push(`${unconsolidated.length} informe(s) sin consolidar (${unconsolidated.join(', ')}): `
|
|
156
|
+
+ 'entran en la próxima propuesta')
|
|
157
|
+
}
|
|
158
|
+
let cases = 0
|
|
159
|
+
try {
|
|
160
|
+
cases = fs.readdirSync(path.join(target, 'evaluations', 'cases'))
|
|
161
|
+
.filter((name) => name.endsWith('.md')).length
|
|
162
|
+
} catch { /* vacío */ }
|
|
163
|
+
return { errors, warnings, proposals: proposals.length, pending, cases }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { SOURCE_TIERS, CADENCES, cadence, evaluate, evaluateTeam }
|