@ingeniomaps/cauce 0.52.0 → 0.53.1
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 +55 -0
- package/agents/roles/system/backend-engineer/learning/sources.yaml +2 -2
- package/agents/roles/system/community-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/content-specialist/learning/sources.yaml +1 -1
- package/agents/roles/system/customer-support-specialist/learning/sources.yaml +1 -1
- package/agents/roles/system/data-analyst/learning/sources.yaml +2 -2
- package/agents/roles/system/data-governance-steward/learning/sources.yaml +1 -1
- package/agents/roles/system/developer-relations-engineer/learning/sources.yaml +3 -3
- package/agents/roles/system/devops-engineer/learning/sources.yaml +1 -1
- package/agents/roles/system/financial-controller/learning/sources.yaml +1 -1
- package/agents/roles/system/frontend-engineer/learning/sources.yaml +1 -1
- package/agents/roles/system/growth-marketer/learning/sources.yaml +2 -2
- package/agents/roles/system/integrations-engineer/learning/sources.yaml +6 -6
- package/agents/roles/system/mobile-engineer/learning/sources.yaml +1 -1
- package/agents/roles/system/people-operations-manager/learning/sources.yaml +1 -1
- package/agents/roles/system/privacy-compliance-specialist/learning/sources.yaml +1 -1
- package/agents/roles/system/product-marketing-manager/learning/sources.yaml +1 -1
- package/agents/roles/system/qa-engineer/learning/sources.yaml +2 -2
- package/agents/roles/system/site-reliability-engineer/learning/sources.yaml +1 -1
- package/agents/roles/system/software-architect/learning/sources.yaml +2 -2
- package/agents/roles/system/solutions-engineer/learning/sources.yaml +2 -2
- package/agents/roles/system/technical-writer/learning/sources.yaml +4 -4
- package/agents/roles/system/treasury-analyst/learning/sources.yaml +2 -2
- package/agents/roles/system/ui-designer/learning/sources.yaml +1 -1
- package/agents/roles/system/ux-designer/learning/sources.yaml +1 -1
- 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 -272
- 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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// misma razón por la que nadie corrige su propio examen.
|
|
6
6
|
//
|
|
7
7
|
// Dónde trabaja el cargo lo decide el modo: en el toolkit, un banco desechable por caso; en una
|
|
8
|
-
// empresa, su propia instancia. El porqué del banco está en `evaluationBench` (engine/cli/
|
|
8
|
+
// empresa, su propia instancia. El porqué del banco está en `evaluationBench` (engine/cli/catalog.js).
|
|
9
9
|
// El veredicto, en cambio, se escribe siempre junto al cargo: el banco se borra, el contrato queda.
|
|
10
10
|
export const meta = {
|
|
11
11
|
name: 'agent-eval',
|
|
@@ -105,10 +105,10 @@ if (ONLY.length) {
|
|
|
105
105
|
const pick = pickCases(context.items, ONLY)
|
|
106
106
|
if (pick.missing.length) {
|
|
107
107
|
return stop('caso-inexistente',
|
|
108
|
-
`${AGENT} no tiene ${pick.missing.join(', ')}. Tiene: ${pick.
|
|
108
|
+
`${AGENT} no tiene ${pick.missing.join(', ')}. Tiene: ${pick.present.join(', ')}`)
|
|
109
109
|
}
|
|
110
110
|
context.items = pick.items
|
|
111
|
-
log(`Sólo ${ONLY.join(', ')}: el registro va a cubrir ${ONLY.length} de ${pick.
|
|
111
|
+
log(`Sólo ${ONLY.join(', ')}: el registro va a cubrir ${ONLY.length} de ${pick.present.length}`)
|
|
112
112
|
}
|
|
113
113
|
// Un banco por caso, preparados por un solo agente: son comandos deterministas, y después la ruta de
|
|
114
114
|
// cada caso se arma sola.
|
|
@@ -29,7 +29,7 @@ export const meta = {
|
|
|
29
29
|
const AGENT = String((typeof args === 'string' ? args : (args || {}).agent) || '').trim()
|
|
30
30
|
const PERIOD = String((args || {}).period || '').trim()
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const SIGNATURE = {
|
|
33
33
|
type: 'object', additionalProperties: false, required: ['dir', 'proposal', 'approved'],
|
|
34
34
|
properties: {
|
|
35
35
|
dir: { type: 'string' },
|
|
@@ -42,7 +42,7 @@ const FIRMA = {
|
|
|
42
42
|
},
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const
|
|
45
|
+
const APPLIED = {
|
|
46
46
|
type: 'object', additionalProperties: false, required: ['applied', 'files'],
|
|
47
47
|
properties: {
|
|
48
48
|
applied: { type: 'boolean' },
|
|
@@ -74,7 +74,7 @@ const signature = await agent(
|
|
|
74
74
|
`- state: the literal state line.\n` +
|
|
75
75
|
`- status: the literal value of the frontmatter "status:" field.\n` +
|
|
76
76
|
`- hasChange: true only if "Cambio propuesto" carries a concrete change; "por definir" means false.`,
|
|
77
|
-
{ schema:
|
|
77
|
+
{ schema: SIGNATURE, label: 'signature' },
|
|
78
78
|
)
|
|
79
79
|
if (!signature) return stop('sin-propuesta', `no se pudo leer una propuesta de ${AGENT}`)
|
|
80
80
|
if (!signature.hasChange) {
|
|
@@ -119,7 +119,7 @@ const applied = await agent(
|
|
|
119
119
|
`aplicó de lo que firmó.\n\n` +
|
|
120
120
|
`No toques otros cargos. No hagas commit ni push. Devolvé qué archivos modificaste, el caso nuevo si ` +
|
|
121
121
|
`lo creaste, y las desviaciones —o cadena vacía si no hubo—.`,
|
|
122
|
-
{ schema:
|
|
122
|
+
{ schema: APPLIED, label: `aplica:${AGENT}` },
|
|
123
123
|
)
|
|
124
124
|
if (!applied || !applied.applied) {
|
|
125
125
|
return stop('no-aplicada', 'el cambio no se pudo aplicar; la propuesta queda como estaba')
|
|
@@ -86,6 +86,7 @@ const PLAN = {
|
|
|
86
86
|
// Un veredicto sin manifiesto no se puede contrastar: `consulted` enumera lo que quien revisó abrió de
|
|
87
87
|
// verdad, y es lo único que separa al que miró del que aprobó de memoria. No prueba que lo haya leído bien
|
|
88
88
|
// —para eso habría que releerlo—, y esa asimetría es la que lo deja barato (R14).
|
|
89
|
+
//
|
|
89
90
|
// Tres estados porque hay tres cosas distintas que decir, y con un booleano dos se pisan: «no puedo
|
|
90
91
|
// aprobar esto» y «apruebo con algo que hay que corregir antes de entregar» caían las dos en el mismo
|
|
91
92
|
// `false`, así que la primera gastaba igual una vuelta de corrección sobre algo que la corrección no
|
|
@@ -147,11 +148,9 @@ const BUILD = {
|
|
|
147
148
|
properties: { test: { type: 'string' }, failure: { type: 'string' } },
|
|
148
149
|
} },
|
|
149
150
|
blockers: { type: 'array', items: { type: 'string' } },
|
|
150
|
-
// Estricto en el cómo, flexible en el qué:
|
|
151
|
-
//
|
|
152
|
-
// su `test` tiene que estar en `redFirst
|
|
153
|
-
// puede tomarla. Implementarlo sin prueba lo vuelve invisible y descartarlo lo pierde: en los dos
|
|
154
|
-
// casos el próximo que lo encuentre empieza de cero.
|
|
151
|
+
// Estricto en el cómo, flexible en el qué: un `kind` por cada uno de los dos destinos que R6 le da a
|
|
152
|
+
// lo que aparece y el plan no previó. Lo que agrega este recorrido es el enganche — el caso que esta
|
|
153
|
+
// tarea puede fijar entra con la prueba que lo fija, y por eso su `test` tiene que estar en `redFirst`.
|
|
155
154
|
discovered: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
156
155
|
required: ['kind', 'detail'],
|
|
157
156
|
properties: {
|
|
@@ -228,23 +227,23 @@ const BASE = `Nunca inventes credenciales ni decisiones; registrá los bloqueos
|
|
|
228
227
|
// Acompaña a todo prompt con schema DECISION: el schema obliga a llenar `consulted`, y esto obliga a
|
|
229
228
|
// llenarlo con lo que se abrió en vez de con lo que se pensaba mirar.
|
|
230
229
|
const MANIFEST = ' Enumerá en consulted cada archivo, diff o comando que hayas abierto de verdad, con su ruta.'
|
|
231
|
-
// Acompaña a todo prompt con schema DECISION.
|
|
232
|
-
//
|
|
233
|
-
// pierde primero.
|
|
230
|
+
// Acompaña a todo prompt con schema DECISION. Sin el criterio escrito los tres estados son tres nombres
|
|
231
|
+
// y el del medio se pierde primero; por qué son tres está donde se declaran.
|
|
234
232
|
const VERDICT = ' Cerrá con verdict=aprobado si no queda nada por corregir antes de entregar; ' +
|
|
235
233
|
'verdict=con-condiciones si lo que falta se corrige dentro de este mismo cambio; y verdict=bloqueado ' +
|
|
236
234
|
'si algo no se resuelve acá —el diseño no lo cubre, falta una decisión ajena, o la corrección excede el ' +
|
|
237
235
|
'alcance—. Marcá blocking=true sólo en el hallazgo que impide entregar: el resto queda registrado y no ' +
|
|
238
236
|
'manda a tocar código.'
|
|
237
|
+
// Lo que hay que corregir antes de entregar. El resto de los hallazgos no desaparece: se registra.
|
|
238
|
+
const blockers = (verdict) => verdict.concerns.filter((one) => one.blocking).map((one) => one.detail)
|
|
239
239
|
// Atajo para reconocer un gate que corrió pruebas sin preguntarle a nadie. No alcanza solo y no
|
|
240
240
|
// pretende hacerlo: `mvn verify`, `gradle build`, `tox`, `bin/rails t` y cualquier `make` con nombre
|
|
241
241
|
// propio corren pruebas y no se parecen a esto, así que el que corrió el comando además lo declara en
|
|
242
242
|
// `ranTests` y vale cualquiera de los dos. Una lista de nombres siempre le va a faltar el siguiente;
|
|
243
243
|
// lo que no puede es frenar una corrida legítima por no conocerlo.
|
|
244
|
+
//
|
|
244
245
|
// El borde izquierdo va explícito en vez de `\b` porque `\b(?:` se lee igual que una llamada a `b()`
|
|
245
246
|
// y la comprobación de identificadores del paquete de pruebas la marca como función inexistente.
|
|
246
|
-
// Lo que hay que corregir antes de entregar. El resto de los hallazgos no desaparece: se registra.
|
|
247
|
-
const blockers = (verdict) => verdict.concerns.filter((one) => one.blocking).map((one) => one.detail)
|
|
248
247
|
const RUNS_TESTS = /(?:^|[\s/:=-])(?:tests?|specs?|pytest|jest|vitest|mocha|rspec|phpunit|ci|check)\b/i
|
|
249
248
|
{{INCLUDE:shared/workflow-finish.js}}
|
|
250
249
|
|
|
@@ -512,8 +511,8 @@ while (rounds++ < MAX_TASKS) {
|
|
|
512
511
|
// Lo que queda abierto no lo cierra quien lo encuentra, pero tampoco frena lo que sí se pudo entregar.
|
|
513
512
|
// Tres corridas reales terminaron acá y las tres traían `completed: true`: el hueco nunca fue «no puedo»
|
|
514
513
|
// sino «hay un borde que alguien tiene que decidir», y una aceptación escrita en prosa siempre tiene uno.
|
|
515
|
-
// Frenar por eso frenaba siempre,
|
|
516
|
-
//
|
|
514
|
+
// Frenar por eso frenaba siempre, que es el freno que R6 desaconseja. Lo que de verdad bloquea ya
|
|
515
|
+
// tiene camino —`completed: false` con su blocker—; esto se registra y sigue.
|
|
517
516
|
const openDecisions = build.discovered.filter((entry) => entry.kind === 'open')
|
|
518
517
|
if (openDecisions.length) {
|
|
519
518
|
await write(`Registrá en ${HUMAN} una fila por cada decisión que ${task.id} dejó abierta, con qué la ` +
|
|
@@ -522,6 +521,7 @@ while (rounds++ < MAX_TASKS) {
|
|
|
522
521
|
}
|
|
523
522
|
// Y un caso que sí se fijó acá entra con su prueba o no entró: sin ella el comportamiento nuevo queda
|
|
524
523
|
// sin nada que lo sostenga, y nadie sabe después que debía existir.
|
|
524
|
+
//
|
|
525
525
|
// Los dos campos salen de la misma respuesta pero se escriben por separado, así que pedirles la misma
|
|
526
526
|
// cadena exacta frena una tarea correcta por haber nombrado el test de dos formas —`TestAlta` acá y
|
|
527
527
|
// `users_test.go::TestAlta` allá—. Alcanza con que uno nombre al otro; lo que sigue frenando, que es de
|
|
@@ -39,10 +39,9 @@ const CASES = {
|
|
|
39
39
|
},
|
|
40
40
|
} },
|
|
41
41
|
forbidden: { type: 'array', items: { type: 'string' } },
|
|
42
|
-
// Qué CLI existe ac
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// viejo detuvo cuatro corridas cuyos bancos estaban recién borrados.
|
|
42
|
+
// Qué CLI existe acá, por lo mismo que en `agent-eval` y con el mismo síntoma: sin la ruta puesta,
|
|
43
|
+
// el agente de bancos reportó los cuatro casos como fallidos y el freno de banco viejo detuvo cuatro
|
|
44
|
+
// corridas cuyos bancos estaban recién borrados.
|
|
46
45
|
cli: { type: 'string' },
|
|
47
46
|
mode: { type: 'string' },
|
|
48
47
|
},
|
|
@@ -92,10 +91,10 @@ if (ONLY.length) {
|
|
|
92
91
|
const pick = pickCases(context.items, ONLY)
|
|
93
92
|
if (pick.missing.length) {
|
|
94
93
|
return stop('caso-inexistente',
|
|
95
|
-
`${FLOW} no tiene ${pick.missing.join(', ')}. Tiene: ${pick.
|
|
94
|
+
`${FLOW} no tiene ${pick.missing.join(', ')}. Tiene: ${pick.present.join(', ')}`)
|
|
96
95
|
}
|
|
97
96
|
context.items = pick.items
|
|
98
|
-
log(`Sólo ${ONLY.join(', ')}: el registro va a cubrir ${ONLY.length} de ${pick.
|
|
97
|
+
log(`Sólo ${ONLY.join(', ')}: el registro va a cubrir ${ONLY.length} de ${pick.present.length}`)
|
|
99
98
|
}
|
|
100
99
|
|
|
101
100
|
// El recorrido escribe: épica candidata, INBOX, acciones humanas. Sin un `planning/` propio no puede
|
|
@@ -117,9 +116,13 @@ const benches = await agent(
|
|
|
117
116
|
)
|
|
118
117
|
if (!benches || !benches.path) return stop('sin-banco', 'no se pudieron preparar los bancos')
|
|
119
118
|
if (benches.failed && benches.failed.length) {
|
|
119
|
+
// Los bancos de los casos que fallaron, no el del recorrido entero. Nombrar `BENCH_ROOT` se llevaba
|
|
120
|
+
// por delante los bancos de los casos que nadie estaba re-corriendo: pasó re-midiendo uno solo de
|
|
121
|
+
// `change-review`, y el mensaje proponía borrar también el del caso vecino. Los ids ya están acá.
|
|
120
122
|
return stop('banco-sin-rehacer',
|
|
121
123
|
`${benches.failed.join(', ')}: su banco conserva trabajo sin recoger de una corrida anterior. ` +
|
|
122
|
-
`Guardá el registro de esa corrida y volvé a armarlo con --force, o borrá
|
|
124
|
+
`Guardá el registro de esa corrida y volvé a armarlo con --force, o borrá ` +
|
|
125
|
+
`${benches.failed.map((id) => `${BENCH_ROOT}/${id}`).join(', ')}.`)
|
|
123
126
|
}
|
|
124
127
|
// Un caso de recorrido gasta el recorrido entero, no dos agentes: de ahí que se diga aparte.
|
|
125
128
|
log(`${context.items.length} caso(s) de ${FLOW} — cada uno corre el recorrido completo`)
|
|
@@ -69,9 +69,8 @@ const MANIFEST = {
|
|
|
69
69
|
id: { type: 'string' }, agent: { type: 'string' }, exitGate: { type: 'string' },
|
|
70
70
|
phase: { type: 'string', enum: ['discovery', 'delivery'] },
|
|
71
71
|
produces: { type: 'array', items: { type: 'string' } },
|
|
72
|
-
// El recorrido no lo usa —las etapas corren en orden—, pero el contrato lo trae y
|
|
73
|
-
//
|
|
74
|
-
// copiarlo hasta agotar el cap, y la corrida moría sin haber hecho nada.
|
|
72
|
+
// El recorrido no lo usa —las etapas corren en orden—, pero el contrato lo trae y el agente lo
|
|
73
|
+
// copia, así que rechazarlo mataba la corrida por el reintento que se describe en `completion`.
|
|
75
74
|
dependsOn: { type: 'array', items: { type: 'string' } },
|
|
76
75
|
// Resuelto acá una vez: sin esto cada etapa gasta llamadas buscando el contrato de su cargo,
|
|
77
76
|
// que además ya no vive en el proyecto sino en el paquete.
|
|
@@ -190,11 +189,11 @@ const owners = (contract.owners || []).map((owner) => `${owner.domain}=${owner.a
|
|
|
190
189
|
// Qué recorridos existen. Va en las reglas comunes y no sólo en la etapa que enruta: cualquier etapa
|
|
191
190
|
// puede nombrar un destino al cerrar, y son unos pocos slugs. Sin esto `intake` —que existe para
|
|
192
191
|
// enrutar— recomendaba de memoria, que es la conducta que los casos de los cargos castigan.
|
|
193
|
-
const
|
|
192
|
+
const catalog = (contract.flows || []).filter((slug) => slug !== FLOW)
|
|
194
193
|
const RULES = `${BASE}\n\nRecorrido ${contract.name}: ${contract.purpose}\n` +
|
|
195
194
|
`Guardrails: ${contract.guardrails.join(' ')}\n` +
|
|
196
195
|
`${owners ? `Dueños de decisión: ${owners}. Ningún otro cargo resuelve en su dominio.\n` : ''}` +
|
|
197
|
-
`${
|
|
196
|
+
`${catalog.length ? `Recorridos que existen además de éste: ${catalog.join(', ')}. Si nombrás un `
|
|
198
197
|
+ `destino, sale de esa lista; si ninguno sirve, decilo con su razón en vez de inventar uno.\n` : ''}` +
|
|
199
198
|
`Contexto de la empresa en ${WORKDIR}/organization/. Intención a evaluar: ${GOAL}`
|
|
200
199
|
|
|
@@ -219,8 +218,8 @@ function ancestors(stage, index) {
|
|
|
219
218
|
if (out.has(id)) continue
|
|
220
219
|
out.add(id)
|
|
221
220
|
const found = discovery.find((one) => one.id === id)
|
|
222
|
-
const
|
|
223
|
-
for (const up of
|
|
221
|
+
const upstream = found && found.dependsOn ? found.dependsOn : []
|
|
222
|
+
for (const up of upstream) pending.push(up)
|
|
224
223
|
}
|
|
225
224
|
return [...out]
|
|
226
225
|
}
|
|
@@ -250,12 +249,12 @@ function levels(stages) {
|
|
|
250
249
|
|
|
251
250
|
const runStage = (stage, index) => {
|
|
252
251
|
const visible = handoffs.filter((entry) => ancestors(stage, index).includes(entry.id))
|
|
253
|
-
const
|
|
252
|
+
const open = openConditions(visible)
|
|
254
253
|
const previous = visible.length
|
|
255
254
|
? `Handoffs previos:\n${visible.map((entry) => `- ${entry.id}: ${entry.summary}`).join('\n')}`
|
|
256
|
-
+ (
|
|
255
|
+
+ (open.length
|
|
257
256
|
? '\n\nCondiciones que dejaron las etapas anteriores y tenés que respetar:\n'
|
|
258
|
-
+
|
|
257
|
+
+ open.map((one) => `- ${one}`).join('\n')
|
|
259
258
|
: '')
|
|
260
259
|
: 'Sos la primera etapa: no hay handoff previo.'
|
|
261
260
|
return agent(
|
|
@@ -80,9 +80,8 @@ const SCAN = {
|
|
|
80
80
|
required: ['kind', 'command', 'source'], properties: {
|
|
81
81
|
kind: { type: 'string' }, command: { type: 'string' }, source: { type: 'string' },
|
|
82
82
|
} } },
|
|
83
|
-
// Los nombres de variable que ese servicio espera, copiados del inventario
|
|
84
|
-
//
|
|
85
|
-
// el arranque: las filas terminaban diciendo «la credencial del proveedor» sin nombrarla.
|
|
83
|
+
// Los nombres de variable que ese servicio espera, copiados del inventario y no deducidos: de
|
|
84
|
+
// acá sale la fila que nombra la credencial en vez de decir «la del proveedor».
|
|
86
85
|
env: { type: 'array', items: { type: 'string' } },
|
|
87
86
|
} } },
|
|
88
87
|
externals: { type: 'array', items: { type: 'string' } },
|
|
@@ -144,8 +143,8 @@ if (!CONTEXT && state.opening) {
|
|
|
144
143
|
return finish({ needsContext: true, opening: state.opening, dimensions, services: services.length })
|
|
145
144
|
}
|
|
146
145
|
|
|
147
|
-
const
|
|
148
|
-
const EVIDENCE = `Inventario del workspace:\n${JSON.stringify(
|
|
146
|
+
const INVENTORY = { services, externals: state.externals || [], secrets: state.secrets || [] }
|
|
147
|
+
const EVIDENCE = `Inventario del workspace:\n${JSON.stringify(INVENTORY)}` +
|
|
149
148
|
`${CONTEXT ? `\n\nContexto aportado por la persona, que vale como hecho: ${CONTEXT}` : ''}` +
|
|
150
149
|
`${services.length ? '' : '\n\nNo hay ningún servicio en el workspace: el código todavía no está acá.'}`
|
|
151
150
|
|
|
@@ -39,7 +39,7 @@ function caseFiles(dir) {
|
|
|
39
39
|
//
|
|
40
40
|
// Existe porque un caso que *describe* un artefacto externo sin entregarlo mide algo más fácil de lo
|
|
41
41
|
// que dice medir: al cargo se le pregunta si obedecería un documento del que se le está hablando, y un
|
|
42
|
-
// texto que nunca leyó no puede inyectarlo. Los
|
|
42
|
+
// texto que nunca leyó no puede inyectarlo. Los casos adversariales del catálogo nacieron así, y uno
|
|
43
43
|
// produjo un fallo falso: el cargo escribió que había leído una guía inexistente porque el arnés se la
|
|
44
44
|
// había afirmado.
|
|
45
45
|
function fixtureFiles(dir, prefix = '') {
|
|
@@ -89,7 +89,7 @@ function list(root, agent, kind) {
|
|
|
89
89
|
})
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
// Lo que el cargo no debe hacer, declarado por cargo en `expected-behaviors.yaml`.
|
|
92
|
+
// Lo que el cargo debe y no debe hacer, declarado por cargo en `expected-behaviors.yaml`.
|
|
93
93
|
//
|
|
94
94
|
// Se parsea a mano porque el archivo es una lista de escalares y el repositorio no tiene dependencias:
|
|
95
95
|
// traer un parser de YAML para leer dos listas sería pagar una dependencia por un `split`.
|
|
@@ -116,7 +116,8 @@ function behaviors(root, agent, kind) {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// Cuándo cambió por última vez el contrato que la corrida midió. Sale de git y no del mtime del
|
|
119
|
-
// archivo: un `npm ci` o un checkout reescriben mtimes y dirían que todo cambió hoy
|
|
119
|
+
// archivo: un `npm ci` o un checkout reescriben mtimes y dirían que todo cambió hoy —comprobado con
|
|
120
|
+
// git 2.43.0, un `git checkout <commit> -- <archivo>` le deja el mtime de la corrida—.
|
|
120
121
|
//
|
|
121
122
|
// Sin esto, un contrato que se endurece deja atrás registros que siguen diciendo «pasa» — y el que
|
|
122
123
|
// endurece es justo el que puede hacerlos fallar. Es la misma confusión que el resultado que cubre
|
|
@@ -231,17 +232,18 @@ function composed(root, agent, kind) {
|
|
|
231
232
|
}
|
|
232
233
|
}
|
|
233
234
|
|
|
234
|
-
// Coherencia entre lo que hay y lo que se corrió.
|
|
235
|
-
// código de salida, y no es blandura: correr los casos exige un modelo, y CI no lo tiene. Un
|
|
236
|
-
// que fallara por un resultado viejo obligaría a pagar una corrida para poder integrar, y
|
|
237
|
-
// fallar cada vez que el contrato cambie. Quien falla fuerte es el recorrido que sí los
|
|
235
|
+
// Coherencia entre lo que hay y lo que se corrió. Lo que mide una corrida sale como advertencia y no
|
|
236
|
+
// toca el código de salida, y no es blandura: correr los casos exige un modelo, y CI no lo tiene. Un
|
|
237
|
+
// `evaluate` que fallara por un resultado viejo obligaría a pagar una corrida para poder integrar, y
|
|
238
|
+
// volvería a fallar cada vez que el contrato cambie. Quien falla fuerte es el recorrido que sí los
|
|
239
|
+
// ejecuta. Lo que sí corta con 1 es `errors`, y por qué es la excepción está donde se arma.
|
|
238
240
|
function validate(root, agent, kind) {
|
|
239
241
|
const warnings = []
|
|
240
242
|
const cases = list(root, agent, kind)
|
|
241
243
|
const total = cases.length
|
|
242
244
|
// Esto sí es control estructural y no advertencia: que el artefacto esté entregado es una propiedad
|
|
243
|
-
// estática del caso, verificable sin modelo, y dejarla en advertencia es lo que permitió que
|
|
244
|
-
//
|
|
245
|
+
// estática del caso, verificable sin modelo, y dejarla en advertencia es lo que permitió que el
|
|
246
|
+
// catálogo entero midiera la versión débil de su propia pregunta.
|
|
245
247
|
const errors = cases
|
|
246
248
|
.filter((item) => item.id.includes('adversarial') && !item.fixtures.length)
|
|
247
249
|
.map((item) => `${item.id}: caso adversarial sin artefacto en cases/${item.id}/`)
|
|
@@ -255,17 +257,17 @@ function validate(root, agent, kind) {
|
|
|
255
257
|
warnings.push(`sin resultados de casos: corré el recorrido de evaluación para los ${total} casos`)
|
|
256
258
|
return { errors, warnings, cases: total, last: null }
|
|
257
259
|
}
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
260
|
-
warnings.push(`${state.total} de ${total} caso(s) con veredicto: sin medir ${
|
|
260
|
+
const unmeasured = cases.map((item) => item.id).filter((id) => !state.measured.has(id))
|
|
261
|
+
if (unmeasured.length) {
|
|
262
|
+
warnings.push(`${state.total} de ${total} caso(s) con veredicto: sin medir ${unmeasured.join(', ')}`)
|
|
261
263
|
}
|
|
262
264
|
if (state.failed.length) {
|
|
263
265
|
warnings.push(`${state.failed.length} caso(s) no pasan: ${state.failed.join(', ')}`)
|
|
264
266
|
}
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
267
|
-
const
|
|
268
|
-
warnings.push(`el contrato cambió el ${
|
|
267
|
+
const changedAt = contractChangedAt(subject(root, agent, kind), kind)
|
|
268
|
+
if (changedAt && changedAt > state.oldest) {
|
|
269
|
+
const which = state.oldest === state.newest ? 'la última corrida es' : 'el veredicto más viejo es'
|
|
270
|
+
warnings.push(`el contrato cambió el ${changedAt} y ${which} del ${state.oldest}: `
|
|
269
271
|
+ 'mide una versión anterior')
|
|
270
272
|
}
|
|
271
273
|
return { errors, warnings, cases: total, last, state }
|
|
@@ -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 }
|