@ingeniomaps/cauce 0.69.0 → 0.71.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 +266 -0
- package/README.md +6 -6
- package/agents/README.md +38 -0
- package/agents/roles/system/accounting-specialist/learning/HISTORY.md +2 -0
- package/agents/roles/system/ai-governance-lead/learning/HISTORY.md +6 -1
- package/agents/roles/system/ai-governance-lead/learning/sources.yaml +4 -4
- package/agents/roles/system/ai-governance-lead/references/operating-model.md +2 -2
- package/agents/roles/system/ai-product-manager/learning/HISTORY.md +4 -1
- package/agents/roles/system/ai-product-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/ai-product-manager/references/operating-model.md +2 -2
- package/agents/roles/system/analytics-engineer/learning/HISTORY.md +4 -1
- package/agents/roles/system/analytics-engineer/learning/sources.yaml +2 -2
- package/agents/roles/system/analytics-engineer/references/operating-model.md +2 -2
- package/agents/roles/system/backend-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/business-strategist/learning/HISTORY.md +2 -0
- package/agents/roles/system/business-strategist/learning/sources.yaml +1 -1
- package/agents/roles/system/business-strategist/references/operating-model.md +2 -2
- package/agents/roles/system/cloud-architect/learning/HISTORY.md +1 -1
- package/agents/roles/system/cloud-architect/learning/sources.yaml +2 -2
- package/agents/roles/system/cloud-architect/references/operating-model.md +2 -2
- package/agents/roles/system/community-manager/learning/HISTORY.md +4 -1
- package/agents/roles/system/community-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/community-manager/references/operating-model.md +1 -1
- package/agents/roles/system/content-specialist/learning/HISTORY.md +2 -0
- package/agents/roles/system/customer-success-manager/learning/HISTORY.md +2 -0
- package/agents/roles/system/customer-success-manager/learning/sources.yaml +4 -4
- package/agents/roles/system/customer-success-manager/references/operating-model.md +3 -3
- package/agents/roles/system/customer-support-specialist/learning/HISTORY.md +2 -0
- package/agents/roles/system/customer-support-specialist/learning/sources.yaml +2 -2
- package/agents/roles/system/customer-support-specialist/references/operating-model.md +2 -2
- package/agents/roles/system/data-analyst/learning/HISTORY.md +2 -0
- package/agents/roles/system/data-engineer/learning/HISTORY.md +4 -1
- package/agents/roles/system/data-engineer/learning/sources.yaml +3 -3
- package/agents/roles/system/data-engineer/references/operating-model.md +2 -2
- package/agents/roles/system/data-governance-steward/learning/HISTORY.md +2 -0
- package/agents/roles/system/data-scientist/learning/HISTORY.md +4 -1
- package/agents/roles/system/data-scientist/learning/sources.yaml +3 -3
- package/agents/roles/system/data-scientist/references/operating-model.md +2 -2
- package/agents/roles/system/database-administrator/learning/HISTORY.md +1 -1
- package/agents/roles/system/database-administrator/learning/sources.yaml +2 -2
- package/agents/roles/system/database-administrator/references/operating-model.md +2 -2
- package/agents/roles/system/developer-relations-engineer/learning/HISTORY.md +4 -1
- package/agents/roles/system/devops-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/engineering-manager/learning/HISTORY.md +2 -0
- package/agents/roles/system/engineering-manager/learning/sources.yaml +1 -1
- package/agents/roles/system/engineering-manager/references/operating-model.md +2 -2
- package/agents/roles/system/financial-controller/learning/HISTORY.md +2 -0
- package/agents/roles/system/financial-controller/learning/sources.yaml +2 -2
- package/agents/roles/system/financial-controller/references/operating-model.md +1 -1
- package/agents/roles/system/finops-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/fraud-risk-analyst/learning/HISTORY.md +2 -0
- package/agents/roles/system/frontend-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/growth-marketer/learning/HISTORY.md +2 -0
- package/agents/roles/system/growth-marketer/learning/sources.yaml +1 -1
- package/agents/roles/system/implementation-manager/learning/HISTORY.md +1 -1
- package/agents/roles/system/implementation-manager/learning/sources.yaml +4 -4
- package/agents/roles/system/implementation-manager/references/operating-model.md +3 -3
- package/agents/roles/system/integrations-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/kyc-aml-specialist/learning/HISTORY.md +3 -2
- package/agents/roles/system/kyc-aml-specialist/learning/sources.yaml +1 -1
- package/agents/roles/system/kyc-aml-specialist/references/operating-model.md +1 -1
- package/agents/roles/system/legal-counsel/learning/HISTORY.md +6 -1
- package/agents/roles/system/legal-counsel/learning/sources.yaml +1 -1
- package/agents/roles/system/legal-counsel/references/operating-model.md +1 -1
- package/agents/roles/system/logistics-operations-manager/learning/HISTORY.md +2 -0
- package/agents/roles/system/machine-learning-engineer/learning/HISTORY.md +4 -1
- package/agents/roles/system/machine-learning-engineer/learning/sources.yaml +6 -6
- package/agents/roles/system/machine-learning-engineer/references/operating-model.md +3 -3
- package/agents/roles/system/mlops-engineer/learning/HISTORY.md +4 -1
- package/agents/roles/system/mlops-engineer/learning/sources.yaml +2 -2
- package/agents/roles/system/mlops-engineer/references/operating-model.md +2 -2
- package/agents/roles/system/mobile-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/mobile-engineer/learning/sources.yaml +1 -1
- package/agents/roles/system/partnerships-manager/learning/HISTORY.md +4 -1
- package/agents/roles/system/partnerships-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/partnerships-manager/references/operating-model.md +2 -2
- package/agents/roles/system/people-operations-manager/learning/HISTORY.md +4 -1
- package/agents/roles/system/people-operations-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/people-operations-manager/references/operating-model.md +2 -2
- package/agents/roles/system/privacy-compliance-specialist/learning/HISTORY.md +2 -0
- package/agents/roles/system/privacy-compliance-specialist/learning/sources.yaml +1 -1
- package/agents/roles/system/privacy-compliance-specialist/references/operating-model.md +1 -1
- package/agents/roles/system/procurement-manager/learning/HISTORY.md +1 -1
- package/agents/roles/system/procurement-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/procurement-manager/references/operating-model.md +2 -2
- package/agents/roles/system/product-manager/learning/HISTORY.md +1 -1
- package/agents/roles/system/product-marketing-manager/learning/HISTORY.md +2 -0
- package/agents/roles/system/product-marketing-manager/learning/sources.yaml +1 -1
- package/agents/roles/system/project-manager/learning/HISTORY.md +4 -1
- package/agents/roles/system/project-manager/learning/sources.yaml +1 -1
- package/agents/roles/system/project-manager/references/operating-model.md +2 -2
- package/agents/roles/system/qa-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/qa-engineer/learning/sources.yaml +9 -11
- package/agents/roles/system/qa-engineer/references/operating-model.md +2 -2
- package/agents/roles/system/release-manager/learning/HISTORY.md +1 -1
- package/agents/roles/system/sales-representative/learning/HISTORY.md +2 -0
- package/agents/roles/system/sales-representative/learning/sources.yaml +2 -2
- package/agents/roles/system/sales-representative/references/operating-model.md +1 -1
- package/agents/roles/system/security-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/security-engineer/learning/sources.yaml +1 -1
- package/agents/roles/system/security-engineer/references/operating-model.md +1 -1
- package/agents/roles/system/site-reliability-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/software-architect/learning/HISTORY.md +2 -0
- package/agents/roles/system/software-architect/learning/sources.yaml +2 -2
- package/agents/roles/system/software-architect/references/operating-model.md +1 -1
- package/agents/roles/system/solutions-engineer/learning/HISTORY.md +4 -1
- package/agents/roles/system/solutions-engineer/learning/sources.yaml +4 -4
- package/agents/roles/system/solutions-engineer/references/operating-model.md +2 -2
- package/agents/roles/system/tech-lead/learning/HISTORY.md +2 -0
- package/agents/roles/system/technical-program-manager/learning/HISTORY.md +4 -1
- package/agents/roles/system/technical-program-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/technical-program-manager/references/operating-model.md +3 -3
- package/agents/roles/system/technical-writer/learning/HISTORY.md +4 -1
- package/agents/roles/system/treasury-analyst/learning/HISTORY.md +2 -0
- package/agents/roles/system/ui-designer/learning/HISTORY.md +2 -0
- package/agents/roles/system/ui-designer/learning/sources.yaml +2 -2
- package/agents/roles/system/user-researcher/learning/HISTORY.md +1 -1
- package/agents/roles/system/user-researcher/learning/sources.yaml +1 -1
- package/agents/roles/system/ux-designer/learning/HISTORY.md +2 -0
- package/agents/roles/system/ux-designer/learning/sources.yaml +1 -1
- package/agents/roles/system/ux-designer/references/operating-model.md +1 -1
- package/automatization/AGENTS.md +1 -1
- package/automatization/runners/antigravity/rules/cauce.md +1 -1
- package/automatization/runners/claude/CLAUDE.md +1 -1
- package/automatization/runners/codex/AGENTS.md +1 -1
- package/automatization/runners/gemini/GEMINI.md +1 -1
- package/automatization/shared/skills/autobuild/SKILL.md +1 -1
- package/automatization/workflows/autobuild.js +81 -27
- package/engine/agents/learning-seal.js +36 -5
- package/engine/agents/learning-sources.js +46 -2
- package/engine/agents/learning.js +13 -1
- package/engine/cli/archive.js +87 -0
- package/engine/cli/args.js +6 -2
- package/engine/cli/catalog.js +9 -1
- package/engine/cli/claims.js +125 -0
- package/engine/cli/ops.js +15 -4
- package/engine/cli/planning.js +139 -107
- package/engine/cli/worktree.js +89 -0
- package/engine/core/ownership.js +13 -5
- package/engine/core/repos.js +67 -0
- package/engine/hooks/files.js +3 -2
- package/engine/planning/adoption.js +1 -1
- package/engine/planning/claims.js +153 -0
- package/engine/planning/contracts.js +43 -202
- package/engine/planning/parser.js +51 -12
- package/engine/planning/state.js +39 -6
- package/engine/planning/structure.js +220 -0
- package/package.json +1 -1
- package/template/.gitattributes +19 -0
- package/template/AGENTS.md +47 -5
- package/template/automatization/AGENTS.md +1 -1
- package/template/gitignore +9 -0
- package/template/planning/BACKLOG.md +5 -0
- package/template/planning/FLOW.md +1 -1
- package/template/planning/PROTOCOL.md +34 -8
- package/template/planning/README.md +3 -3
- package/template/planning/RECURRING.md +1 -1
- package/template/planning/adr/system/OPS-001-planificacion-como-fuente-de-verdad.md +3 -2
- package/template/planning/business-rules/system/BR-OPS-001-una-sola-tarea-activa.md +7 -4
- package/template/planning/business-rules/system/BR-OPS-005-una-tarea-un-runner.md +44 -0
- package/template/planning/claims/README.md +70 -0
- package/template/planning/delivery/README.md +1 -0
- package/template/planning/delivery/multi-repo.md +11 -0
- package/template/planning/delivery/teamwork.md +162 -0
- package/template/planning/done/README.md +41 -0
- package/template/planning/wip/README.md +49 -0
- package/template/planning/DONE.md +0 -13
- package/template/planning/WIP.md +0 -22
|
@@ -8,7 +8,6 @@ const { PLACEHOLDERS } = require('../core/onboarding')
|
|
|
8
8
|
const TEST_TRACE = /^(?:n\/a\s*[—-]\s*.+|(?:A|C\d+)\s*(?:→|->)\s*\S.+)$/i
|
|
9
9
|
const DECISION_TRACE = /\[(?:fuente|supuesto):\s*[^\]]+\]/i
|
|
10
10
|
const COMMIT_TRACE = /^(?:n\/a\s*[—-]\s*.+|[0-9a-f]{7,40}\s+\S.*)$/i
|
|
11
|
-
const EPIC_AUXILIARY_FILES = new Set(['notes.md', 'plan.md', 'research.md', 'spec.md'])
|
|
12
11
|
|
|
13
12
|
function validTestTrace(value) {
|
|
14
13
|
return String(value || '').split(/\s*;\s*/).filter(Boolean)
|
|
@@ -75,6 +74,9 @@ function doneEntryErrors(entry, epics = []) {
|
|
|
75
74
|
const at = `${entry.source} ${entry.slug}`
|
|
76
75
|
const errors = []
|
|
77
76
|
if (!entry.acceptance) errors.push(`${at}: falta acept:`)
|
|
77
|
+
// La fecha de cierre. Mientras las entradas vivían en un archivo, el orden lo daba la posición; con un
|
|
78
|
+
// archivo por tarea no hay posición, y sin fecha no hay forma de saber cuál se cerró antes.
|
|
79
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.fecha)) errors.push(`${at}: falta fecha: AAAA-MM-DD`)
|
|
78
80
|
if (!entry.done) errors.push(`${at}: falta done:`)
|
|
79
81
|
if (!entry.qa) errors.push(`${at}: falta qa:`)
|
|
80
82
|
if (!entry.commit) errors.push(`${at}: falta commit:`)
|
|
@@ -117,201 +119,41 @@ function validateEpic(epic, done = new Set()) {
|
|
|
117
119
|
return errors
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
// Las dependencias declaradas, contra lo que existe y contra sí mismas. Dos errores distintos y los dos
|
|
123
|
+
// dejan tareas que no se le ofrecen a nadie: una que depende de algo que no existe no está lista nunca, y
|
|
124
|
+
// un ciclo se traba entero. Las dos se ven como una cola que no avanza y sin causa visible.
|
|
125
|
+
function dependencyErrors(milestones, done) {
|
|
124
126
|
const errors = []
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
`
|
|
132
|
-
+ 'o un directorio epic-NNN-<slug>/ con spec.md adentro.',
|
|
133
|
-
)
|
|
134
|
-
continue
|
|
135
|
-
}
|
|
136
|
-
if (!entry.isDirectory() || !/^epic-\d{3}-/.test(entry.name)) continue
|
|
137
|
-
const epicDir = path.join(roadmap, entry.name)
|
|
138
|
-
if (!fs.existsSync(path.join(epicDir, 'spec.md'))) {
|
|
139
|
-
errors.push(`roadmap/${entry.name}: falta spec.md`)
|
|
140
|
-
}
|
|
141
|
-
for (const child of fs.readdirSync(epicDir, { withFileTypes: true })) {
|
|
142
|
-
if (!child.isFile() || !EPIC_AUXILIARY_FILES.has(child.name)) {
|
|
143
|
-
errors.push(`roadmap/${entry.name}/${child.name}: archivo auxiliar no permitido`)
|
|
127
|
+
const tasks = milestones.flatMap((milestone) => milestone.tasks)
|
|
128
|
+
const queued = new Map(tasks.map((task) => [task.slug, task.depends || []]))
|
|
129
|
+
for (const task of tasks) {
|
|
130
|
+
for (const dep of task.depends || []) {
|
|
131
|
+
if (dep === task.slug) errors.push(`BACKLOG ${task.slug}: depende de sí misma`)
|
|
132
|
+
else if (!queued.has(dep) && !done.set.has(dep)) {
|
|
133
|
+
errors.push(`BACKLOG ${task.slug}: depende de ${dep}, que no existe en BACKLOG ni DONE`)
|
|
144
134
|
}
|
|
145
135
|
}
|
|
146
136
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
for (const line of text.split('\n')) {
|
|
159
|
-
const heading = line.match(P.MILESTONE_HEADING)
|
|
160
|
-
if (heading) { milestone = heading[1]; continue }
|
|
161
|
-
if (/^##\s+Hito\b/.test(line)) {
|
|
162
|
-
errors.push(`BACKLOG "${line.trim()}": encabezado inválido; se escribe ## Hito <slug> — <Título>, `
|
|
163
|
-
+ 'y sin él las tareas que vienen abajo quedan huérfanas')
|
|
164
|
-
milestone = ''
|
|
165
|
-
continue
|
|
137
|
+
// Recorrido en profundidad con el camino a cuestas: al reencontrar un slug que sigue en el camino,
|
|
138
|
+
// ese camino **es** el ciclo, y nombrarlo entero es lo que lo hace reparable — decir sólo que hay uno
|
|
139
|
+
// deja el trabajo de encontrarlo del lado de quien lee.
|
|
140
|
+
const estado = new Map()
|
|
141
|
+
const visitar = (slug, camino) => {
|
|
142
|
+
if (estado.get(slug) === 'listo') return
|
|
143
|
+
const desde = camino.indexOf(slug)
|
|
144
|
+
if (desde >= 0) {
|
|
145
|
+
const ciclo = [...camino.slice(desde), slug]
|
|
146
|
+
errors.push(`BACKLOG: ciclo de dependencias ${ciclo.join(' → ')}`)
|
|
147
|
+
return
|
|
166
148
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (lane) {
|
|
171
|
-
errors.push(`BACKLOG ${lane[1].trim()}: lane "${lane[2]}" no existe; usá ${P.LANES.join(' | ')}, `
|
|
172
|
-
+ 'o dejá la tarea sin clasificar')
|
|
173
|
-
continue
|
|
174
|
-
}
|
|
175
|
-
const at = `BACKLOG hito ${milestone}: no la lee nadie`
|
|
176
|
-
if (/^-\s+\[[xX]\]/.test(line)) {
|
|
177
|
-
errors.push(`${at} — ${line.trim().slice(0, 60)}. Una tarea terminada se mueve a DONE.md, no se tilda acá.`)
|
|
178
|
-
continue
|
|
179
|
-
}
|
|
180
|
-
errors.push(`${at} — ${line.trim().slice(0, 60)}. Una tarea se escribe `
|
|
181
|
-
+ '`- [ ] **slug** [lane] — descripción`, con `(→ CN) (epic: NNN)` o `_Aceptación:_` después del guión.')
|
|
182
|
-
}
|
|
183
|
-
return errors
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// El número de una regla es su identificador, y lo cita todo el sistema: cargos, workflows, plantillas
|
|
187
|
-
// y entradas de DONE. El override se declara escribiendo un archivo con el mismo nombre que el del
|
|
188
|
-
// sistema —ahí redefinir sus números es el punto—; en cualquier otro archivo, reusar un `R` crea una
|
|
189
|
-
// segunda definición que nadie declaró y que ninguna herramienta veía. Las propias se numeran `P`.
|
|
190
|
-
function ruleIds(file) {
|
|
191
|
-
return [...P.read(file).matchAll(/^##\s+([A-Z]\d+)\s+[—-]/gm)].map((match) => match[1])
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Qué IDs deja de regir un override por nombre: los que definía el archivo del sistema y el propio no
|
|
195
|
-
// redefine. Reemplazar el archivo entero es la función del override y está documentada; lo que no se
|
|
196
|
-
// veía es la consecuencia, porque la advertencia nombraba el par de archivos y no la diferencia. El
|
|
197
|
-
// caso caro es una regla que el motor sigue exigiendo —R17 lo hace—: queda exigida y sin estar escrita
|
|
198
|
-
// en ningún lado, y quien la vea fallar la va a buscar en `rules/`, donde ya no está.
|
|
199
|
-
// Dos secciones de una épica que compiten por el mismo rol. El parser prefiere la exacta, así que
|
|
200
|
-
// resuelve —y en silencio: quien escribió las dos no se entera de que una se ignora entera. La
|
|
201
|
-
// promoción dejó de generarlas cuando lo importado empezó a bajar un nivel; a mano se siguen pudiendo
|
|
202
|
-
// escribir, y ahí el aviso es lo único que lo dice.
|
|
203
|
-
function competingSections(dir) {
|
|
204
|
-
const roadmap = path.join(dir, 'roadmap')
|
|
205
|
-
const avisos = []
|
|
206
|
-
let files = []
|
|
207
|
-
try { files = fs.readdirSync(roadmap).filter((file) => /^epic-\d{3}-/.test(file)) } catch { return [] }
|
|
208
|
-
for (const file of files.sort()) {
|
|
209
|
-
const text = P.read(path.join(roadmap, file))
|
|
210
|
-
const titles = [...text.matchAll(/^##\s+(.+)$/gm)].map((hit) => hit[1].trim())
|
|
211
|
-
for (const role of [/Criterios/i, /Historias/i]) {
|
|
212
|
-
const casan = titles.filter((title) => role.test(title))
|
|
213
|
-
if (casan.length < 2) continue
|
|
214
|
-
const exact = new RegExp(`^${role.source}$`, role.flags)
|
|
215
|
-
const gana = casan.find((title) => exact.test(title)) || casan[0]
|
|
216
|
-
const ignoradas = casan.filter((title) => title !== gana)
|
|
217
|
-
avisos.push(`roadmap/${file}: "## ${gana}" convive con "## ${ignoradas.join('", "## ')}"; `
|
|
218
|
-
+ 'sólo se lee la primera y el resto se ignora entero')
|
|
149
|
+
for (const dep of queued.get(slug) || []) {
|
|
150
|
+
// La que se depende a sí misma ya tiene su error, más claro que un ciclo de un solo paso.
|
|
151
|
+
if (dep !== slug && queued.has(dep)) visitar(dep, [...camino, slug])
|
|
219
152
|
}
|
|
153
|
+
estado.set(slug, 'listo')
|
|
220
154
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
function retiredByOverride(dir, name) {
|
|
225
|
-
const rules = path.join(dir, 'rules')
|
|
226
|
-
const system = path.join(rules, 'system', name)
|
|
227
|
-
if (!fs.existsSync(system)) return []
|
|
228
|
-
const redefined = ruleIds(path.join(rules, name))
|
|
229
|
-
return ruleIds(system).filter((id) => !redefined.includes(id))
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function validateRules(dir) {
|
|
233
|
-
const rules = path.join(dir, 'rules')
|
|
234
|
-
const owner = new Map()
|
|
235
|
-
const errors = []
|
|
236
|
-
const files = (sub) => {
|
|
237
|
-
try {
|
|
238
|
-
return fs.readdirSync(path.join(rules, sub), { withFileTypes: true })
|
|
239
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'README.md')
|
|
240
|
-
.map((entry) => entry.name).sort()
|
|
241
|
-
} catch { return [] }
|
|
242
|
-
}
|
|
243
|
-
const system = new Set(files('system'))
|
|
244
|
-
for (const name of system) {
|
|
245
|
-
for (const id of ruleIds(path.join(rules, 'system', name))) {
|
|
246
|
-
if (owner.has(id)) errors.push(`rules/system/${name}: ${id} ya lo define ${owner.get(id)}`)
|
|
247
|
-
else owner.set(id, `rules/system/${name}`)
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
for (const name of files('')) {
|
|
251
|
-
// El override se declara por nombre: redefinir los números del archivo que reemplaza es su función.
|
|
252
|
-
if (system.has(name)) continue
|
|
253
|
-
for (const id of ruleIds(path.join(rules, name))) {
|
|
254
|
-
const definedBy = owner.get(id)
|
|
255
|
-
if (!definedBy) { owner.set(id, `rules/${name}`); continue }
|
|
256
|
-
errors.push(definedBy.startsWith('rules/system/')
|
|
257
|
-
? `rules/${name}: ${id} ya lo define ${definedBy}; una regla propia se numera P1..Pn, `
|
|
258
|
-
+ 'o vive en un archivo con el mismo nombre para declarar el override'
|
|
259
|
-
: `rules/${name}: ${id} ya lo define ${definedBy}`)
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
return errors
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Una decisión que no dice si rige no decide nada, y el molde traía el menú entero en la línea de estado:
|
|
266
|
-
// casi una de cada cinco decisiones escritas con este modelo se publicó con el menú intacto. Presentar
|
|
267
|
-
// las opciones no obliga a elegir; esto sí. Las secciones son las cuatro que se escriben siempre —las
|
|
268
|
-
// alternativas quedan en el molde sin exigirse, porque pedirlas rechazaría a casi todas las que existen—.
|
|
269
|
-
const ADR_STATES = ['Propuesto', 'Aceptado', 'Obsoleto']
|
|
270
|
-
const ADR_SUPERSEDED = /^Reemplazada por \[[^\]]+\]\([^)]+\)(?: \(\d{4}-\d{2}-\d{2}\))?$/
|
|
271
|
-
const ADR_SECTIONS = ['Contexto', 'Decisión', 'Consecuencias', 'Estado de implementación']
|
|
272
|
-
|
|
273
|
-
function validateAdrFile(at, text) {
|
|
274
|
-
const errors = []
|
|
275
|
-
const declared = ((text.match(/^\*\*Estado:\*\*\s*(.+?)\s*$/m) || [])[1] || '').trim()
|
|
276
|
-
if (!declared) errors.push(`${at}: falta **Estado:**`)
|
|
277
|
-
else if (declared.includes('|')) errors.push(`${at}: el estado sigue siendo el menú de la plantilla; elegí uno`)
|
|
278
|
-
else if (!ADR_STATES.includes(declared) && !ADR_SUPERSEDED.test(declared)) {
|
|
279
|
-
errors.push(`${at}: estado "${declared}" fuera de ${ADR_STATES.join(' | ')} `
|
|
280
|
-
+ '| Reemplazada por [NNN](NNN-slug.md)')
|
|
281
|
-
}
|
|
282
|
-
for (const section of ADR_SECTIONS) {
|
|
283
|
-
if (!new RegExp(`^##\\s+${section}\\s*$`, 'm').test(text)) errors.push(`${at}: falta ## ${section}`)
|
|
284
|
-
}
|
|
285
|
-
return errors
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// El nombre lleva el id porque de ahí sale la identidad con que se detecta un override, y porque una
|
|
289
|
-
// decisión se cita por número. Sin él, el archivo existe y no lo alcanza ninguna referencia.
|
|
290
|
-
function validateAdr(dir) {
|
|
291
|
-
const adr = path.join(dir, 'adr')
|
|
292
|
-
const errors = []
|
|
293
|
-
const numbers = new Map()
|
|
294
|
-
const scan = (sub, pattern) => {
|
|
295
|
-
let entries = []
|
|
296
|
-
try { entries = fs.readdirSync(path.join(adr, sub), { withFileTypes: true }) } catch { return }
|
|
297
|
-
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
298
|
-
if (!entry.isFile() || !entry.name.endsWith('.md')) continue
|
|
299
|
-
if (entry.name === 'README.md' || entry.name === '000-template.md') continue
|
|
300
|
-
const at = `adr/${sub ? `${sub}/` : ''}${entry.name}`
|
|
301
|
-
const id = entry.name.match(pattern)
|
|
302
|
-
if (!id) {
|
|
303
|
-
errors.push(`${at}: nadie lo lee como decisión. Una ADR se nombra NNN-<slug>.md, `
|
|
304
|
-
+ 'y la del sistema <ID>-NNN-<slug>.md en system/.')
|
|
305
|
-
continue
|
|
306
|
-
}
|
|
307
|
-
if (numbers.has(id[1])) errors.push(`${at}: ${id[1]} ya lo usa ${numbers.get(id[1])}`)
|
|
308
|
-
else numbers.set(id[1], at)
|
|
309
|
-
errors.push(...validateAdrFile(at, P.read(path.join(adr, sub, entry.name))))
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
scan('', /^(\d{3})-[a-z0-9-]+\.md$/)
|
|
313
|
-
scan('system', /^([A-Z][A-Z0-9]*-\d{3})-[a-z0-9-]+\.md$/)
|
|
314
|
-
return errors
|
|
155
|
+
for (const slug of queued.keys()) visitar(slug, [])
|
|
156
|
+
return [...new Set(errors)]
|
|
315
157
|
}
|
|
316
158
|
|
|
317
159
|
// Todo lo que se juzga sobre el estado ya leído: épicas, hitos, tareas, WIP, evidencia y acciones
|
|
@@ -319,7 +161,7 @@ function validateAdr(dir) {
|
|
|
319
161
|
// `validateDoneEntry`, `validateRules`— y estaba creciendo del otro lado sólo porque ahí era más
|
|
320
162
|
// rápido escribirla. No lee nada: recibe el estado, así que se prueba sin tocar disco.
|
|
321
163
|
function validateState({
|
|
322
|
-
epics, milestones, done,
|
|
164
|
+
epics, milestones, done, wips = [], roles = new Set(), humanActions = [], adopted = new Set(),
|
|
323
165
|
}) {
|
|
324
166
|
const errors = []
|
|
325
167
|
const epicNums = new Set()
|
|
@@ -399,15 +241,20 @@ function validateState({
|
|
|
399
241
|
}
|
|
400
242
|
}
|
|
401
243
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
244
|
+
errors.push(...dependencyErrors(milestones, done))
|
|
245
|
+
|
|
246
|
+
for (const wip of wips) {
|
|
247
|
+
const at = `wip/${wip.runner}.md`
|
|
248
|
+
if (!backlogSlugs.has(wip.task) && !done.set.has(wip.task)) {
|
|
249
|
+
errors.push(`${at}: ${wip.task} no existe en BACKLOG ni DONE`)
|
|
250
|
+
}
|
|
405
251
|
// El WIP es el punto de retorno tras una interrupción, y el protocolo manda seguir desde el primer
|
|
406
252
|
// paso sin tildar. Un plan que el motor no puede contar se lee como un plan terminado, así que la
|
|
407
253
|
// recuperación se queda sin de dónde retomar justo cuando es lo único que quedó del trabajo.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
254
|
+
if (!wip.complete && !wip.pending) {
|
|
255
|
+
errors.push(`${at}: el plan de ${wip.task} no tiene pasos que el motor pueda contar; `
|
|
256
|
+
+ 'se escriben `1. [ ] paso`')
|
|
257
|
+
}
|
|
411
258
|
}
|
|
412
259
|
for (const row of humanActions) {
|
|
413
260
|
if (!row.valid) {
|
|
@@ -444,15 +291,9 @@ function validateState({
|
|
|
444
291
|
module.exports = {
|
|
445
292
|
validateState,
|
|
446
293
|
doneEntryErrors,
|
|
447
|
-
validateAdr,
|
|
448
|
-
validateRules,
|
|
449
|
-
retiredByOverride,
|
|
450
|
-
competingSections,
|
|
451
|
-
validateBacklogStructure,
|
|
452
294
|
validCommitTrace,
|
|
453
295
|
validDecisionTrace,
|
|
454
296
|
validTestTrace,
|
|
455
297
|
validateDoneEntry,
|
|
456
298
|
validateEpic,
|
|
457
|
-
validateRoadmapStructure,
|
|
458
299
|
}
|
|
@@ -209,6 +209,11 @@ function taskFromLine(line) {
|
|
|
209
209
|
acceptance,
|
|
210
210
|
conditions: acceptanceConditions(acceptance),
|
|
211
211
|
criteria: criteriaRefs(rest),
|
|
212
|
+
// De qué otras tareas depende. El orden del BACKLOG alcanzaba mientras hubiera un runner: con dos,
|
|
213
|
+
// el segundo toma la que sigue mientras el primero construye la de la que depende, y el orden deja
|
|
214
|
+
// de decir nada.
|
|
215
|
+
depends: ((rest.match(/\(depende:\s*([^)]+)\)/i) || [])[1] || '')
|
|
216
|
+
.split(',').map((one) => one.trim()).filter(Boolean),
|
|
212
217
|
noSplit: noSplitReason(rest),
|
|
213
218
|
}
|
|
214
219
|
}
|
|
@@ -234,17 +239,22 @@ function readBacklog(dir) {
|
|
|
234
239
|
return milestones
|
|
235
240
|
}
|
|
236
241
|
|
|
242
|
+
// Lo que hay en `done/` y no es una entrada: una tabla de acciones humanas resueltas y la prosa que
|
|
243
|
+
// explica el formato. Se nombran acá porque el lector recorre el directorio entero — una tarea cerrada
|
|
244
|
+
// es un archivo suyo, y cualquier nombre sirve mientras el slug de adentro sea el que manda.
|
|
245
|
+
const NOT_ENTRIES = new Set(['human-actions.md', 'README.md'])
|
|
246
|
+
|
|
237
247
|
function doneFiles(dir) {
|
|
238
|
-
const files = [path.join(dir, 'DONE.md')]
|
|
239
|
-
const archive = path.join(dir, 'done')
|
|
240
248
|
try {
|
|
241
|
-
|
|
242
|
-
.
|
|
243
|
-
|
|
244
|
-
return
|
|
249
|
+
return fs.readdirSync(path.join(dir, 'done'))
|
|
250
|
+
.filter((file) => file.endsWith('.md') && !NOT_ENTRIES.has(file)).sort()
|
|
251
|
+
.map((file) => path.join(dir, 'done', file))
|
|
252
|
+
} catch { return [] }
|
|
245
253
|
}
|
|
246
254
|
|
|
247
|
-
|
|
255
|
+
// `fecha` entra al vocabulario porque un campo que no esté acá no corta al anterior: sin nombrarlo, el
|
|
256
|
+
// `done:` de la entrada se lo tragaría entero como parte de su propio texto.
|
|
257
|
+
const DONE_FIELDS = 'acept|fecha|done|qa|tests|decisions|commit'
|
|
248
258
|
|
|
249
259
|
// Un campo vale hasta el próximo campo, una línea en blanco o el fin de la entrada. Mismo corte que ya
|
|
250
260
|
// se arregló para los criterios y las historias, con el mismo síntoma: el valor es prosa y se envuelve a
|
|
@@ -275,7 +285,8 @@ function readDone(dir) {
|
|
|
275
285
|
entries.push({
|
|
276
286
|
slug: match[1].trim(),
|
|
277
287
|
epic: ((match[2].match(/\(epic:\s*(\d{3})\)/) || [])[1] || ''),
|
|
278
|
-
acceptance: field('acept'),
|
|
288
|
+
acceptance: field('acept'), fecha: field('fecha'),
|
|
289
|
+
done: field('done'), qa: field('qa'), tests: field('tests'),
|
|
279
290
|
decisions: field('decisions'), commit: field('commit'),
|
|
280
291
|
source: path.relative(dir, file), raw: match[0].trimEnd(),
|
|
281
292
|
})
|
|
@@ -341,19 +352,47 @@ function readHumanActions(dir) {
|
|
|
341
352
|
})
|
|
342
353
|
}
|
|
343
354
|
|
|
344
|
-
|
|
345
|
-
|
|
355
|
+
// El plan de un runner vive en su propio archivo, y el nombre sale de su id. Con una instancia sidecar
|
|
356
|
+
// hay un solo `planning/` por máquina, así que un archivo compartido lo escriben todos los agentes que
|
|
357
|
+
// corren ahí: el segundo pisaba el plan del primero y `context` le entregaba su tarea.
|
|
358
|
+
//
|
|
359
|
+
// El id es una ruta, así que se aplana para poder ser un nombre. Queda largo y legible a propósito —
|
|
360
|
+
// el directorio no viaja por git y sirve para mirar de quién es cada plan cuando algo quedó a medias.
|
|
361
|
+
function wipName(runner) {
|
|
362
|
+
return String(runner || '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase()
|
|
363
|
+
|| 'sin-runner'
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function parseWip(text, runner) {
|
|
346
367
|
if (/^status:\s*IDLE/m.test(text)) return null
|
|
347
368
|
const field = frontmatter(text)
|
|
348
369
|
const task = field('task')
|
|
349
370
|
if (!task) return null
|
|
350
371
|
return {
|
|
351
|
-
task, phase: field('phase') || '?', service: field('service'),
|
|
372
|
+
task, runner, phase: field('phase') || '?', service: field('service'),
|
|
352
373
|
complete: (text.match(/^\d+\.\s+\[[xX]\]/gm) || []).length,
|
|
353
374
|
pending: (text.match(/^\d+\.\s+\[\s\]/gm) || []).length,
|
|
354
375
|
}
|
|
355
376
|
}
|
|
356
377
|
|
|
378
|
+
// El de un runner: el único que le corresponde continuar. El `runner` que devuelve es el nombre
|
|
379
|
+
// aplanado, que es la identidad que hay en disco — devolver el id crudo dejaba a quien compara dos
|
|
380
|
+
// lecturas comparando formas distintas de lo mismo, y no coincidían nunca.
|
|
381
|
+
function readWip(dir, runner) {
|
|
382
|
+
const name = wipName(runner)
|
|
383
|
+
return parseWip(read(path.join(dir, 'wip', `${name}.md`)), name)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Todos los que hay. Lo pregunta `check`, que juzga si cada plan apunta a una tarea que existe, y `tree`,
|
|
387
|
+
// que muestra qué está en vuelo: las dos son preguntas sobre la instancia y no sobre quien pregunta.
|
|
388
|
+
function readWips(dir) {
|
|
389
|
+
let names = []
|
|
390
|
+
try { names = fs.readdirSync(path.join(dir, 'wip')) } catch { return [] }
|
|
391
|
+
return names.filter((name) => name.endsWith('.md')).sort()
|
|
392
|
+
.map((name) => parseWip(read(path.join(dir, 'wip', name)), name.replace(/\.md$/, '')))
|
|
393
|
+
.filter(Boolean)
|
|
394
|
+
}
|
|
395
|
+
|
|
357
396
|
// Un ítem se cuenta cuando empieza con su nombre en negrita, y el nombre existe para poder citarlo
|
|
358
397
|
// —`HUMAN_ACTIONS.md` y las propuestas mensuales se refieren a un ítem por ese slug—. La convención se
|
|
359
398
|
// conserva; lo que no se conserva es el silencio.
|
|
@@ -378,7 +417,7 @@ function readInbox(dir) {
|
|
|
378
417
|
module.exports = {
|
|
379
418
|
EPIC_STATES, HUMAN_ACTION_STATES, LANES, MILESTONE_HEADING, STOP_REASONS,
|
|
380
419
|
TASK_LINE, TASK_LINE_ANY_LANE,
|
|
381
|
-
read, section, withoutComments, frontmatter, readEpics, readBacklog, readDone, readWip,
|
|
420
|
+
read, section, withoutComments, frontmatter, readEpics, readBacklog, readDone, readWip, readWips, wipName,
|
|
382
421
|
acceptanceConditions, tableRows, taskFromLine,
|
|
383
422
|
readInbox, readHumanActions,
|
|
384
423
|
}
|
package/engine/planning/state.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// que fija el protocolo. Vivía en el CLI, donde nadie podía ejercitarlo sin lanzar un proceso.
|
|
6
6
|
|
|
7
7
|
const P = require('./parser')
|
|
8
|
+
const C = require('./claims')
|
|
8
9
|
|
|
9
10
|
function snapshot(root) {
|
|
10
11
|
const milestones = P.readBacklog(root)
|
|
@@ -12,8 +13,9 @@ function snapshot(root) {
|
|
|
12
13
|
epics: P.readEpics(root),
|
|
13
14
|
milestones,
|
|
14
15
|
done: P.readDone(root),
|
|
15
|
-
|
|
16
|
+
wips: P.readWips(root),
|
|
16
17
|
inbox: P.readInbox(root),
|
|
18
|
+
claims: C.read(root),
|
|
17
19
|
queued: new Set(milestones.flatMap((m) => m.tasks.map((t) => t.slug))),
|
|
18
20
|
}
|
|
19
21
|
}
|
|
@@ -31,21 +33,52 @@ function pendingHumanActions(root) {
|
|
|
31
33
|
// WIP activo primero —es el mutex y manda incluso si tiene una acción humana abierta—, si no la
|
|
32
34
|
// primera tarea no terminada y no bloqueada recorriendo los hitos en su orden: agotado el primero,
|
|
33
35
|
// sigue por el que viene.
|
|
34
|
-
|
|
36
|
+
//
|
|
37
|
+
// Con equipo se agrega un escalón entre los dos: lo que ya reclamé va antes que lo libre, porque es lo
|
|
38
|
+
// que dije que iba a hacer. Y lo que reclamó otro no se ofrece — sin eso, dos runners preguntando a la
|
|
39
|
+
// vez reciben la misma tarea y ninguno se entera, que es la colisión que no se ve.
|
|
40
|
+
//
|
|
41
|
+
// Quién soy acá es el runner y no la persona: dos agentes en la misma máquina comparten la identidad de
|
|
42
|
+
// git, así que comparar por owner le devolvería a cada uno la tarea del otro como propia.
|
|
43
|
+
function currentTask({ milestones, done, wips = [], claims = [] }, blockers = [], runner = '') {
|
|
44
|
+
// El plan de otro runner no es asunto de éste: leerlo como propio era lo que le entregaba a un agente
|
|
45
|
+
// la tarea que otro estaba construyendo, con el WIP ajeno adentro y diciéndole que estaba libre.
|
|
46
|
+
const wip = wips.find((one) => one.runner === P.wipName(runner)) || null
|
|
35
47
|
const queue = milestones.flatMap((milestone) => milestone.tasks.map((task) => ({ ...task, hito: milestone.slug })))
|
|
48
|
+
const mine = new Set(claims.filter((one) => runner && one.runner === runner).map((one) => one.slug))
|
|
49
|
+
const others = new Map(claims.filter((one) => !mine.has(one.slug)).map((one) => [one.slug, one.owner]))
|
|
36
50
|
if (wip) {
|
|
37
51
|
const active = queue.find((task) => task.slug === wip.task)
|
|
38
52
|
|| {
|
|
39
53
|
slug: wip.task, hito: '', tier: '', cast: { build: '', review: [] },
|
|
40
54
|
service: wip.service, acceptance: '', epic: '', criteria: [],
|
|
41
55
|
}
|
|
42
|
-
return { task: active, skipped: [] }
|
|
56
|
+
return { task: active, claimed: mine.has(wip.task), skipped: [], taken: [], waiting: [] }
|
|
43
57
|
}
|
|
44
58
|
const blocked = new Set(blockers.map((action) => action.task))
|
|
45
|
-
const pending = queue.filter((task) => !done.set.has(task.slug))
|
|
59
|
+
const pending = queue.filter((task) => !done.set.has(task.slug) && !blocked.has(task.slug))
|
|
60
|
+
// Una tarea está lista cuando todo lo que declaró depender ya está en DONE. Mientras no lo esté no se
|
|
61
|
+
// le ofrece a nadie, y en particular no a otro runner: lo que sigue a una tarea en vuelo es trabajo
|
|
62
|
+
// del que la tiene, y dárselo a otro produce dos ramas que se van a pisar al integrar.
|
|
63
|
+
const ready = (task) => (task.depends || []).every((dep) => done.set.has(dep))
|
|
64
|
+
const claimed = pending.find((task) => mine.has(task.slug))
|
|
46
65
|
return {
|
|
47
|
-
task: pending.find((task) => !
|
|
48
|
-
|
|
66
|
+
task: claimed || pending.find((task) => !others.has(task.slug) && ready(task)) || null,
|
|
67
|
+
// Que la tarea devuelta ya sea mía o esté libre cambia lo que corresponde hacer con ella, y desde
|
|
68
|
+
// afuera las dos se ven igual.
|
|
69
|
+
claimed: Boolean(claimed),
|
|
70
|
+
skipped: queue.filter((task) => !done.set.has(task.slug) && blocked.has(task.slug))
|
|
71
|
+
.map((task) => task.slug),
|
|
72
|
+
taken: pending.filter((task) => others.has(task.slug))
|
|
73
|
+
.map((task) => ({ slug: task.slug, owner: others.get(task.slug) })),
|
|
74
|
+
// Lo que espera a otra tarea, con cuál y quién la tiene: la tercera causa por la que una cola puede
|
|
75
|
+
// no ofrecer nada, y `context` las distingue por la misma razón que distingue las otras dos.
|
|
76
|
+
// El filtro garantiza que hay dependencias y que al menos una no está cerrada —sin eso la tarea
|
|
77
|
+
// estaría lista y no acá—, así que buscarla no puede fallar y no lleva defensa.
|
|
78
|
+
waiting: pending.filter((task) => !others.has(task.slug) && !ready(task)).map((task) => {
|
|
79
|
+
const dep = task.depends.find((one) => !done.set.has(one))
|
|
80
|
+
return { slug: task.slug, dep, owner: others.get(dep) || (mine.has(dep) ? 'vos' : '') }
|
|
81
|
+
}),
|
|
49
82
|
}
|
|
50
83
|
}
|
|
51
84
|
|