@ingeniomaps/cauce 0.59.0 → 0.60.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 +37 -0
- package/automatization/workflows/autobuild.js +13 -4
- package/automatization/workflows/onboard.js +11 -5
- package/engine/automation/index.js +1 -0
- package/engine/cli/instance.js +25 -128
- package/engine/cli/planning.js +1 -0
- package/engine/cli/upgrade-report.js +150 -0
- package/engine/integrations/registry.js +23 -6
- package/engine/planning/contracts.js +26 -0
- package/engine/planning/parser.js +9 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,43 @@ desde este repositorio no va, porque el que lee no puede actuar sobre eso. Cuand
|
|
|
14
14
|
unas pocas líneas casi siempre es porque cuenta cómo se descubrió el problema o por qué se eligió el
|
|
15
15
|
diseño — eso vive en el commit y en el código.
|
|
16
16
|
|
|
17
|
+
## [0.60.0] - 2026-09-03
|
|
18
|
+
|
|
19
|
+
### Corregido
|
|
20
|
+
|
|
21
|
+
- **La épica que promueve una integración pasa `check`.** La descripción remota trae sus propios
|
|
22
|
+
encabezados —el fixture de Jira trae `## Criterios de aceptación`— y quedaban compitiendo con los que
|
|
23
|
+
escribe la promoción, así que el parser leía la sección importada, que no tiene ningún criterio, y el
|
|
24
|
+
planning quedaba en rojo apenas promovías. Ahora lo importado baja un nivel al entrar, el parser
|
|
25
|
+
prefiere la coincidencia exacta cuando existe —`## Historias (Tareas)` sigue resolviendo— y el
|
|
26
|
+
criterio sale sin el guión de la viñeta pegado.
|
|
27
|
+
|
|
28
|
+
- **Curar un draft ya no deja la integración en rojo.** El README manda editar el draft y correr
|
|
29
|
+
`check`, y ese `check` fallaba: editar deja obsoleto un flag de `remote.json` que sólo se recalcula en
|
|
30
|
+
`sync`, `rebase` y `reconcile`. Dejó de exigirse porque no protegía nada: se leía en un único lugar,
|
|
31
|
+
la validación que lo comparaba consigo mismo, y lo que decide qué vuelve al remoto compara el
|
|
32
|
+
contenido. **Qué cambia para vos**: el recorrido documentado funciona sin pasos intermedios.
|
|
33
|
+
|
|
34
|
+
- **`/onboard` escribe el mapa donde vive desde 0.57.0.** Seguía mandándolo a `AGENTS.md`, así que el
|
|
35
|
+
primer recorrido de una instancia nueva deshacía el arreglo que sacó de ahí lo que sólo sabe tu
|
|
36
|
+
proyecto. Ahora escribe las tres secciones de `organization/workspace.md` —incluidas integraciones y
|
|
37
|
+
excepciones de autonomía, que ningún recorrido llenaba— y suma `organization/domains.md`, que no
|
|
38
|
+
nombraba.
|
|
39
|
+
|
|
40
|
+
- **`upgrade` te dice cuando se lleva las instrucciones de tu runner.** `AGENTS.md` es del toolkit y se
|
|
41
|
+
reemplaza entero, con el bloque que `automation install` dejó adentro. Lo recuperás reinstalando —el
|
|
42
|
+
propio `upgrade` te lo recuerda— pero ese recordatorio no tenía causa visible, y el bloque desaparecía
|
|
43
|
+
sin explicación. Ahora se nombra el archivo y el runner, una sola vez.
|
|
44
|
+
|
|
45
|
+
- **`check` avisa cuando dos secciones de una épica compiten por el mismo rol.** El parser prefiere la
|
|
46
|
+
exacta, así que resuelve — en silencio, y quien escribió las dos no se entera de que una se ignora
|
|
47
|
+
entera. Un título con sufijo como `## Historias (Tareas)` no dispara nada: es el único de su rol.
|
|
48
|
+
|
|
49
|
+
- **`/autobuild` ve las excepciones de autonomía de tu proyecto.** Armaba sus límites leyendo sólo
|
|
50
|
+
`AGENTS.md`, y desde 0.57.0 los tuyos están en `organization/workspace.md`. Lo que viajaba a cada
|
|
51
|
+
subagente como «Límites del proyecto» eran los genéricos del toolkit. Ahora ese archivo entra en la
|
|
52
|
+
lectura única de Triage; si no existe o su sección sigue como la trae el molde, no se inventa ninguno.
|
|
53
|
+
|
|
17
54
|
## [0.59.0] - 2026-09-03
|
|
18
55
|
|
|
19
56
|
### Corregido
|
|
@@ -29,6 +29,7 @@ export const meta = {
|
|
|
29
29
|
{{INCLUDE:shared/workflow-root.js}}
|
|
30
30
|
const CONFIG = `${ROOT}/ops.config.json`
|
|
31
31
|
const P = `${ROOT}/planning`
|
|
32
|
+
const ORG = `${ROOT}/organization`
|
|
32
33
|
const BACKLOG = `${P}/BACKLOG.md`
|
|
33
34
|
const DONE = `${P}/DONE.md`
|
|
34
35
|
const WIP = `${P}/WIP.md`
|
|
@@ -263,15 +264,23 @@ phase = (name) => { ran.push(name); announce(name) }
|
|
|
263
264
|
|
|
264
265
|
phase('Triage')
|
|
265
266
|
// El contrato se lee una sola vez por corrida y viaja como texto: ningún subagente relee AGENTS.md,
|
|
266
|
-
// ops.config.json ni PROTOCOL.md. `ops check` y el guard planning-drift siguen validando
|
|
267
|
+
// workspace.md, ops.config.json ni PROTOCOL.md. `ops check` y el guard planning-drift siguen validando
|
|
268
|
+
// el resultado.
|
|
269
|
+
//
|
|
270
|
+
// `organization/workspace.md` está en esa lista desde que 0.57.0 sacó del `AGENTS.md` lo que sólo sabe
|
|
271
|
+
// el proyecto: los límites que éste amplía o restringe viven ahí, y sin leerlo lo que viaja a cada
|
|
272
|
+
// subagente como «Límites del proyecto» eran sólo los genéricos del toolkit.
|
|
267
273
|
const contract = await agent(
|
|
268
|
-
`${BASE}\n\nLeé ${ROOT}/AGENTS.md, ${CONFIG} y ${P}/PROTOCOL.md una sola vez y no
|
|
274
|
+
`${BASE}\n\nLeé ${ROOT}/AGENTS.md, ${ORG}/workspace.md, ${CONFIG} y ${P}/PROTOCOL.md una sola vez y no ` +
|
|
275
|
+
`leas nada más. Reportá los ` +
|
|
269
276
|
`valores de configuración textualmente: project, workspaceRoots como entradas "nombre → ruta", ` +
|
|
270
277
|
`runner.maxTaskHours, runner.commitPerTask y runner.humanCheckpointBetweenMilestones como humanCheckpoint. ` +
|
|
271
278
|
`En gates poné una entrada "ruta → comando" por cada workspaceRoot que declare \`verify\`, y ninguna por ` +
|
|
272
279
|
`las que no lo declaren: la lista vacía significa que el proyecto no dice con qué se verifica. ` +
|
|
273
280
|
`Copiá la sección "## Contratos" de PROTOCOL.md dentro de contracts tal cual, sin reformular, resumir ni ` +
|
|
274
|
-
`reordenar. En boundaries listá
|
|
281
|
+
`reordenar. En boundaries listá los límites que AGENTS.md enuncia y las "Excepciones de autonomía" que ` +
|
|
282
|
+
`declare ${ORG}/workspace.md, que son las de este proyecto: si ese archivo no existe o su sección sigue ` +
|
|
283
|
+
`como la trae el molde, no inventes ninguna.`,
|
|
275
284
|
{ schema: CONTRACT, label: 'contract-digest' },
|
|
276
285
|
)
|
|
277
286
|
if (!contract) return stop('contract-unavailable', `no se pudo leer ${CONFIG} ni ${P}/PROTOCOL.md`)
|
|
@@ -281,7 +290,7 @@ const limits = bounds.length ? ` Límites del proyecto: ${bounds.join('; ')}.` :
|
|
|
281
290
|
// Alcance de escritura: para subagentes que tocan código o ejecutan gates del producto.
|
|
282
291
|
const SCOPE = `${BASE}\n\nProyecto ${contract.project}. workspaceRoots es el límite completo de escritura del ` +
|
|
283
292
|
`producto: ${contract.workspaceRoots.join('; ')}.${limits} Este preámbulo ya trae el contrato; no vuelvas a leer ` +
|
|
284
|
-
`${ROOT}/AGENTS.md, ${CONFIG} ni ${P}/PROTOCOL.md.`
|
|
293
|
+
`${ROOT}/AGENTS.md, ${ORG}/workspace.md, ${CONFIG} ni ${P}/PROTOCOL.md.`
|
|
285
294
|
// Formatos de planning: sólo para subagentes que escriben roadmap, BACKLOG, WIP, DONE o gates.
|
|
286
295
|
const LEDGER = `${SCOPE}\n\nContratos de planning, textuales de ${P}/PROTOCOL.md:\n${contract.contracts}`
|
|
287
296
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Arranque de una instancia recién creada: convierte un repositorio que nadie le explicó al toolkit en
|
|
2
|
-
// contexto escrito —`organization/`, el mapa real
|
|
2
|
+
// contexto escrito —`organization/`, el mapa real en `organization/workspace.md`, las raíces— y en la primera
|
|
3
3
|
// épica.
|
|
4
4
|
//
|
|
5
5
|
// El orden importa y se pagó caro: la primera versión le pedía a un agente que «inventariara el
|
|
@@ -153,7 +153,8 @@ phase('Draft')
|
|
|
153
153
|
const drafted = await agent(
|
|
154
154
|
`${BASE}\n\n${EVIDENCE}\n\nEscribí de una sola pasada el contexto de esta instancia, reemplazando el ` +
|
|
155
155
|
`molde en vez de comentarlo:\n` +
|
|
156
|
-
`1. ${ORG}/company.md y ${ORG}/
|
|
156
|
+
`1. ${ORG}/company.md, ${ORG}/product.md y ${ORG}/domains.md: lo que la persona contó y los nombres ` +
|
|
157
|
+
`del repositorio ` +
|
|
157
158
|
`permiten afirmar. Lo que nada sostiene queda "Por definir" y su pregunta va a openQuestions. Una ` +
|
|
158
159
|
`dimensión que no llegaste a preguntar no se completa deduciéndola: queda "Por definir" con su ` +
|
|
159
160
|
`pregunta en openQuestions, aunque puedas imaginar la respuesta. Y lo que sí deducís de otra ` +
|
|
@@ -161,9 +162,14 @@ const drafted = await agent(
|
|
|
161
162
|
`lo que alguien dijo. No des ` +
|
|
162
163
|
`por sentado que el proyecto vende algo: puede sostenerse con donaciones, presupuesto interno o ` +
|
|
163
164
|
`trabajo voluntario, y una sección que no aplica se dice, no se completa con algo plausible.\n` +
|
|
164
|
-
`2.
|
|
165
|
-
`sus
|
|
166
|
-
`
|
|
165
|
+
`2. ${ORG}/workspace.md, que es donde vive lo que sólo sabe este proyecto y \`upgrade\` no toca, con ` +
|
|
166
|
+
`sus tres secciones. En "## Mapa real", una entrada por servicio con su ruta, su runtime y sus ` +
|
|
167
|
+
`comandos **tal como los declara**, diciendo de qué archivo salió cada uno. No afirmes que ` +
|
|
168
|
+
`funcionan: nadie los corrió. Un servicio sin comandos declarados se escribe así, que es ` +
|
|
169
|
+
`información. En "## Integraciones y ambientes", las credenciales que cada una necesita, porque ` +
|
|
170
|
+
`\`check\` cruza ese archivo para avisar de las que nadie carga. Y "## Excepciones de autonomía" ` +
|
|
171
|
+
`sólo si el proyecto declaró alguna: lo que nada sostiene queda "Por definir" con su pregunta en ` +
|
|
172
|
+
`openQuestions.\n` +
|
|
167
173
|
`3. ${ROOT}/ops.config.json: dejá en workspaceRoots las raíces de código reales, que es lo que un guard ` +
|
|
168
174
|
`usa para bloquear una escritura fuera de lugar. Una raíz de más lo apaga.\n` +
|
|
169
175
|
`${services.length ? '' : 'Sin servicios, el mapa queda declarado como pendiente, diciendo qué lo ' +
|
package/engine/cli/instance.js
CHANGED
|
@@ -9,7 +9,6 @@ const fs = require('node:fs')
|
|
|
9
9
|
const path = require('node:path')
|
|
10
10
|
const F = require('../core/files')
|
|
11
11
|
const O = require('../core/ownership')
|
|
12
|
-
const CL = require('../core/changelog')
|
|
13
12
|
const M = require('../core/manifest')
|
|
14
13
|
const OB = require('../core/onboarding')
|
|
15
14
|
const P = require('../planning/parser')
|
|
@@ -17,6 +16,7 @@ const ST = require('../planning/state')
|
|
|
17
16
|
const A = require('../automation')
|
|
18
17
|
const { fail } = require('./io')
|
|
19
18
|
const { declareEngine, pinEngine, undeclareEngine } = require('./dependency')
|
|
19
|
+
const { adviceFor, previewUpgrade, reportUpgrade } = require('./upgrade-report')
|
|
20
20
|
|
|
21
21
|
const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
|
|
22
22
|
|
|
@@ -134,17 +134,6 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
|
|
|
134
134
|
return root
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
// Qué trae la versión nueva, leído del paquete: sin esto el reemplazo de system/ es a ciegas.
|
|
138
|
-
function printChangelog(from, to) {
|
|
139
|
-
const notes = CL.between(CL.read(PROJECT_ROOT), from, to)
|
|
140
|
-
if (!notes.length) return
|
|
141
|
-
for (const note of notes) {
|
|
142
|
-
console.log(`\n ── ${note.version} ──`)
|
|
143
|
-
for (const line of note.body.split('\n')) if (line.trim()) console.log(` ${line}`)
|
|
144
|
-
}
|
|
145
|
-
console.log('')
|
|
146
|
-
}
|
|
147
|
-
|
|
148
137
|
function instanceVersion(root) {
|
|
149
138
|
try {
|
|
150
139
|
return JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8')).cauceVersion || ''
|
|
@@ -262,53 +251,6 @@ function renameTeamsToFlows(root) {
|
|
|
262
251
|
return moved
|
|
263
252
|
}
|
|
264
253
|
|
|
265
|
-
// Qué hacer en vez de haber editado, según a quién pertenece cada archivo que se perdería. Vive
|
|
266
|
-
// aparte de `upgrade` porque su reloj es el de la frontera de propiedad y el de la redacción del
|
|
267
|
-
// consejo, no el del procedimiento que lo imprime, y porque así se puede probar sin tocar el disco.
|
|
268
|
-
//
|
|
269
|
-
// Tres clases distintas, y antes eran dos: todo lo que no vivía bajo `system/` recibía el consejo del
|
|
270
|
-
// runtime, así que editar el protocolo respondía con cómo desactivar un guard. Decirle a alguien la
|
|
271
|
-
// salida ajena lo manda a buscar una configuración que no existe.
|
|
272
|
-
function adviceFor(changed) {
|
|
273
|
-
const ruleFiles = changed.filter((file) => file.includes('/system/'))
|
|
274
|
-
const runtime = changed.filter((file) => !file.includes('/system/')
|
|
275
|
-
&& O.RUNTIME_PATHS.some((base) => file.startsWith(`${base}/`)))
|
|
276
|
-
const docs = changed.filter((file) => !ruleFiles.includes(file) && !runtime.includes(file))
|
|
277
|
-
const advice = []
|
|
278
|
-
if (ruleFiles.length) {
|
|
279
|
-
advice.push(
|
|
280
|
-
'Las ruleFiles y decisiones bajo system/ son del toolkit. Para cambiar una, escribí la tuya al\n'
|
|
281
|
-
+ 'lado con el mismo ID: el proyecto manda y `check` lo reporta como override explícito.',
|
|
282
|
-
)
|
|
283
|
-
}
|
|
284
|
-
if (runtime.length) {
|
|
285
|
-
advice.push(
|
|
286
|
-
'El runtime es del toolkit: en vez de editarlo, agregá lo tuyo al lado con otro nombre —un\n'
|
|
287
|
-
+ 'guard propio sobrevive a cada actualización— y registralo en la configuración de tu runner,\n'
|
|
288
|
-
+ 'que sí es del proyecto. Para desactivar un guard alcanza con quitarlo de esa configuración.',
|
|
289
|
-
)
|
|
290
|
-
}
|
|
291
|
-
if (docs.length) {
|
|
292
|
-
advice.push(
|
|
293
|
-
'Esos docs son del toolkit y no llevan una línea de la empresa: se reemplazan enteros en\n'
|
|
294
|
-
+ 'cada actualización para que las mejoras lleguen. Lo que tu proyecto decide distinto va donde sí\n'
|
|
295
|
-
+ 'es suyo —una ADR propia, una regla propia, o `planning/delivery/project.md` para la entrega—.',
|
|
296
|
-
)
|
|
297
|
-
}
|
|
298
|
-
// `AGENTS.md` se lo gana aparte porque hasta ahora el README mandaba completarlo, así que el consejo
|
|
299
|
-
// genérico de arriba —«no llevan una línea de la empresa»— le miente justo a quien le hizo caso.
|
|
300
|
-
if (docs.includes('AGENTS.md')) {
|
|
301
|
-
advice.push(
|
|
302
|
-
'En `AGENTS.md` en particular: el mapa real, las integraciones y las excepciones de autonomía\n'
|
|
303
|
-
+ 'ahora van en `organization/workspace.md`, que es del proyecto y no se reemplaza. Movelos ahí\n'
|
|
304
|
-
+ 'antes de repetir con --force, o los vas a tener que fusionar de nuevo en cada versión.',
|
|
305
|
-
)
|
|
306
|
-
}
|
|
307
|
-
return advice.join('\n\n')
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Actualiza sólo lo que el toolkit declara suyo. Todo lo demás —planning, organization, reglas
|
|
311
|
-
// propias, agentes editados— queda intacto por construcción, no por comparación.
|
|
312
254
|
function upgrade(dir, cli) {
|
|
313
255
|
const root = path.resolve(dir || '.')
|
|
314
256
|
if (!fs.existsSync(path.join(root, 'ops.config.json'))) {
|
|
@@ -333,34 +275,12 @@ function upgrade(dir, cli) {
|
|
|
333
275
|
const changed = O.localChanges(root)
|
|
334
276
|
const overrides = O.overrides(root)
|
|
335
277
|
|
|
278
|
+
// El código de salida lo aplica acá y no adentro: `--check` mira y cuenta, y quien decide qué hacer
|
|
279
|
+
// con lo que vio es el comando. Escondido en la función que informa, el corte del flujo no se ve.
|
|
336
280
|
if (dry) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
for (const file of changed) console.log(` editado localmente: ${file}`)
|
|
341
|
-
if (from === to) {
|
|
342
|
-
// Contra el motor instalado, no contra lo publicado: la comparación es local y sin red. Decirlo
|
|
343
|
-
// importa porque `init` fija la versión exacta, así que el motor no se mueve solo y esta línea,
|
|
344
|
-
// a secas, se leía como «no hay nada nuevo» durante todas las versiones siguientes.
|
|
345
|
-
console.log(`= ${to}: la instancia está al día con el motor instalado`)
|
|
346
|
-
// Con `--save-exact`, sin el cual el caret de npm vuelve falsa la línea de arriba. Por qué, y por
|
|
347
|
-
// qué no alcanza con documentarlo, en `pinEngine`.
|
|
348
|
-
console.log(' para traer una versión más nueva:'
|
|
349
|
-
+ ' npm install --save-exact --save-dev @ingeniomaps/cauce@latest')
|
|
350
|
-
if (changed.length) process.exit(1)
|
|
351
|
-
return
|
|
352
|
-
}
|
|
353
|
-
// Hacia atrás también es legítimo —una versión rompió algo y se vuelve—, pero anunciarlo como «hay
|
|
354
|
-
// una versión más nueva» era mentir con el número a la vista. Y lo que corresponde imprimir es lo
|
|
355
|
-
// contrario: no lo que se gana, sino lo que se deja.
|
|
356
|
-
if (CL.compare(to, from) < 0) {
|
|
357
|
-
console.log(`↩ volvés a ${to} desde ${from}. Esto es lo que dejás de tener:`)
|
|
358
|
-
printChangelog(to, from)
|
|
359
|
-
} else {
|
|
360
|
-
console.log(`⚠ hay una versión más nueva: ${to} (la instancia tiene ${from || 'una previa'})`)
|
|
361
|
-
printChangelog(from, to)
|
|
362
|
-
}
|
|
363
|
-
process.exit(1)
|
|
281
|
+
const code = previewUpgrade({ from, to, changed })
|
|
282
|
+
if (code) process.exit(code)
|
|
283
|
+
return
|
|
364
284
|
}
|
|
365
285
|
|
|
366
286
|
// Antes de retirar nada, comprobar que no se lleve puesto aprendizaje acumulado.
|
|
@@ -385,7 +305,7 @@ function upgrade(dir, cli) {
|
|
|
385
305
|
// incluye a propósito —lo reemplazaría en cada actualización, que es lo que un archivo del proyecto
|
|
386
306
|
// no debe sufrir— así que sin este paso no llega por ninguna vía, y la instancia queda leyendo una
|
|
387
307
|
// instrucción que apunta a un archivo que no tiene.
|
|
388
|
-
const
|
|
308
|
+
const config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
|
|
389
309
|
const added = []
|
|
390
310
|
for (const relative of O.addedPaths()) {
|
|
391
311
|
const target = path.join(root, relative)
|
|
@@ -396,7 +316,7 @@ function upgrade(dir, cli) {
|
|
|
396
316
|
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
397
317
|
let content = fs.readFileSync(origin, 'utf8')
|
|
398
318
|
for (const [key, value] of Object.entries({
|
|
399
|
-
'{{PROJECT_NAME}}':
|
|
319
|
+
'{{PROJECT_NAME}}': config.project || path.basename(root),
|
|
400
320
|
'{{MODE}}': O.mode(root),
|
|
401
321
|
'{{WORKSPACE_PATH}}': O.mode(root) === 'embedded' ? '.' : '..',
|
|
402
322
|
})) content = content.replaceAll(key, value)
|
|
@@ -404,6 +324,21 @@ function upgrade(dir, cli) {
|
|
|
404
324
|
added.push(relative)
|
|
405
325
|
}
|
|
406
326
|
|
|
327
|
+
// Qué bloque de runner se lleva puesto el reemplazo. `AGENTS.md` es del toolkit y se reemplaza
|
|
328
|
+
// entero, con el bloque que `automation install` dejó adentro: la instancia lo recupera reinstalando
|
|
329
|
+
// —el recordatorio de abajo ya lo pide— pero sin decir esto ese recordatorio no tiene causa visible,
|
|
330
|
+
// y quien mire su archivo ve el bloque desaparecido sin explicación.
|
|
331
|
+
const droppedBlocks = []
|
|
332
|
+
for (const runner of [...new Set(Object.keys(M.readRunners(root)).map((key) => key.split('/')[0]))]) {
|
|
333
|
+
// Sólo los archivos sueltos: `system` trae también colecciones, y un bloque de runner vive en un
|
|
334
|
+
// archivo de instrucciones —hoy `AGENTS.md`—, nunca dentro de un directorio del sistema.
|
|
335
|
+
for (const relative of O.SYSTEM_FILES) {
|
|
336
|
+
const file = path.join(root, relative)
|
|
337
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) continue
|
|
338
|
+
if (fs.readFileSync(file, 'utf8').includes(A.blockStart(runner))) droppedBlocks.push({ runner, relative })
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
407
342
|
for (const relative of [...system, ...O.RUNTIME_PATHS]) {
|
|
408
343
|
const origin = path.join(PROJECT_ROOT, O.sourceOf(relative))
|
|
409
344
|
if (!fs.existsSync(origin)) continue
|
|
@@ -445,50 +380,12 @@ function upgrade(dir, cli) {
|
|
|
445
380
|
))
|
|
446
381
|
M.write(root, M.prune(root, record), null, kept)
|
|
447
382
|
|
|
448
|
-
const config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
|
|
449
383
|
config.cauceVersion = to
|
|
450
384
|
F.atomicWriteJson(path.join(root, 'ops.config.json'), config)
|
|
451
385
|
// Acá es donde las tres versiones se saben a la vez, así que es donde pueden quedar diciendo lo mismo.
|
|
452
386
|
const pinned = pinEngine(root, to)
|
|
453
387
|
|
|
454
|
-
|
|
455
|
-
// Descartar con --force es legítimo; hacerlo sin dejar rastro no. Queda en la salida del comando,
|
|
456
|
-
// que es la evidencia que el protocolo pide para cualquier cambio.
|
|
457
|
-
for (const file of changed) console.log(`− descartado tu cambio en ${file}`)
|
|
458
|
-
for (const relative of retired) console.log(`− retirado ${relative}: Cauce ya no lo distribuye`)
|
|
459
|
-
// Nombrarlos importa tanto como crearlos: existen para que los completes, y uno que aparece sin
|
|
460
|
-
// aviso no lo completa nadie.
|
|
461
|
-
for (const relative of added) console.log(`+ ${relative}: lo agrega esta versión, completalo`)
|
|
462
|
-
// Se dice porque explica un diff en un archivo que la empresa versiona, y que si no aparecería sin
|
|
463
|
-
// autor.
|
|
464
|
-
if (pinned === 'ilegible') console.log(' ⚠ package.json no se pudo leer: su versión quedó como estaba')
|
|
465
|
-
else if (pinned) console.log(` package.json: ${pinned} → ${to}, la versión exacta que acabás de aplicar`)
|
|
466
|
-
printChangelog(from, to)
|
|
467
|
-
console.log(` ${system.length} ruta(s) del sistema y ${O.RUNTIME_PATHS.length} del runtime actualizadas`)
|
|
468
|
-
for (const override of overrides) {
|
|
469
|
-
console.log(`= conservado ${override.collection}/${override.project}: sobrescribe ${override.system}`)
|
|
470
|
-
}
|
|
471
|
-
// Sólo cuando es cierto: llegar acá con algo en `changed` es haber descartado contenido de la
|
|
472
|
-
// empresa con --force, que las líneas de arriba enumeran.
|
|
473
|
-
if (!changed.length) console.log(' planning, organization y todo lo propio quedaron intactos')
|
|
474
|
-
// No se borra: sin la dependencia declarada, quitarle `.ops/` la dejaría sin motor. Se avisa y
|
|
475
|
-
// decide una persona.
|
|
476
|
-
if (fs.existsSync(path.join(root, '.ops', 'engine'))) {
|
|
477
|
-
console.log('\n⚠ esta instancia tiene el motor vendorizado en .ops/, que Cauce ya no distribuye.')
|
|
478
|
-
console.log(' Corré "npm install" para tenerlo como dependencia y después borrá .ops/ a mano.')
|
|
479
|
-
}
|
|
480
|
-
// El wiring del runner no se actualiza solo: vive fuera de la instancia y lo escribe otro comando.
|
|
481
|
-
// Sin este recordatorio, una mejora en un workflow o en el catálogo se queda en el paquete.
|
|
482
|
-
const runners = Object.keys(M.readRunners(root))
|
|
483
|
-
.map((key) => key.split('/')[0])
|
|
484
|
-
.filter((name, index, all) => all.indexOf(name) === index)
|
|
485
|
-
for (const name of runners) {
|
|
486
|
-
console.log(` reinstalá tu runner para que el wiring quede al día: make install-${name}`)
|
|
487
|
-
}
|
|
488
|
-
// Después de aplicar, no antes: recién acá el paquete tiene la versión nueva y la comparación dice
|
|
489
|
-
// algo. Es además el momento en que alguien está mirando qué le trajo la actualización.
|
|
490
|
-
const FK = require('../agents/fork')
|
|
491
|
-
for (const entry of FK.drift(root)) console.log(` ⚠ ${FK.driftLine(entry)}`)
|
|
388
|
+
reportUpgrade({ root, from, to, system, changed, retired, added, overrides, pinned, droppedBlocks })
|
|
492
389
|
}
|
|
493
390
|
|
|
494
|
-
module.exports = { copyTemplate, scaffold, providerNames,
|
|
391
|
+
module.exports = { copyTemplate, scaffold, providerNames, upgrade, destroy, PROJECT_ROOT }
|
package/engine/cli/planning.js
CHANGED
|
@@ -77,6 +77,7 @@ function check(dir, cli) {
|
|
|
77
77
|
errors.push(...integration.errors)
|
|
78
78
|
warnings.push(...integration.warnings)
|
|
79
79
|
|
|
80
|
+
warnings.push(...PC.competingSections(root))
|
|
80
81
|
// Sobrescribir una entrada de system/ es legítimo y esperado; lo que no puede pasar es que
|
|
81
82
|
// ocurra en silencio, porque esa entrada deja de recibir las mejoras del toolkit.
|
|
82
83
|
for (const override of O.overrides(path.resolve(root, '..'))) {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Lo que `upgrade` le dice a quien lo corrió: qué hacer en vez de haber editado, y qué le pasó a la
|
|
4
|
+
// instancia. Vive aparte del ciclo de vida porque su reloj es el de lo que hay para decir —la frontera
|
|
5
|
+
// de propiedad, la redacción de un consejo, cada versión que agrega o retira algo— y no el del
|
|
6
|
+
// procedimiento que lo hace. Y porque así se prueba sin tocar el disco.
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs')
|
|
9
|
+
const path = require('node:path')
|
|
10
|
+
const O = require('../core/ownership')
|
|
11
|
+
const M = require('../core/manifest')
|
|
12
|
+
const CL = require('../core/changelog')
|
|
13
|
+
|
|
14
|
+
const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
|
|
15
|
+
|
|
16
|
+
// Qué hacer en vez de haber editado, según a quién pertenece cada archivo que se perdería.
|
|
17
|
+
//
|
|
18
|
+
// Tres clases distintas, y antes eran dos: todo lo que no vivía bajo `system/` recibía el consejo del
|
|
19
|
+
// runtime, así que editar el protocolo respondía con cómo desactivar un guard. Decirle a alguien la
|
|
20
|
+
// salida ajena lo manda a buscar una configuración que no existe.
|
|
21
|
+
function adviceFor(changed) {
|
|
22
|
+
const ruleFiles = changed.filter((file) => file.includes('/system/'))
|
|
23
|
+
const runtime = changed.filter((file) => !file.includes('/system/')
|
|
24
|
+
&& O.RUNTIME_PATHS.some((base) => file.startsWith(`${base}/`)))
|
|
25
|
+
const docs = changed.filter((file) => !ruleFiles.includes(file) && !runtime.includes(file))
|
|
26
|
+
const advice = []
|
|
27
|
+
if (ruleFiles.length) {
|
|
28
|
+
advice.push(
|
|
29
|
+
'Las ruleFiles y decisiones bajo system/ son del toolkit. Para cambiar una, escribí la tuya al\n'
|
|
30
|
+
+ 'lado con el mismo ID: el proyecto manda y `check` lo reporta como override explícito.',
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
if (runtime.length) {
|
|
34
|
+
advice.push(
|
|
35
|
+
'El runtime es del toolkit: en vez de editarlo, agregá lo tuyo al lado con otro nombre —un\n'
|
|
36
|
+
+ 'guard propio sobrevive a cada actualización— y registralo en la configuración de tu runner,\n'
|
|
37
|
+
+ 'que sí es del proyecto. Para desactivar un guard alcanza con quitarlo de esa configuración.',
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
if (docs.length) {
|
|
41
|
+
advice.push(
|
|
42
|
+
'Esos docs son del toolkit y no llevan una línea de la empresa: se reemplazan enteros en\n'
|
|
43
|
+
+ 'cada actualización para que las mejoras lleguen. Lo que tu proyecto decide distinto va donde sí\n'
|
|
44
|
+
+ 'es suyo —una ADR propia, una regla propia, o `planning/delivery/project.md` para la entrega—.',
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
// `AGENTS.md` se lo gana aparte porque hasta ahora el README mandaba completarlo, así que el consejo
|
|
48
|
+
// genérico de arriba —«no llevan una línea de la empresa»— le miente justo a quien le hizo caso.
|
|
49
|
+
if (docs.includes('AGENTS.md')) {
|
|
50
|
+
advice.push(
|
|
51
|
+
'En `AGENTS.md` en particular: el mapa real, las integraciones y las excepciones de autonomía\n'
|
|
52
|
+
+ 'ahora van en `organization/workspace.md`, que es del proyecto y no se reemplaza. Movelos ahí\n'
|
|
53
|
+
+ 'antes de repetir con --force, o los vas a tener que fusionar de nuevo en cada versión.',
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
return advice.join('\n\n')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Actualiza sólo lo que el toolkit declara suyo. Todo lo demás —planning, organization, reglas
|
|
60
|
+
// propias, agentes editados— queda intacto por construcción, no por comparación.
|
|
61
|
+
// Qué le pasó a la instancia en esta actualización, contado a quien la corrió. Recibe el resultado
|
|
62
|
+
// entero en vez de recalcular nada: lo que se informa es exactamente lo que ocurrió.
|
|
63
|
+
function reportUpgrade({ root, from, to, system, changed, retired, added, overrides, pinned, droppedBlocks }) {
|
|
64
|
+
console.log(`✓ Cauce ${from || '(previa)'} → ${to}`)
|
|
65
|
+
// Descartar con --force es legítimo; hacerlo sin dejar rastro no. Queda en la salida del comando,
|
|
66
|
+
// que es la evidencia que el protocolo pide para cualquier cambio.
|
|
67
|
+
for (const file of changed) console.log(`− descartado tu cambio en ${file}`)
|
|
68
|
+
for (const relative of retired) console.log(`− retirado ${relative}: Cauce ya no lo distribuye`)
|
|
69
|
+
// Nombrarlos importa tanto como crearlos: existen para que los completes, y uno que aparece sin
|
|
70
|
+
// aviso no lo completa nadie.
|
|
71
|
+
for (const relative of added) console.log(`+ ${relative}: lo agrega esta versión, completalo`)
|
|
72
|
+
// Se dice porque explica un diff en un archivo que la empresa versiona, y que si no aparecería sin
|
|
73
|
+
// autor.
|
|
74
|
+
if (pinned === 'ilegible') console.log(' ⚠ package.json no se pudo leer: su versión quedó como estaba')
|
|
75
|
+
else if (pinned) console.log(` package.json: ${pinned} → ${to}, la versión exacta que acabás de aplicar`)
|
|
76
|
+
printChangelog(from, to)
|
|
77
|
+
console.log(` ${system.length} ruta(s) del sistema y ${O.RUNTIME_PATHS.length} del runtime actualizadas`)
|
|
78
|
+
for (const override of overrides) {
|
|
79
|
+
console.log(`= conservado ${override.collection}/${override.project}: sobrescribe ${override.system}`)
|
|
80
|
+
}
|
|
81
|
+
// Sólo cuando es cierto: llegar acá con algo en `changed` es haber descartado contenido de la
|
|
82
|
+
// empresa con --force, que las líneas de arriba enumeran.
|
|
83
|
+
if (!changed.length) console.log(' planning, organization y todo lo propio quedaron intactos')
|
|
84
|
+
// No se borra: sin la dependencia declarada, quitarle `.ops/` la dejaría sin motor. Se avisa y
|
|
85
|
+
// decide una persona.
|
|
86
|
+
if (fs.existsSync(path.join(root, '.ops', 'engine'))) {
|
|
87
|
+
console.log('\n⚠ esta instancia tiene el motor vendorizado en .ops/, que Cauce ya no distribuye.')
|
|
88
|
+
console.log(' Corré "npm install" para tenerlo como dependencia y después borrá .ops/ a mano.')
|
|
89
|
+
}
|
|
90
|
+
// El wiring del runner no se actualiza solo: vive fuera de la instancia y lo escribe otro comando.
|
|
91
|
+
// Sin este recordatorio, una mejora en un workflow o en el catálogo se queda en el paquete.
|
|
92
|
+
for (const { runner, relative } of droppedBlocks) {
|
|
93
|
+
console.log(`− ${relative}: se reemplazó entero y con él las instrucciones de ${runner}`)
|
|
94
|
+
}
|
|
95
|
+
const runners = Object.keys(M.readRunners(root))
|
|
96
|
+
.map((key) => key.split('/')[0])
|
|
97
|
+
.filter((name, index, all) => all.indexOf(name) === index)
|
|
98
|
+
for (const name of runners) {
|
|
99
|
+
console.log(` reinstalá tu runner para que el wiring quede al día: make install-${name}`)
|
|
100
|
+
}
|
|
101
|
+
// Después de aplicar, no antes: recién acá el paquete tiene la versión nueva y la comparación dice
|
|
102
|
+
// algo. Es además el momento en que alguien está mirando qué le trajo la actualización.
|
|
103
|
+
const FK = require('../agents/fork')
|
|
104
|
+
for (const entry of FK.drift(root)) console.log(` ⚠ ${FK.driftLine(entry)}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Qué trae la versión nueva, leído del paquete: sin esto el reemplazo de system/ es a ciegas.
|
|
108
|
+
function printChangelog(from, to) {
|
|
109
|
+
const notes = CL.between(CL.read(PROJECT_ROOT), from, to)
|
|
110
|
+
if (!notes.length) return
|
|
111
|
+
for (const note of notes) {
|
|
112
|
+
console.log(`\n ── ${note.version} ──`)
|
|
113
|
+
for (const line of note.body.split('\n')) if (line.trim()) console.log(` ${line}`)
|
|
114
|
+
}
|
|
115
|
+
console.log('')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Mirar sin tocar: qué traería la actualización y qué se perdería si se aplicara. Devuelve el código de
|
|
119
|
+
// salida en vez de cortar el proceso, para que el corte se vea donde se decide.
|
|
120
|
+
//
|
|
121
|
+
// Lo editado localmente va antes de mirar versiones, porque son dos preguntas distintas —«¿hay algo más
|
|
122
|
+
// nuevo?» y «¿qué tengo editado que se perdería?»— y la segunda tiene respuesta útil aunque la primera
|
|
123
|
+
// sea que no. Adentro del `if` de abajo, el único modo que no toca nada era el único que no podía avisar.
|
|
124
|
+
function previewUpgrade({ from, to, changed }) {
|
|
125
|
+
for (const file of changed) console.log(` editado localmente: ${file}`)
|
|
126
|
+
if (from === to) {
|
|
127
|
+
// Contra el motor instalado, no contra lo publicado: la comparación es local y sin red. Decirlo
|
|
128
|
+
// importa porque `init` fija la versión exacta, así que el motor no se mueve solo y esta línea,
|
|
129
|
+
// a secas, se leía como «no hay nada nuevo» durante todas las versiones siguientes.
|
|
130
|
+
console.log(`= ${to}: la instancia está al día con el motor instalado`)
|
|
131
|
+
// Con `--save-exact`, sin el cual el caret de npm vuelve falsa la línea de arriba. Por qué, y por
|
|
132
|
+
// qué no alcanza con documentarlo, en `pinEngine`.
|
|
133
|
+
console.log(' para traer una versión más nueva:'
|
|
134
|
+
+ ' npm install --save-exact --save-dev @ingeniomaps/cauce@latest')
|
|
135
|
+
return changed.length ? 1 : 0
|
|
136
|
+
}
|
|
137
|
+
// Hacia atrás también es legítimo —una versión rompió algo y se vuelve—, pero anunciarlo como «hay
|
|
138
|
+
// una versión más nueva» era mentir con el número a la vista. Y lo que corresponde imprimir es lo
|
|
139
|
+
// contrario: no lo que se gana, sino lo que se deja.
|
|
140
|
+
if (CL.compare(to, from) < 0) {
|
|
141
|
+
console.log(`↩ volvés a ${to} desde ${from}. Esto es lo que dejás de tener:`)
|
|
142
|
+
printChangelog(to, from)
|
|
143
|
+
} else {
|
|
144
|
+
console.log(`⚠ hay una versión más nueva: ${to} (la instancia tiene ${from || 'una previa'})`)
|
|
145
|
+
printChangelog(from, to)
|
|
146
|
+
}
|
|
147
|
+
return 1
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { adviceFor, previewUpgrade, reportUpgrade }
|
|
@@ -131,10 +131,14 @@ function validate(root, onlyProvider = '') {
|
|
|
131
131
|
if (typeof snapshot.sync.missingFromRemote !== 'boolean') {
|
|
132
132
|
errors.push(`${at}: missingFromRemote debe ser boolean`)
|
|
133
133
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
// `draftChanged` no se valida contra el contenido, y es deliberado: el README manda curar el draft
|
|
135
|
+
// a mano y ese camino no recalcula el flag, así que exigir la coincidencia ponía en rojo el paso
|
|
136
|
+
// siguiente del recorrido documentado, todas las veces.
|
|
137
|
+
//
|
|
138
|
+
// Lo que se perdería sería una señal que no existe. El campo se escribe en `sync`, `rebase` y
|
|
139
|
+
// `reconcile`, y se leía en un solo lugar: esta comprobación, que lo comparaba consigo mismo
|
|
140
|
+
// recalculado. Lo que decide qué vuelve al remoto es `S.derive`, que compara el contenido contra
|
|
141
|
+
// la base y nunca lo consulta. Un derivado que se puede recalcular no es un error de validación.
|
|
138
142
|
const signals = S.derive(snapshot, draft)
|
|
139
143
|
if (snapshot.sync.role === 'context' && fields.state !== 'context') {
|
|
140
144
|
errors.push(`${at}: context exige state=context`)
|
|
@@ -324,8 +328,8 @@ function promote(root, name, key) {
|
|
|
324
328
|
const target = path.join(roadmap, recovered || `epic-${num}-${slug}.md`)
|
|
325
329
|
const content = `---\nepic: ${num}\ntitle: ${sections.title}\nstatus: open\n` +
|
|
326
330
|
`service: ${fields.service}\nsource: ${name}\nremote: ${key}\n---\n\n` +
|
|
327
|
-
`# Épica ${num} — ${sections.title}\n\n## Resultado\n\n${sections.Descripción}\n\n` +
|
|
328
|
-
`## Criterios\n\n- **C1** — ${
|
|
331
|
+
`# Épica ${num} — ${sections.title}\n\n## Resultado\n\n${nestHeadings(sections.Descripción)}\n\n` +
|
|
332
|
+
`## Criterios\n\n- **C1** — ${criterionText(sections.Aceptación)}\n\n` +
|
|
329
333
|
`## Contexto relevante\n\n- Importado desde ${name}:${key}; validar rutas del servicio antes de activar.\n\n` +
|
|
330
334
|
`## Historias\n\n- [ ] **${slug}** (→ C1) — ${oneLine(sections.title)}. ` +
|
|
331
335
|
`(service: ${fields.service})\n`
|
|
@@ -383,6 +387,17 @@ const slugify = (value) => String(value).normalize('NFD').replace(/[\u0300-\u036
|
|
|
383
387
|
.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 70)
|
|
384
388
|
const oneLine = (value) => String(value).replace(/\s+/g, ' ').trim()
|
|
385
389
|
|
|
390
|
+
// La descripción remota entra dentro de `## Resultado`, y trae sus propios encabezados: el fixture del
|
|
391
|
+
// propio toolkit trae `## Criterios de aceptación`. En el mismo nivel compiten con las secciones que
|
|
392
|
+
// escribe la épica, y el parser toma la primera que case: leía la importada, que no tiene ninguna
|
|
393
|
+
// viñeta `- **CN** —`, y `check` rechazaba la épica recién promovida. Bajarlos un nivel los deja
|
|
394
|
+
// legibles y fuera de la competencia, y cubre igual un `## Historias` o un `## Resultado` remotos.
|
|
395
|
+
const nestHeadings = (text) => String(text).replace(/^(#{1,5})\s+/gm, '#$1 ')
|
|
396
|
+
|
|
397
|
+
// El texto de un criterio importado, sin el guión de la viñeta de la que salió: sin esto el criterio
|
|
398
|
+
// quedaba escrito `- **C1** — - La fecha…`, con los dos guiones pegados.
|
|
399
|
+
const criterionText = (value) => oneLine(String(value).replace(/^\s*[-*]\s+/, ''))
|
|
400
|
+
|
|
386
401
|
// En cuál de tres estados está el proveedor: `ready`, `incomplete` o `unreadable`. Lee su propio
|
|
387
402
|
// `config.json`, que es donde vive la decisión, y no el registro: un proveedor listado y apagado no es
|
|
388
403
|
// un proveedor disponible.
|
|
@@ -398,6 +413,8 @@ function providerState(root, name) {
|
|
|
398
413
|
module.exports = {
|
|
399
414
|
providerState,
|
|
400
415
|
adapter,
|
|
416
|
+
criterionText,
|
|
417
|
+
nestHeadings,
|
|
401
418
|
frontmatter,
|
|
402
419
|
promote,
|
|
403
420
|
providerConfig,
|
|
@@ -177,6 +177,31 @@ function ruleIds(file) {
|
|
|
177
177
|
// veía es la consecuencia, porque la advertencia nombraba el par de archivos y no la diferencia. El
|
|
178
178
|
// caso caro es una regla que el motor sigue exigiendo —R17 lo hace—: queda exigida y sin estar escrita
|
|
179
179
|
// en ningún lado, y quien la vea fallar la va a buscar en `rules/`, donde ya no está.
|
|
180
|
+
// Dos secciones de una épica que compiten por el mismo rol. El parser prefiere la exacta, así que
|
|
181
|
+
// resuelve —y en silencio: quien escribió las dos no se entera de que una se ignora entera. La
|
|
182
|
+
// promoción dejó de generarlas cuando lo importado empezó a bajar un nivel; a mano se siguen pudiendo
|
|
183
|
+
// escribir, y ahí el aviso es lo único que lo dice.
|
|
184
|
+
function competingSections(dir) {
|
|
185
|
+
const roadmap = path.join(dir, 'roadmap')
|
|
186
|
+
const avisos = []
|
|
187
|
+
let files = []
|
|
188
|
+
try { files = fs.readdirSync(roadmap).filter((file) => /^epic-\d{3}-/.test(file)) } catch { return [] }
|
|
189
|
+
for (const file of files.sort()) {
|
|
190
|
+
const text = P.read(path.join(roadmap, file))
|
|
191
|
+
const titles = [...text.matchAll(/^##\s+(.+)$/gm)].map((hit) => hit[1].trim())
|
|
192
|
+
for (const role of [/Criterios/i, /Historias/i]) {
|
|
193
|
+
const casan = titles.filter((title) => role.test(title))
|
|
194
|
+
if (casan.length < 2) continue
|
|
195
|
+
const exact = new RegExp(`^${role.source}$`, role.flags)
|
|
196
|
+
const gana = casan.find((title) => exact.test(title)) || casan[0]
|
|
197
|
+
const ignoradas = casan.filter((title) => title !== gana)
|
|
198
|
+
avisos.push(`roadmap/${file}: "## ${gana}" convive con "## ${ignoradas.join('", "## ')}"; `
|
|
199
|
+
+ 'sólo se lee la primera y el resto se ignora entero')
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return avisos
|
|
203
|
+
}
|
|
204
|
+
|
|
180
205
|
function retiredByOverride(dir, name) {
|
|
181
206
|
const rules = path.join(dir, 'rules')
|
|
182
207
|
const system = path.join(rules, 'system', name)
|
|
@@ -432,6 +457,7 @@ module.exports = {
|
|
|
432
457
|
validateAdr,
|
|
433
458
|
validateRules,
|
|
434
459
|
retiredByOverride,
|
|
460
|
+
competingSections,
|
|
435
461
|
validateBacklogStructure,
|
|
436
462
|
validCommitTrace,
|
|
437
463
|
validDecisionTrace,
|
|
@@ -46,9 +46,17 @@ function frontmatter(text) {
|
|
|
46
46
|
return (key) => values[key] || ''
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// La coincidencia exacta gana sobre la parcial, y sólo si no hay exacta se acepta la parcial. Con
|
|
50
|
+
// `find` a secas, `## Criterios de aceptación` le ganaba a `## Criterios` por estar antes: la épica que
|
|
51
|
+
// promovía una integración traía la sección importada primero y el parser leía esa, que no tiene
|
|
52
|
+
// ninguna viñeta `- **CN** —`. Anclar del todo habría roto lo contrario —`## Historias (Tareas)` es un
|
|
53
|
+
// título real de una migración—, así que la parcial sigue valiendo cuando es la única.
|
|
49
54
|
function section(text, heading) {
|
|
50
55
|
const parts = text.split(/^##\s+/m)
|
|
51
|
-
|
|
56
|
+
const titles = parts.map((part) => part.split('\n')[0].trim())
|
|
57
|
+
const exact = parts.findIndex((part, i) => new RegExp(`^${heading.source}$`, heading.flags).test(titles[i]))
|
|
58
|
+
if (exact >= 0) return parts[exact]
|
|
59
|
+
return parts.find((part, i) => heading.test(titles[i])) || ''
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
function withoutComments(text) {
|