@ingeniomaps/cauce 0.3.0 → 0.4.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +16 -1
  3. package/agents/roles/system/ai-governance-lead/SKILL.md +2 -0
  4. package/agents/roles/system/ai-governance-lead/learning/sources.yaml +2 -4
  5. package/agents/roles/system/ai-product-manager/SKILL.md +2 -0
  6. package/agents/roles/system/ai-product-manager/learning/sources.yaml +2 -2
  7. package/agents/roles/system/analytics-engineer/SKILL.md +2 -0
  8. package/agents/roles/system/analytics-engineer/learning/sources.yaml +2 -1
  9. package/agents/roles/system/backend-engineer/SKILL.md +2 -0
  10. package/agents/roles/system/backend-engineer/learning/sources.yaml +2 -4
  11. package/agents/roles/system/business-operations-manager/SKILL.md +2 -0
  12. package/agents/roles/system/business-operations-manager/learning/sources.yaml +2 -4
  13. package/agents/roles/system/business-strategist/SKILL.md +2 -0
  14. package/agents/roles/system/business-strategist/learning/sources.yaml +2 -8
  15. package/agents/roles/system/cloud-architect/SKILL.md +2 -0
  16. package/agents/roles/system/cloud-architect/learning/sources.yaml +2 -2
  17. package/agents/roles/system/community-manager/SKILL.md +2 -0
  18. package/agents/roles/system/community-manager/learning/sources.yaml +2 -4
  19. package/agents/roles/system/content-specialist/SKILL.md +2 -0
  20. package/agents/roles/system/content-specialist/learning/sources.yaml +2 -8
  21. package/agents/roles/system/customer-success-manager/SKILL.md +2 -0
  22. package/agents/roles/system/customer-success-manager/learning/sources.yaml +2 -8
  23. package/agents/roles/system/customer-support-specialist/SKILL.md +2 -0
  24. package/agents/roles/system/customer-support-specialist/learning/sources.yaml +2 -8
  25. package/agents/roles/system/data-analyst/SKILL.md +2 -0
  26. package/agents/roles/system/data-analyst/learning/sources.yaml +2 -8
  27. package/agents/roles/system/data-engineer/SKILL.md +2 -0
  28. package/agents/roles/system/data-engineer/learning/sources.yaml +2 -4
  29. package/agents/roles/system/data-scientist/SKILL.md +2 -0
  30. package/agents/roles/system/data-scientist/learning/sources.yaml +2 -4
  31. package/agents/roles/system/database-administrator/SKILL.md +2 -0
  32. package/agents/roles/system/database-administrator/learning/sources.yaml +2 -2
  33. package/agents/roles/system/developer-relations-engineer/SKILL.md +2 -0
  34. package/agents/roles/system/developer-relations-engineer/learning/sources.yaml +2 -4
  35. package/agents/roles/system/devops-engineer/SKILL.md +2 -0
  36. package/agents/roles/system/devops-engineer/learning/sources.yaml +2 -4
  37. package/agents/roles/system/engineering-manager/SKILL.md +2 -0
  38. package/agents/roles/system/engineering-manager/learning/sources.yaml +2 -4
  39. package/agents/roles/system/financial-controller/SKILL.md +2 -0
  40. package/agents/roles/system/financial-controller/learning/sources.yaml +2 -8
  41. package/agents/roles/system/finops-engineer/SKILL.md +2 -0
  42. package/agents/roles/system/finops-engineer/learning/sources.yaml +2 -8
  43. package/agents/roles/system/frontend-engineer/SKILL.md +2 -0
  44. package/agents/roles/system/frontend-engineer/learning/sources.yaml +2 -4
  45. package/agents/roles/system/growth-marketer/SKILL.md +2 -0
  46. package/agents/roles/system/growth-marketer/learning/sources.yaml +2 -8
  47. package/agents/roles/system/implementation-manager/SKILL.md +2 -0
  48. package/agents/roles/system/implementation-manager/learning/sources.yaml +2 -4
  49. package/agents/roles/system/legal-counsel/SKILL.md +2 -0
  50. package/agents/roles/system/legal-counsel/learning/sources.yaml +2 -8
  51. package/agents/roles/system/machine-learning-engineer/SKILL.md +2 -0
  52. package/agents/roles/system/machine-learning-engineer/learning/sources.yaml +2 -4
  53. package/agents/roles/system/mlops-engineer/SKILL.md +2 -0
  54. package/agents/roles/system/mlops-engineer/learning/sources.yaml +2 -2
  55. package/agents/roles/system/mobile-engineer/SKILL.md +2 -0
  56. package/agents/roles/system/mobile-engineer/learning/sources.yaml +2 -4
  57. package/agents/roles/system/partnerships-manager/SKILL.md +2 -0
  58. package/agents/roles/system/partnerships-manager/learning/sources.yaml +2 -4
  59. package/agents/roles/system/people-operations-manager/SKILL.md +2 -0
  60. package/agents/roles/system/people-operations-manager/learning/sources.yaml +2 -4
  61. package/agents/roles/system/privacy-compliance-specialist/SKILL.md +2 -0
  62. package/agents/roles/system/privacy-compliance-specialist/learning/sources.yaml +2 -8
  63. package/agents/roles/system/procurement-manager/SKILL.md +2 -0
  64. package/agents/roles/system/procurement-manager/learning/sources.yaml +2 -4
  65. package/agents/roles/system/product-manager/SKILL.md +2 -0
  66. package/agents/roles/system/product-manager/learning/sources.yaml +2 -0
  67. package/agents/roles/system/product-marketing-manager/SKILL.md +2 -0
  68. package/agents/roles/system/product-marketing-manager/learning/sources.yaml +2 -8
  69. package/agents/roles/system/project-manager/SKILL.md +2 -0
  70. package/agents/roles/system/project-manager/learning/sources.yaml +2 -4
  71. package/agents/roles/system/qa-engineer/SKILL.md +2 -0
  72. package/agents/roles/system/qa-engineer/learning/sources.yaml +2 -4
  73. package/agents/roles/system/release-manager/SKILL.md +2 -0
  74. package/agents/roles/system/release-manager/learning/sources.yaml +2 -4
  75. package/agents/roles/system/revenue-operations-manager/SKILL.md +2 -0
  76. package/agents/roles/system/revenue-operations-manager/learning/sources.yaml +2 -4
  77. package/agents/roles/system/sales-representative/SKILL.md +2 -0
  78. package/agents/roles/system/sales-representative/learning/sources.yaml +2 -8
  79. package/agents/roles/system/security-engineer/SKILL.md +2 -0
  80. package/agents/roles/system/security-engineer/learning/sources.yaml +2 -4
  81. package/agents/roles/system/site-reliability-engineer/SKILL.md +2 -0
  82. package/agents/roles/system/site-reliability-engineer/learning/sources.yaml +2 -4
  83. package/agents/roles/system/software-architect/SKILL.md +2 -0
  84. package/agents/roles/system/software-architect/learning/sources.yaml +2 -4
  85. package/agents/roles/system/solutions-engineer/SKILL.md +2 -0
  86. package/agents/roles/system/solutions-engineer/learning/sources.yaml +2 -4
  87. package/agents/roles/system/technical-program-manager/SKILL.md +2 -0
  88. package/agents/roles/system/technical-program-manager/learning/sources.yaml +2 -1
  89. package/agents/roles/system/technical-writer/SKILL.md +2 -0
  90. package/agents/roles/system/technical-writer/learning/sources.yaml +2 -4
  91. package/agents/roles/system/ui-designer/SKILL.md +2 -0
  92. package/agents/roles/system/ui-designer/learning/sources.yaml +2 -0
  93. package/agents/roles/system/user-researcher/SKILL.md +2 -0
  94. package/agents/roles/system/user-researcher/learning/sources.yaml +2 -0
  95. package/agents/roles/system/ux-designer/SKILL.md +2 -0
  96. package/agents/roles/system/ux-designer/learning/sources.yaml +2 -0
  97. package/engine/agents/catalog.js +45 -20
  98. package/engine/agents/learning.js +19 -2
  99. package/engine/cli/ops.js +67 -21
  100. package/engine/core/manifest.js +54 -0
  101. package/engine/core/ownership.js +68 -21
  102. package/engine/teams/registry.js +12 -3
  103. package/package.json +1 -1
  104. package/template/organization/roles/README.md +37 -0
@@ -11,6 +11,8 @@ Actuar como responsable del lenguaje visual de la interfaz y su aplicación sist
11
11
 
12
12
  1. Localizar la raíz operativa del proyecto.
13
13
  2. Leer `AGENTS.md`, `ops.config.json` y `organization/README.md` si existen.
14
+ Leer también `organization/roles/ui-designer.md` si existe: son las restricciones reales de
15
+ esta empresa para este cargo.
14
16
  3. Leer `organization/company.md`, `organization/product.md` y cualquier guía de marca o design system autorizado.
15
17
  4. Leer el flujo y criterios entregados por UX Designer, además de evidencia relevante y restricciones confirmadas.
16
18
  5. Inspeccionar componentes, tokens y convenciones existentes antes de crear otros. Separar hechos, restricciones y supuestos. No inventar marca, usuarios, métricas ni evidencia observable.
@@ -5,6 +5,8 @@ rules:
5
5
  reject_unsourced_claims: true
6
6
  automatic_apply: false
7
7
 
8
+ # El contexto de la empresa no es una fuente de la profesión: vive en
9
+ # organization/roles/ui-designer.md dentro de cada instalación.
8
10
  sources:
9
11
  - name: W3C Web Accessibility Initiative
10
12
  url: https://www.w3.org/WAI/
@@ -11,6 +11,8 @@ Actuar como responsable de producir evidencia confiable sobre usuarios. Explicar
11
11
 
12
12
  1. Localizar la raíz operativa del proyecto.
13
13
  2. Leer `AGENTS.md`, `ops.config.json` y `organization/README.md` si existen.
14
+ Leer también `organization/roles/user-researcher.md` si existe: son las restricciones reales de
15
+ esta empresa para este cargo.
14
16
  3. Leer `organization/company.md` y `organization/product.md`. Consultar políticas de privacidad, seguridad, accesibilidad y salvaguarda cuando apliquen.
15
17
  4. Leer sólo el estado de `planning/` necesario para comprender la decisión que la investigación debe informar.
16
18
  5. Clasificar cada entrada como dato original, observación, interpretación, inferencia, supuesto o pregunta.
@@ -5,6 +5,8 @@ rules:
5
5
  reject_unsourced_claims: true
6
6
  automatic_apply: false
7
7
 
8
+ # El contexto de la empresa no es una fuente de la profesión: vive en
9
+ # organization/roles/user-researcher.md dentro de cada instalación.
8
10
  sources:
9
11
  - name: GOV.UK User Research
10
12
  url: https://www.gov.uk/service-manual/user-research
@@ -11,6 +11,8 @@ Actuar como responsable de la estructura y comportamiento de la experiencia. Red
11
11
 
12
12
  1. Localizar la raíz operativa del proyecto.
13
13
  2. Leer `AGENTS.md`, `ops.config.json` y `organization/README.md` si existen.
14
+ Leer también `organization/roles/ux-designer.md` si existe: son las restricciones reales de
15
+ esta empresa para este cargo.
14
16
  3. Leer `organization/company.md`, `organization/product.md` y las reglas de diseño o accesibilidad disponibles.
15
17
  4. Consultar evidencia del User Researcher y decisiones aprobadas del Product Manager. Leer el estado de `planning/` necesario.
16
18
  5. Separar hechos, evidencia, restricciones, supuestos y decisiones pendientes. No inventar investigación, usuarios, métricas ni evidencia observable.
@@ -5,6 +5,8 @@ rules:
5
5
  reject_unsourced_claims: true
6
6
  automatic_apply: false
7
7
 
8
+ # El contexto de la empresa no es una fuente de la profesión: vive en
9
+ # organization/roles/ux-designer.md dentro de cada instalación.
8
10
  sources:
9
11
  - name: W3C Web Accessibility Initiative
10
12
  url: https://www.w3.org/WAI/
@@ -1,15 +1,18 @@
1
1
  'use strict'
2
2
 
3
3
  // Resolución única del catálogo de cargos. La usan el ciclo de aprendizaje, la validación de
4
- // equipos y la instalación en runners: tres lugares que antes resolvían por su cuenta.
4
+ // equipos y la instalación en runners.
5
5
  //
6
- // Cada tipo separa lo del toolkit de lo del proyecto igual que el resto de las colecciones:
6
+ // El catálogo se reparte en dos lugares según de quién es cada cosa:
7
7
  //
8
- // agents/<tipo>/system/<slug>/SKILL.md viene con Cauce y se reemplaza al actualizar
9
- // agents/<tipo>/<slug>/SKILL.md es del proyecto y manda sobre el anterior
8
+ // <paquete>/agents/<tipo>/system/<slug>/ viene con Cauce, se actualiza con la dependencia
9
+ // <proyecto>/agents/<tipo>/<slug>/ es de la empresa y manda sobre el anterior
10
10
  //
11
- // Un slug repetido entre tipos distintos sigue siendo ambiguo: ahí no hay ninguna regla que
12
- // diga cuál gana, y elegir en silencio sería peor que fallar.
11
+ // Los cargos del sistema no se copian al proyecto: evolucionan como profesión y esa evolución es
12
+ // la misma para todos. Lo que es de cada empresa —su contexto— vive en `organization/roles/`.
13
+ //
14
+ // Un slug repetido entre tipos distintos sigue siendo ambiguo: ahí no hay ninguna regla que diga
15
+ // cuál gana, y elegir en silencio sería peor que fallar.
13
16
 
14
17
  const fs = require('node:fs')
15
18
  const path = require('node:path')
@@ -24,44 +27,66 @@ function directories(dir) {
24
27
  } catch { return [] }
25
28
  }
26
29
 
30
+ // Dónde está el catálogo que trae Cauce. Se reconoce por tener `roles/system/`, que es el espacio
31
+ // del sistema y no algo que un proyecto deba crear.
32
+ function systemCatalog(root) {
33
+ return require('../core/ownership').packageDir(root, 'agents')
34
+ }
35
+
36
+ function projectCatalog(root) {
37
+ return path.join(root, 'agents')
38
+ }
39
+
27
40
  function types(root) {
28
- return directories(path.join(root, 'agents'))
41
+ const own = directories(projectCatalog(root))
42
+ const system = directories(systemCatalog(root) || path.join(root, '\0'))
43
+ return [...new Set([...own, ...system])]
29
44
  }
30
45
 
31
- // Todos los cargos visibles: los del proyecto ocultan a los del sistema con el mismo slug.
46
+ // Todos los cargos visibles: los de la empresa ocultan a los del sistema con el mismo slug.
32
47
  function list(root) {
33
- const catalog = path.join(root, 'agents')
48
+ const system = systemCatalog(root)
34
49
  const found = new Map()
35
50
  for (const type of types(root)) {
36
- const base = path.join(catalog, type)
37
- // El sistema primero para que el proyecto lo sobrescriba al pasar por encima.
38
- for (const [source, system] of [[path.join(base, 'system'), true], [base, false]]) {
51
+ // El sistema primero, para que el proyecto lo sobrescriba al pasar por encima.
52
+ const sources = [
53
+ system ? [path.join(system, type, 'system'), true] : null,
54
+ [path.join(projectCatalog(root), type), false],
55
+ ].filter(Boolean)
56
+ for (const [source, fromSystem] of sources) {
39
57
  for (const slug of directories(source)) {
40
58
  if (slug === 'system' || !SLUG.test(slug)) continue
41
59
  const dir = path.join(source, slug)
42
60
  if (!fs.existsSync(path.join(dir, 'SKILL.md'))) continue
43
- found.set(slug, { slug, type, dir, system })
61
+ found.set(slug, { slug, type, dir, system: fromSystem })
44
62
  }
45
63
  }
46
64
  }
47
65
  return [...found.values()].sort((left, right) => left.slug.localeCompare(right.slug))
48
66
  }
49
67
 
50
- function resolve(root, slug) {
68
+ function find(root, slug) {
51
69
  if (!SLUG.test(slug || '')) throw new Error(`agente inválido: ${slug || '(vacío)'}`)
70
+ const system = systemCatalog(root)
52
71
  const matches = []
53
72
  for (const type of types(root)) {
54
- const own = path.join(root, 'agents', type, slug)
55
- const system = path.join(root, 'agents', type, 'system', slug)
73
+ const own = path.join(projectCatalog(root), type, slug)
74
+ const shipped = system ? path.join(system, type, 'system', slug) : ''
56
75
  // Dentro de un tipo la precedencia está definida; entre tipos no, y por eso se acumulan.
57
- if (fs.existsSync(path.join(own, 'SKILL.md'))) matches.push(own)
58
- else if (fs.existsSync(path.join(system, 'SKILL.md'))) matches.push(system)
76
+ if (fs.existsSync(path.join(own, 'SKILL.md'))) matches.push({ dir: own, system: false })
77
+ else if (shipped && fs.existsSync(path.join(shipped, 'SKILL.md'))) {
78
+ matches.push({ dir: shipped, system: true })
79
+ }
59
80
  }
60
81
  if (!matches.length) throw new Error(`no existe agents/<tipo>/${slug}/SKILL.md`)
61
82
  if (matches.length > 1) {
62
- throw new Error(`agente ambiguo ${slug}: ${matches.map((dir) => path.relative(root, dir)).join(', ')}`)
83
+ throw new Error(`agente ambiguo ${slug}: ${matches.map((entry) => entry.dir).join(', ')}`)
63
84
  }
64
85
  return matches[0]
65
86
  }
66
87
 
67
- module.exports = { list, resolve, types }
88
+ function resolve(root, slug) {
89
+ return find(root, slug).dir
90
+ }
91
+
92
+ module.exports = { find, list, projectCatalog, resolve, systemCatalog, types }
@@ -19,11 +19,28 @@ function agentRoot(root, agent) {
19
19
  return catalog.resolve(root, agent)
20
20
  }
21
21
 
22
+ // Un cargo del sistema vive dentro del paquete: escribir ahí perdería el informe en el próximo
23
+ // `npm ci`, y además duplicaría en cada empresa una investigación sobre la profesión que se hace
24
+ // mejor una sola vez. Lo que sí es de esta empresa es su contexto, y ese tiene otro lugar.
25
+ function assertWritable(root, agent) {
26
+ const found = catalog.find(root, agent)
27
+ // Lo que decide no es si el cargo es del sistema, sino si vive dentro de este repositorio. En el
28
+ // toolkit los cargos del sistema son propios y se aprenden acá; en una instancia vienen del
29
+ // paquete, y escribir ahí se pierde en el próximo `npm ci` o en el próximo upgrade.
30
+ const own = path.resolve(catalog.projectCatalog(root))
31
+ if (path.resolve(found.dir).startsWith(`${own}${path.sep}`)) return found.dir
32
+ throw new Error(
33
+ `${agent} es un cargo que trae Cauce y su aprendizaje se hace en el toolkit, no acá.\n` +
34
+ ` Lo que este cargo debe saber de esta empresa va en organization/roles/${agent}.md.\n` +
35
+ ` Para tener una versión propia del cargo, escribila en agents/roles/${agent}/.`,
36
+ )
37
+ }
38
+
22
39
  function isoDate(now = new Date()) { return now.toISOString().slice(0, 10) }
23
40
  function month(now = new Date()) { return now.toISOString().slice(0, 7) }
24
41
 
25
42
  function prepareReport(root, agent, now = new Date()) {
26
- const reports = path.join(agentRoot(root, agent), 'learning', 'reports')
43
+ const reports = path.join(assertWritable(root, agent), 'learning', 'reports')
27
44
  const file = path.join(reports, `${isoDate(now)}.md`)
28
45
  fs.mkdirSync(reports, { recursive: true })
29
46
  if (fs.existsSync(file)) return { file, created: false }
@@ -51,7 +68,7 @@ status: draft
51
68
  }
52
69
 
53
70
  function prepareProposal(root, agent, now = new Date()) {
54
- const target = agentRoot(root, agent)
71
+ const target = assertWritable(root, agent)
55
72
  const proposalDir = path.join(target, 'learning', 'proposals')
56
73
  const file = path.join(proposalDir, `${month(now)}.md`)
57
74
  fs.mkdirSync(proposalDir, { recursive: true })
package/engine/cli/ops.js CHANGED
@@ -12,6 +12,7 @@ const A = require('../automation')
12
12
  const F = require('../core/files')
13
13
  const O = require('../core/ownership')
14
14
  const CL = require('../core/changelog')
15
+ const M = require('../core/manifest')
15
16
  const C = require('../config/validate')
16
17
  const T = require('../teams/registry')
17
18
  const AG = require('../agents/catalog')
@@ -125,25 +126,13 @@ function init(target) {
125
126
  '{{PLANNING_DIR}}': 'planning',
126
127
  '{{WORKSPACE_PATH}}': mode === 'embedded' ? '.' : '..',
127
128
  }, process.argv.includes('--force'))
128
- copyTemplate(path.join(PROJECT_ROOT, 'agents'), path.join(root, 'agents'), {
129
- '{{PROJECT_NAME}}': name,
130
- '{{MODE}}': mode,
131
- '{{PLANNING_DIR}}': 'planning',
132
- '{{WORKSPACE_PATH}}': mode === 'embedded' ? '.' : '..',
133
- }, process.argv.includes('--force'))
134
- copyTemplate(path.join(PROJECT_ROOT, 'teams'), path.join(root, 'teams'), {
135
- '{{PROJECT_NAME}}': name,
136
- '{{MODE}}': mode,
137
- '{{PLANNING_DIR}}': 'planning',
138
- '{{WORKSPACE_PATH}}': mode === 'embedded' ? '.' : '..',
139
- }, process.argv.includes('--force'))
140
129
  // `ci.yml` valida el toolkit con `npm run ci`; una instancia no tiene ese script ni sus pruebas.
141
130
  copyTemplate(path.join(PROJECT_ROOT, '.github', 'workflows'), path.join(root, '.github', 'workflows'), {
142
131
  '{{PROJECT_NAME}}': name,
143
132
  '{{MODE}}': mode,
144
133
  '{{PLANNING_DIR}}': 'planning',
145
134
  '{{WORKSPACE_PATH}}': mode === 'embedded' ? '.' : '..',
146
- }, process.argv.includes('--force'), ['ci.yml'])
135
+ }, process.argv.includes('--force'), ['ci.yml', 'agent-learning.yml'])
147
136
  const preserve = process.argv.includes('--force')
148
137
  copyRuntime(
149
138
  path.join(PROJECT_ROOT, 'automatization', 'hooks'),
@@ -176,11 +165,24 @@ function init(target) {
176
165
  const engine = path.join(root, '.ops', 'engine')
177
166
  copyRuntime(path.join(PROJECT_ROOT, 'engine'), engine, preserve, root)
178
167
  fs.chmodSync(path.join(engine, 'cli', 'ops.js'), 0o755)
168
+ // Sin npm no hay de dónde leer el catálogo en tiempo de ejecución: viaja junto al motor.
169
+ copyRuntime(path.join(PROJECT_ROOT, 'agents'), path.join(root, '.ops', 'agents'), preserve, root)
170
+ copyRuntime(path.join(PROJECT_ROOT, 'teams'), path.join(root, '.ops', 'teams'), preserve, root)
171
+ }
172
+ let entregado = {}
173
+ for (const relative of O.trackedPaths()) {
174
+ const dir = path.join(root, relative)
175
+ if (fs.existsSync(dir)) entregado = { ...entregado, ...M.record(root, relative, O.treeFiles(dir)) }
179
176
  }
177
+ M.write(root, entregado)
180
178
  // La instancia recuerda de qué versión salió: sin esto no hay actualización posible.
181
179
  const configFile = path.join(root, 'ops.config.json')
182
180
  const config = JSON.parse(fs.readFileSync(configFile, 'utf8'))
183
181
  config.cauceVersion = version
182
+ // El esquema vive donde quedó el motor; la plantilla no puede saberlo de antemano.
183
+ config.$schema = engineMode === 'dependency'
184
+ ? 'node_modules/@ingeniomaps/cauce/engine/schemas/ops-config.schema.json'
185
+ : '.ops/engine/schemas/ops-config.schema.json'
184
186
  F.atomicWriteJson(configFile, config)
185
187
  console.log(`\n✓ ${name}: sistema ops creado en ${root}`)
186
188
  if (engineMode === 'dependency') console.log(' siguiente: npm install (el motor viene de la dependencia)')
@@ -492,7 +494,7 @@ function upgrade(dir) {
492
494
  const from = instanceVersion(root)
493
495
  const to = require(path.join(PROJECT_ROOT, 'package.json')).version
494
496
  const system = O.systemPaths(root)
495
- const changed = O.localChanges(root, PROJECT_ROOT)
497
+ const changed = O.localChanges(root)
496
498
  const overrides = O.overrides(root)
497
499
 
498
500
  if (dry) {
@@ -503,14 +505,37 @@ function upgrade(dir) {
503
505
  process.exit(1)
504
506
  }
505
507
 
508
+ // Antes de retirar nada, comprobar que no se lleve puesto aprendizaje acumulado.
509
+ const rescatar = O.retiredWithLearning(root)
510
+ if (rescatar.length && !force) {
511
+ for (const file of rescatar) console.error(`✗ ${file}`)
512
+ fail(
513
+ `\n${rescatar.length} archivo(s) de aprendizaje quedaron en una ruta que Cauce ya no mantiene.\n\n` +
514
+ 'Movelos a un cargo propio en agents/roles/<slug>/learning/ y repetí, o descartalos con --force.',
515
+ )
516
+ }
517
+
506
518
  if (changed.length && !force) {
507
519
  for (const file of changed) console.error(`✗ ${file}`)
520
+ const reglas = changed.filter((file) => file.includes('/system/'))
521
+ const runtime = changed.filter((file) => !file.includes('/system/'))
522
+ const guia = []
523
+ if (reglas.length) {
524
+ guia.push(
525
+ 'Las reglas y decisiones bajo system/ son del toolkit. Para cambiar una, escribí la tuya al\n'
526
+ + 'lado con el mismo ID: el proyecto manda y `check` lo reporta como override explícito.',
527
+ )
528
+ }
529
+ if (runtime.length) {
530
+ guia.push(
531
+ 'El runtime es del toolkit: en vez de editarlo, agregá lo tuyo al lado con otro nombre —un\n'
532
+ + 'guard propio sobrevive a cada actualización— y registralo en la configuración de tu runner,\n'
533
+ + 'que sí es del proyecto. Para desactivar un guard alcanza con quitarlo de esa configuración.',
534
+ )
535
+ }
508
536
  fail(
509
- `\n${changed.length} archivo(s) del runtime fueron editados y se perderían.\n\n` +
510
- 'El runtime es del toolkit: en vez de editarlo, agregá lo tuyo al lado con otro nombre —un\n' +
511
- 'guard propio sobrevive a cada actualización— y registralo en la configuración de tu runner,\n' +
512
- 'que sí es del proyecto. Para desactivar un guard alcanza con quitarlo de esa configuración.\n' +
513
- 'Si el cambio ya no te sirve, repetí con --force para descartarlo.',
537
+ `\n${changed.length} archivo(s) que mantiene Cauce fueron editados y se perderían.\n\n` +
538
+ `${guia.join('\n\n')}\n\nSi el cambio ya no te sirve, repetí con --force para descartarlo.`,
514
539
  )
515
540
  }
516
541
 
@@ -518,8 +543,9 @@ function upgrade(dir) {
518
543
  const origin = path.join(PROJECT_ROOT, O.sourceOf(relative))
519
544
  if (!fs.existsSync(origin)) continue
520
545
  const target = path.join(root, relative)
521
- // Una instancia que toma el motor de npm no debe recuperar la copia: la actualiza el lockfile.
522
- if (relative === '.ops/engine' && !fs.existsSync(target)) continue
546
+ // `.ops/` es el paquete vendorizado de una instancia sin npm. Si no existe, esta instancia lo
547
+ // toma de la dependencia y crearlo sería duplicar lo que el lockfile ya versiona.
548
+ if (relative.startsWith('.ops/') && !fs.existsSync(target)) continue
523
549
  const skip = O.TEMPLATE_OWNED
524
550
  .filter((owned) => owned.startsWith(`${relative}/`))
525
551
  .map((owned) => path.basename(owned))
@@ -530,6 +556,25 @@ function upgrade(dir) {
530
556
  }
531
557
  }
532
558
 
559
+ // Retirar lo que el toolkit ya no distribuye, después de haber actualizado lo que sí.
560
+ const retirado = []
561
+ for (const relative of O.RETIRED) {
562
+ const target = path.join(root, relative)
563
+ if (!fs.existsSync(target)) continue
564
+ F.assertNoSymlinkPath(root, target)
565
+ fs.rmSync(target, { recursive: true, force: true })
566
+ retirado.push(relative)
567
+ }
568
+
569
+ // Dejar registrado lo que se entregó, para poder distinguir después una edición local de una
570
+ // mejora del toolkit.
571
+ let registro = M.read(root)
572
+ for (const relative of O.trackedPaths()) {
573
+ const dir = path.join(root, relative)
574
+ if (fs.existsSync(dir)) registro = { ...registro, ...M.record(root, relative, O.treeFiles(dir)) }
575
+ }
576
+ M.write(root, registro)
577
+
533
578
  const config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
534
579
  config.cauceVersion = to
535
580
  F.atomicWriteJson(path.join(root, 'ops.config.json'), config)
@@ -538,6 +583,7 @@ function upgrade(dir) {
538
583
  // Descartar con --force es legítimo; hacerlo sin dejar rastro no. Queda en la salida del comando,
539
584
  // que es la evidencia que el protocolo pide para cualquier cambio.
540
585
  for (const file of changed) console.log(`− descartado tu cambio en ${file}`)
586
+ for (const relative of retirado) console.log(`− retirado ${relative}: Cauce ya no lo distribuye`)
541
587
  printChangelog(from, to)
542
588
  console.log(` ${system.length} ruta(s) del sistema y ${O.RUNTIME_PATHS.length} del runtime actualizadas`)
543
589
  for (const override of overrides) {
@@ -0,0 +1,54 @@
1
+ 'use strict'
2
+
3
+ // Qué entregó Cauce y con qué contenido. Sin este registro no se puede distinguir un archivo que
4
+ // la empresa editó de uno que cambió río arriba: los dos se ven igual comparando la instancia
5
+ // contra el paquete, y `upgrade` terminaría negándose ante cualquier mejora del toolkit.
6
+ //
7
+ // Se guarda junto a la configuración porque es estado de la instalación, no del producto, y se
8
+ // commitea: todo el equipo tiene que ver lo mismo.
9
+
10
+ const crypto = require('node:crypto')
11
+ const fs = require('node:fs')
12
+ const path = require('node:path')
13
+
14
+ const FILE = path.join('.cauce', 'manifest.json')
15
+
16
+ function digest(file) {
17
+ try {
18
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex').slice(0, 16)
19
+ } catch { return '' }
20
+ }
21
+
22
+ function read(root) {
23
+ try {
24
+ const data = JSON.parse(fs.readFileSync(path.join(root, FILE), 'utf8'))
25
+ return data && typeof data.files === 'object' ? data.files : {}
26
+ } catch { return {} }
27
+ }
28
+
29
+ function write(root, files) {
30
+ const target = path.join(root, FILE)
31
+ fs.mkdirSync(path.dirname(target), { recursive: true })
32
+ const ordered = Object.fromEntries(Object.entries(files).sort(([left], [right]) => left.localeCompare(right)))
33
+ fs.writeFileSync(target, `${JSON.stringify({ version: 1, files: ordered }, null, 2)}\n`)
34
+ }
35
+
36
+ // Registra lo entregado en una ruta, relativo a la raíz de la instancia.
37
+ function record(root, relative, files) {
38
+ const current = read(root)
39
+ for (const file of files) current[`${relative}/${file}`] = digest(path.join(root, relative, file))
40
+ return current
41
+ }
42
+
43
+ // Archivos que la empresa modificó después de recibirlos. Un archivo sin registro previo no
44
+ // cuenta: llegó con una versión anterior a este mecanismo, o lo agregó el proyecto.
45
+ function edited(root, relative, files) {
46
+ const recorded = read(root)
47
+ return files.filter((file) => {
48
+ const key = `${relative}/${file}`
49
+ if (!recorded[key]) return false
50
+ return recorded[key] !== digest(path.join(root, relative, file))
51
+ })
52
+ }
53
+
54
+ module.exports = { FILE, digest, edited, read, record, write }
@@ -22,6 +22,7 @@ const SYSTEM_FILES = [
22
22
  'planning/rules/README.md',
23
23
  'planning/roadmap/README.md',
24
24
  'planning/roadmap/epic-000-template.md',
25
+ 'organization/roles/README.md',
25
26
  ]
26
27
 
27
28
  // Colecciones mixtas: el toolkit posee `<dir>/system/`, el proyecto todo lo demás del directorio.
@@ -29,7 +30,6 @@ const SYSTEM_COLLECTIONS = [
29
30
  'planning/adr',
30
31
  'planning/business-rules',
31
32
  'planning/rules',
32
- 'teams',
33
33
  ]
34
34
 
35
35
  // Copias del runtime: el proyecto las recibe para poder ejecutar sin red, pero no las escribe.
@@ -37,6 +37,8 @@ const SYSTEM_COLLECTIONS = [
37
37
  // se reporta antes de pisarla, nunca después.
38
38
  const RUNTIME_PATHS = [
39
39
  '.ops/engine',
40
+ '.ops/agents',
41
+ '.ops/teams',
40
42
  'automatization/hooks',
41
43
  'automatization/runners',
42
44
  'automatization/workflows',
@@ -48,9 +50,15 @@ const TEMPLATE_OWNED = ['automatization/runners/README.md']
48
50
 
49
51
  // Dentro del paquete, lo que una instancia recibe en su raíz vive bajo `template/`; el catálogo,
50
52
  // los equipos y la automatización están en la raíz del paquete, y el motor en `engine/`.
53
+ const TEMPLATE_PREFIXES = ['planning/', 'organization/', 'integrations/']
54
+
51
55
  function sourceOf(relative) {
52
56
  if (relative === '.ops/engine') return 'engine'
53
- if (relative.startsWith('planning/')) return path.join('template', relative)
57
+ if (relative === '.ops/agents') return 'agents'
58
+ if (relative === '.ops/teams') return 'teams'
59
+ if (TEMPLATE_PREFIXES.some((prefix) => relative.startsWith(prefix))) {
60
+ return path.join('template', relative)
61
+ }
54
62
  return relative
55
63
  }
56
64
 
@@ -71,6 +79,19 @@ function engineAt(root, relative = '') {
71
79
  .find((candidate) => fs.existsSync(candidate)) || ''
72
80
  }
73
81
 
82
+ // Definiciones que consume el motor —cargos y equipos— y que por eso viajan con el paquete en vez
83
+ // de copiarse. Se reconocen por contener `system/`, que es el espacio del toolkit y no algo que un
84
+ // proyecto deba crear. Una sola implementación para las dos, o divergen.
85
+ function packageDir(root, name) {
86
+ const candidates = [
87
+ path.join(root, 'node_modules', '@ingeniomaps', 'cauce', name),
88
+ path.join(root, '.ops', name),
89
+ path.join(root, name),
90
+ ]
91
+ return candidates.find((dir) => fs.existsSync(path.join(dir, 'system'))
92
+ || fs.existsSync(path.join(dir, 'roles', 'system'))) || ''
93
+ }
94
+
74
95
  // Identidad de una entrada dentro de una colección, para detectar que el proyecto sobrescribe
75
96
  // algo del sistema. Las reglas y decisiones se identifican por su ID; el resto, por su nombre.
76
97
  const ID_PATTERN = /^(?:BR-)?[A-Z][A-Z0-9]*-\d{3}/
@@ -106,12 +127,35 @@ function overrides(root) {
106
127
  return found
107
128
  }
108
129
 
130
+ // Rutas que el toolkit dejó de materializar. Sin esto una instancia arrastra para siempre lo que
131
+ // alguna versión suya copió: `upgrade` agrega y reemplaza, pero nunca quitaba nada.
132
+ const RETIRED = [
133
+ 'agents/roles/system',
134
+ 'teams/system',
135
+ '.github/workflows/agent-learning.yml',
136
+ ]
137
+
138
+ // Aprendizaje que quedó dentro de una ruta retirada. Es lo único ahí que no se puede reponer, así
139
+ // que se detecta antes de borrar nada: perderlo en silencio sería peor que dejar el directorio.
140
+ function retiredWithLearning(root) {
141
+ const found = []
142
+ for (const relative of RETIRED) {
143
+ const dir = path.join(root, relative)
144
+ if (!fs.existsSync(dir)) continue
145
+ for (const file of treeFiles(dir)) {
146
+ if (!/(^|\/)learning\/(reports|proposals)\//.test(file)) continue
147
+ if (path.basename(file).startsWith('_')) continue
148
+ found.push(`${relative}/${file}`)
149
+ }
150
+ }
151
+ return found
152
+ }
153
+
109
154
  // Rutas que `upgrade` reemplaza. Todo lo que no aparezca acá pertenece al proyecto.
155
+ // Se listan aunque todavía no existan en la instancia: así un archivo nuevo del sistema llega en
156
+ // vez de esperar a que alguien lo cree a mano.
110
157
  function systemPaths(root) {
111
- const paths = []
112
- for (const file of SYSTEM_FILES) {
113
- if (fs.existsSync(path.join(root, file))) paths.push(file)
114
- }
158
+ const paths = [...SYSTEM_FILES]
115
159
  for (const collection of SYSTEM_COLLECTIONS) {
116
160
  const dir = path.join(root, collection, 'system')
117
161
  if (fs.existsSync(dir)) paths.push(`${collection}/system`)
@@ -132,28 +176,31 @@ function treeFiles(dir, prefix = '') {
132
176
  return found.sort()
133
177
  }
134
178
 
135
- // Archivos del runtime que el proyecto editó: existen en las dos partes y difieren. Un archivo
136
- // que sólo existe en la instancia es un agregado propio, no una edición, y sobrevive intacto.
137
- function localChanges(root, packageRoot) {
179
+ // Rutas que Cauce mantiene y que por eso conviene registrar y vigilar.
180
+ function trackedPaths() {
181
+ return [...RUNTIME_PATHS, ...SYSTEM_COLLECTIONS.map((collection) => `${collection}/system`)]
182
+ }
183
+
184
+ // Archivos que la empresa editó después de recibirlos. Se compara contra lo que Cauce entregó,
185
+ // no contra el paquete: si se comparara contra el paquete, cualquier mejora del toolkit se vería
186
+ // idéntica a una edición local y bloquearía la actualización.
187
+ function localChanges(root) {
188
+ const manifest = require('./manifest')
138
189
  const changed = []
139
- for (const target of RUNTIME_PATHS) {
140
- const from = path.join(packageRoot, sourceOf(target))
141
- const to = path.join(root, target)
142
- if (!fs.existsSync(to) || !fs.existsSync(from)) continue
143
- for (const file of treeFiles(from)) {
144
- if (TEMPLATE_OWNED.includes(`${target}/${file}`)) continue
145
- const mine = path.join(to, file)
146
- if (!fs.existsSync(mine)) continue
147
- if (fs.readFileSync(mine, 'utf8') !== fs.readFileSync(path.join(from, file), 'utf8')) {
148
- changed.push(`${target}/${file}`)
149
- }
150
- }
190
+ for (const target of trackedPaths()) {
191
+ const dir = path.join(root, target)
192
+ if (!fs.existsSync(dir)) continue
193
+ for (const file of manifest.edited(root, target, treeFiles(dir))) changed.push(`${target}/${file}`)
151
194
  }
152
195
  return changed
153
196
  }
154
197
 
155
198
  module.exports = {
199
+ RETIRED,
200
+ trackedPaths,
201
+ packageDir,
156
202
  RUNTIME_PATHS,
203
+ retiredWithLearning,
157
204
  engineAt,
158
205
  engineCandidates,
159
206
  TEMPLATE_OWNED,
@@ -12,11 +12,16 @@ const OUTCOMES = ['epic', 'report']
12
12
 
13
13
  // El proyecto manda sobre el sistema: un team propio con el mismo slug reemplaza al de `system/`,
14
14
  // que se sigue actualizando debajo sin que nadie tenga que forkearlo.
15
+ function systemTeams(root) {
16
+ return require('../core/ownership').packageDir(root, 'teams')
17
+ }
18
+
15
19
  function teamFile(root, slug) {
16
20
  if (!SLUG.test(slug || '')) throw new Error(`team inválido: ${slug || '(vacío)'}`)
17
21
  const own = path.join(root, 'teams', slug, 'team.json')
18
22
  if (fs.existsSync(own)) return own
19
- return path.join(root, 'teams', 'system', slug, 'team.json')
23
+ const system = systemTeams(root)
24
+ return system ? path.join(system, 'system', slug, 'team.json') : own
20
25
  }
21
26
 
22
27
  // Devuelve el motivo real: "no existe" y "ambiguo" piden acciones distintas de quien lo lea.
@@ -93,9 +98,13 @@ function slugsIn(dir) {
93
98
  }
94
99
 
95
100
  // Un slug aparece una sola vez aunque exista en los dos niveles: el del proyecto ya ganó.
101
+ // Los propios de la empresa y los que trae Cauce, sin duplicar un slug que ya ganó el proyecto.
96
102
  function list(root) {
97
- const teams = path.join(root, 'teams')
98
- const slugs = new Set([...slugsIn(teams), ...slugsIn(path.join(teams, 'system'))])
103
+ const system = systemTeams(root)
104
+ const slugs = new Set([
105
+ ...slugsIn(path.join(root, 'teams')),
106
+ ...(system ? slugsIn(path.join(system, 'system')) : []),
107
+ ])
99
108
  return [...slugs].sort()
100
109
  }
101
110
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -0,0 +1,37 @@
1
+ # Contexto por cargo
2
+
3
+ Un cargo del catálogo sabe de su profesión, no de esta empresa. Acá va lo que necesita saber para
4
+ trabajar en **este** contexto: `organization/roles/<slug>.md`.
5
+
6
+ ```markdown
7
+ # QA Engineer en Aparatejo
8
+
9
+ No hay ambiente de staging: la verificación corre contra el entorno local y contra producción con
10
+ datos de prueba. No hay equipo de soporte, así que un defecto que llegue a producción lo paga el
11
+ tiempo de las cuatro personas del equipo.
12
+
13
+ Cada imagen generada cuesta plata real: una prueba que dispare generación tiene que declarar cuántas.
14
+ ```
15
+
16
+ ## Qué va acá y qué no
17
+
18
+ **Acá**: restricciones reales de esta empresa, herramientas que usa, quién decide qué, límites de
19
+ presupuesto, deudas conocidas, lo que ya se intentó y no funcionó.
20
+
21
+ **Acá no**: cómo se hace la profesión. Que exista una técnica nueva de testing o que cambie una norma
22
+ le pasa a todas las empresas por igual: eso vive en el catálogo y llega actualizando Cauce.
23
+
24
+ ## Por qué está separado
25
+
26
+ Los cargos que trae Cauce viven en el paquete y se actualizan con él. Si el contexto de tu empresa
27
+ estuviera escrito dentro de un cargo del catálogo, cada actualización te obligaría a elegir entre
28
+ perder tu contexto o congelar el cargo.
29
+
30
+ Separados, las dos cosas mejoran sin pisarse: la profesión la mantiene el toolkit, el contexto lo
31
+ mantenés vos.
32
+
33
+ ## Si necesitás cambiar el cargo, no sólo darle contexto
34
+
35
+ Escribí tu propia versión en `agents/roles/<slug>/`, con el mismo slug: reemplaza a la del catálogo.
36
+ Tené en cuenta que a partir de ahí ese cargo deja de recibir mejoras, así que conviene sólo cuando el
37
+ contrato en sí no te sirve —no cuando lo que falta es contexto—.