@ingeniomaps/cauce 0.55.0 → 0.57.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 CHANGED
@@ -14,6 +14,52 @@ 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.57.0] - 2026-09-03
18
+
19
+ ### Agregado
20
+
21
+ - **`organization/workspace.md`: dónde va lo que sólo sabe tu proyecto.** El mapa real, las
22
+ integraciones con su entorno concreto y las excepciones de autonomía. Es tuyo y `upgrade` no lo toca.
23
+ Hasta ahora el paso 2 del README te mandaba completar `AGENTS.md`, que es del toolkit y se reemplaza
24
+ entero: no perdías nada —desde 0.55.0 el `upgrade` se detiene y lo nombra— pero te tocaba fusionar a
25
+ mano en cada versión, sobre el único archivo garantizado a entrar en conflicto. Es la separación que
26
+ el toolkit ya usa en `planning/rules/system/` y en `planning/delivery/project.md`.
27
+
28
+ **Si ya llenaste tu `AGENTS.md`**: movés esas secciones a `organization/workspace.md` y listo. El
29
+ `upgrade` te lo dice cuando se detenga, antes de que puedas descartarlas con `--force`.
30
+
31
+ ### Corregido
32
+
33
+ - **`check` te dice qué reglas deja de regir un override.** Un archivo propio con el nombre de uno de
34
+ `system/` lo reemplaza entero, no las reglas que mencionás: las que ese archivo definía y el tuyo no
35
+ redefine dejan de existir para tu proyecto. La advertencia nombraba el par de archivos y se leía como
36
+ benigna. El caso caro es una regla que el motor **sigue exigiendo** —R17 lo hace—: quedaba exigida sin
37
+ estar escrita en ningún lado. Ahora dice cuáles: `deja de regir R2, R17…`. Sigue siendo advertencia.
38
+
39
+ - **Instalar un runner ya no hace que `upgrade` te acuse de editar `AGENTS.md`.** En modo `embedded`,
40
+ `automation install` con Codex escribe sus instrucciones dentro de ese archivo, entre marcas. El
41
+ registro anotaba el bloque en un solo lado, así que el `upgrade` siguiente se detenía sobre un archivo
42
+ que nadie había tocado —lo había escrito el comando que el README manda correr justo después—. Lo que
43
+ vos escribas alrededor se sigue detectando igual.
44
+
45
+ ## [0.56.0] - 2026-09-03
46
+
47
+ ### Corregido
48
+
49
+ - **El upgrade que documentaba el README rompía el pin exacto que puso `init`.** La razón para no
50
+ saltear su primer paso es que «`init` fija la versión exacta, así que `npm update` no la mueve», y
51
+ el comando de al lado la desarmaba: npm guarda con caret por defecto, así que
52
+ `npm install --save-dev @ingeniomaps/cauce@latest` dejaba `^0.55.0`, y dentro de `0.x` ese caret
53
+ alcanza a los patches —que Cauce publica—. Un `npm install` en otra máquina podía dejar el motor en
54
+ una versión que `ops.config.json` no decía.
55
+
56
+ Se arregla por los dos lados. El comando lleva `--save-exact` en los tres lugares que lo dictan —el
57
+ README, el `make upgrade` de tu instancia y la salida de `upgrade --check`—, y además **`upgrade`
58
+ repone la versión exacta al terminar**, así que una instancia que ya quedó con un rango se repara
59
+ sola en la próxima actualización. **Qué cambia para vos**: `upgrade` puede tocar ahora una línea de
60
+ tu `package.json`, y lo dice cuando lo hace. El resto del manifiesto queda como estaba, y una
61
+ instancia sin `package.json` no recibe uno.
62
+
17
63
  ## [0.55.0] - 2026-09-02
18
64
 
19
65
  ### Corregido
package/README.md CHANGED
@@ -224,7 +224,9 @@ equivalentes.
224
224
  Después de inicializar:
225
225
 
226
226
  1. Edita `ops.config.json`: nombre, modo y raíces de código.
227
- 2. Completa `AGENTS.md`: límites de autonomía e integraciones reales.
227
+ 2. Completa `organization/workspace.md`: el mapa real, las integraciones con su entorno y las
228
+ excepciones de autonomía. Es del proyecto y `upgrade` no lo toca; `AGENTS.md`, en cambio, lo
229
+ mantiene Cauce entero.
228
230
  3. Completa `organization/company.md` y `organization/product.md` con el contexto estable de la empresa.
229
231
  4. Copia `planning/roadmap/epic-000-template.md` a `epic-001-<slug>.md`.
230
232
  5. Ejecuta `node tools/ops.js check planning` antes de activar cualquier runner.
@@ -266,13 +268,15 @@ existente detiene el `upgrade` antes de pisarlo.
266
268
  Son tres pasos y `make upgrade` hace los dos primeros:
267
269
 
268
270
  ```bash
269
- npm install --save-dev @ingeniomaps/cauce@latest # trae el motor nuevo
271
+ npm install --save-exact --save-dev @ingeniomaps/cauce@latest # trae el motor nuevo
270
272
  node tools/ops.js upgrade . # aplica system/ y el runtime
271
273
  node tools/ops.js automation install . claude # el wiring del runner
272
274
  ```
273
275
 
274
276
  El primero no se puede saltear: `init` fija la versión exacta, así que `npm update` no la mueve y
275
- `upgrade` compara contra el motor instalado —lo dice en su salida—. El tercero tampoco: los workflows y
277
+ `upgrade` compara contra el motor instalado —lo dice en su salida—. Va con `--save-exact` porque npm
278
+ guarda con caret por defecto, y ese caret es justamente lo que volvería falsa la frase anterior; si
279
+ alguna vez se instaló sin él, `upgrade` repone la versión exacta al terminar. El tercero tampoco: los workflows y
276
280
  las skills viven en el runner, no en la instancia, y `upgrade` no los toca. `upgrade` lo recuerda al
277
281
  terminar.
278
282
 
@@ -32,21 +32,17 @@ function mergeConfig(current, incoming) {
32
32
  return incoming
33
33
  }
34
34
 
35
- // Una entrada de hook que puso Cauce se reconoce por el guard al que apunta, y lo nuestro es lo que
36
- // efectivamente entregamos, no todo lo que vive en nuestra carpeta. Reconocer por directorio
37
- // desregistraba el guard propio del proyecto en cada reinstalación: `template/AGENTS.md` invita a
38
- // ponerlo justo ahí y promete que sobrevive a cada actualización, el archivo quedaba en disco, y lo
39
- // único que se veía era una línea diciendo que se había quitado una entrada obsoleta.
40
- // Se saca del archivo del usuario antes de fusionar para que el merge deje exactamente las de esta
41
- // versión, ni una más.
42
35
  const HOOK_PATH = /automatization\/hooks\/([a-z0-9-]+\.sh)(?:\s|$)/
43
36
 
44
- // Dos fuentes, y la segunda es la que cierra el borde. `expectedHooks()` dice qué entrega el motor de
45
- // hoy y alcanza para una instancia que nunca instaló; `delivered` es lo que esta instancia registró
46
- // haber recibido la última vez, y es lo único que reconoce un guard que entregamos en una versión y
47
- // retiramos en la siguiente: su nombre ya no está en la lista de hoy, así que sin el registro quedaría
48
- // vivo en la configuración llamando a un guard que Cauce ya no mantiene. Un guard propio del proyecto
49
- // no está en ninguna de las dos, que es exactamente por lo que sobrevive.
37
+ // Si una entrada de hook la puso Cauce. Lo nuestro es lo que efectivamente entregamos, no todo lo que
38
+ // vive en nuestra carpeta: `template/AGENTS.md` invita al proyecto a poner su guard justo ahí y le
39
+ // promete que sobrevive a cada actualización, así que reconocer por directorio lo desregistraba en
40
+ // cada reinstalación —el archivo quedaba en disco y el guard dejaba de correr—.
41
+ //
42
+ // Por eso son dos fuentes y no una. `expectedHooks()` es lo que entrega el motor de hoy, y alcanza
43
+ // para una instancia que nunca instaló; `delivered` es lo que ésta registró haber recibido, y es lo
44
+ // único que reconoce un guard que entregamos en una versión y retiramos en la siguiente, cuyo nombre
45
+ // ya no está en la primera. Un guard del proyecto no está en ninguna de las dos, y sobrevive.
50
46
  function isDelivered(command, delivered) {
51
47
  const hit = String(command).match(HOOK_PATH)
52
48
  if (!hit) return false
@@ -58,6 +54,8 @@ function deliveredHookCommands(commands) {
58
54
  return [...commands].filter((command) => HOOK_PATH.test(String(command)))
59
55
  }
60
56
 
57
+ // Saca del archivo del usuario lo que pusimos nosotros, para que el merge deje exactamente las
58
+ // entradas de esta versión y ni una más.
61
59
  function withoutDeliveredHooks(config, live, delivered = new Set()) {
62
60
  const dropped = []
63
61
  const walk = (node) => {
@@ -105,12 +103,10 @@ function reportRemoved(name, dropped, live, output) {
105
103
  function includesConfig(actual, expected) {
106
104
  if (Array.isArray(expected)) {
107
105
  return Array.isArray(actual) && expected.every((item) => actual.some((value) => {
108
- // Un grupo de hooks se compara por su contenido, no por su forma serializada. La comprobación es
109
- // «contiene», que es lo correcto, pero con el ítem entero como unidad un grupo `{matcher, hooks}`
110
- // con un hook de más dejaba de ser el mismo objeto y contaba como ausente: `doctor` llamaba
111
- // divergente a una configuración que tenía todo lo esperado, y el anidamiento —donde vive la
112
- // diferencia real— no se miraba nunca. La instalación promete conservar lo que el proyecto ya
113
- // tenía, así que sumar un hook propio a un grupo nuestro es exactamente lo que permite.
106
+ // Un grupo de hooks se compara por su contenido y no por su forma serializada, porque la
107
+ // instalación promete conservar lo que el proyecto ya tenía: con el ítem entero como unidad, un
108
+ // grupo con un hook propio de más contaba como ausente y `doctor` llamaba divergente a una
109
+ // configuración completa.
114
110
  if (item && typeof item === 'object' && Array.isArray(item.hooks)) {
115
111
  return value && typeof value === 'object'
116
112
  && value.matcher === item.matcher
@@ -384,8 +384,6 @@ function install(root, name, output = console, options = {}) {
384
384
  // guard que el runner intenta ejecutar y falla. Se quitan las nuestras y las vuelve a poner el merge.
385
385
  const live = new Set((JSON.stringify(incoming).match(/"command":"[^"]*"/g) || [])
386
386
  .map((entry) => JSON.parse(`{${entry}}`).command))
387
- // Lo que este adaptador anotó haber entregado la última vez. Es lo único que reconoce una entrada
388
- // nuestra cuyo guard el motor ya no trae: por nombre no se distingue de la que agregó el proyecto.
389
387
  const previous = new Set(recorded[deliveryKey(name, HOOKS_KEY)] || [])
390
388
  const clean = runner.config.owned
391
389
  ? { config: {}, dropped: [] }
@@ -399,9 +397,8 @@ function install(root, name, output = console, options = {}) {
399
397
  }
400
398
  output.log(`✓ ${name}: configuración instalada en ${runner.config.target}`)
401
399
  const deliveredPaths = { ...recorded }
402
- // Qué wiring de guards dejamos puesto, para poder retirarlo el día que el motor deje de traerlo. Se
403
- // anota lo que instalamos y no lo que quedó en el archivo: ahí conviven las entradas del proyecto, y
404
- // registrarlas nos autorizaría a borrar lo ajeno en la instalación siguiente.
400
+ // Se anota lo que instalamos, no lo que quedó en el archivo: ahí conviven las entradas del proyecto,
401
+ // y registrarlas nos autorizaría a borrar lo ajeno en la instalación siguiente.
405
402
  const ourHooks = deliveredHookCommands(live)
406
403
  if (ourHooks.length) deliveredPaths[deliveryKey(name, HOOKS_KEY)] = ourHooks
407
404
  else delete deliveredPaths[deliveryKey(name, HOOKS_KEY)]
@@ -417,6 +414,13 @@ function install(root, name, output = console, options = {}) {
417
414
  output.log(`✓ ${name}: sus instrucciones quedaron dentro de ${resolved.item.target}`)
418
415
  }
419
416
  deliveredPaths[deliveryKey(name, resolved.item.target)] = M.digest(resolved.target)
417
+ // Y en la otra sección, porque este archivo está en las dos: `AGENTS.md` es del sistema y es
418
+ // donde este runner deja su bloque. Anotarlo sólo acá dejaba a `localChanges` comparando contra
419
+ // el digest previo al bloque, así que el `upgrade` siguiente se detenía echándole a la empresa
420
+ // una edición que había hecho el comando de al lado. Lo que la empresa escriba alrededor cambia
421
+ // el digest igual, así que se sigue viendo.
422
+ const own = path.relative(root, resolved.target).split(path.sep).join('/')
423
+ if (O.SYSTEM_FILES.includes(own)) M.write(root, M.recordPaths(root, [own], M.read(root)))
420
424
  continue
421
425
  }
422
426
  if (status === 'ajeno' && ownFile) {
@@ -0,0 +1,73 @@
1
+ 'use strict'
2
+
3
+ // Cómo una instancia declara el motor en su `package.json`: ponerlo, reponerlo y sacarlo. Vive aparte
4
+ // del ciclo de vida porque su reloj es otro —npm y el versionado, no crear/actualizar/borrar— y porque
5
+ // sus tres consumidores no se solapan: `scaffold` declara, `upgrade` repone y `destroy` saca.
6
+
7
+ const fs = require('node:fs')
8
+ const path = require('node:path')
9
+ const F = require('../core/files')
10
+ const { fail } = require('./io')
11
+
12
+ // Declara el motor como dependencia exacta: el lockfile decide qué versión corre, no una copia.
13
+ // Conserva el manifiesto existente porque el repo anfitrión puede tener el suyo.
14
+ function declareEngine(manifest, version) {
15
+ let pkg = { name: path.basename(path.dirname(manifest)), private: true, version: '0.0.0' }
16
+ if (fs.existsSync(manifest)) {
17
+ try { pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')) } catch (error) {
18
+ fail(`package.json inválido en ${manifest}: ${error.message}`)
19
+ }
20
+ }
21
+ pkg.devDependencies = { ...pkg.devDependencies, '@ingeniomaps/cauce': version }
22
+ F.atomicWriteJson(manifest, pkg)
23
+ }
24
+
25
+ // Repone la versión exacta que `declareEngine` había dejado, y devuelve la que había si cambió algo.
26
+ //
27
+ // El pin no sobrevive por sí solo: npm guarda con caret, así que el `npm install @latest` del primer
28
+ // paso del upgrade lo convierte en un rango, y con eso deja de valer la razón que el README da para no
29
+ // saltear ese paso —«init fija la versión exacta, así que npm update no la mueve»—. Repararlo acá y no
30
+ // sólo documentar el comando es lo que vuelve la propiedad independiente de cómo se haya instalado.
31
+ //
32
+ // Sin manifiesto no hay nada que reponer: una instancia que no lo tiene resuelve el motor de otra
33
+ // forma, y crearle uno sería otra operación que nadie pidió en un upgrade.
34
+ function pinEngine(root, version) {
35
+ const manifest = path.join(root, 'package.json')
36
+ if (!fs.existsSync(manifest)) return null
37
+ let pkg
38
+ try { pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')) } catch { return 'ilegible' }
39
+ const current = (pkg.devDependencies || {})['@ingeniomaps/cauce']
40
+ if (!current || current === version) return null
41
+ declareEngine(manifest, version)
42
+ return current
43
+ }
44
+
45
+ // La inversa exacta de `declareEngine`: saca la clave que puso y nada más. El resto del manifiesto es
46
+ // del repo anfitrión aunque hoy no tenga otra cosa —un `package.json` vacío puede ser lo que alguien
47
+ // escribió para tener scripts— así que el archivo se borra sólo si es idéntico al que `declareEngine`
48
+ // habría creado desde cero, sin dependencias, sin scripts y con su `version: 0.0.0`.
49
+ //
50
+ // Lo que no se toca nunca es `node_modules/` ni el lockfile: los escribe npm, pueden tener dependencias
51
+ // del proyecto y borrarlos por nuestra cuenta destruye trabajo ajeno. Se nombran en la salida, que es
52
+ // la mitad que faltaba: `destroy` decía «tu repositorio queda donde está» y dejaba un `package.json`
53
+ // cuya única dependencia era Cauce. En un repo Rust eso es basura conspicua y nadie avisaba.
54
+ function undeclareEngine(manifest) {
55
+ if (!fs.existsSync(manifest)) return []
56
+ let pkg
57
+ try { pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')) } catch { return [] }
58
+ const dev = pkg.devDependencies || {}
59
+ if (!('@ingeniomaps/cauce' in dev)) return []
60
+ delete dev['@ingeniomaps/cauce']
61
+ pkg.devDependencies = dev
62
+ const ours = !Object.keys(dev).length && !Object.keys(pkg.dependencies || {}).length
63
+ && !Object.keys(pkg.scripts || {}).length && pkg.private === true && pkg.version === '0.0.0'
64
+ if (ours) {
65
+ fs.rmSync(manifest, { force: true })
66
+ return ['package.json (lo había creado init: sin dependencias ni scripts propios)']
67
+ }
68
+ if (!Object.keys(dev).length) delete pkg.devDependencies
69
+ F.atomicWriteJson(manifest, pkg)
70
+ return ['package.json: se quitó la dependencia del motor y el resto queda como estaba']
71
+ }
72
+
73
+ module.exports = { declareEngine, pinEngine, undeclareEngine }
@@ -16,6 +16,7 @@ const P = require('../planning/parser')
16
16
  const ST = require('../planning/state')
17
17
  const A = require('../automation')
18
18
  const { fail } = require('./io')
19
+ const { declareEngine, pinEngine, undeclareEngine } = require('./dependency')
19
20
 
20
21
  const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
21
22
 
@@ -29,10 +30,8 @@ function providerNames() {
29
30
  } catch { return [] }
30
31
  }
31
32
 
32
- // Devuelve, por archivo conservado, el digest de lo que el molde **habría** escrito. Adoptar Cauce en
33
- // un repositorio con contenido es `init --force`, y lo que se conserva ahí lo escribió la empresa: si
34
- // el manifiesto lo registra hasheando el disco queda declarado como entregado por Cauce con contenido
35
- // que Cauce nunca entregó, y el `upgrade` siguiente no ve ninguna edición local y lo reemplaza.
33
+ // Devuelve, por archivo conservado, el digest de lo que **habría** escrito. Quien registra la entrega
34
+ // necesita esos dos contenidos distintos, y el que quedó en disco no es uno de ellos.
36
35
  function copyTemplate(source, target, replacements, force, skip = [], quiet = false) {
37
36
  F.assertNoSymlinkPath(path.dirname(target), target)
38
37
  fs.mkdirSync(target, { recursive: true })
@@ -86,47 +85,6 @@ function copyRuntime(source, target, preserve = false, boundary = target, skip =
86
85
  return preserved
87
86
  }
88
87
 
89
- // Declara el motor como dependencia exacta: el lockfile decide qué versión corre, no una copia.
90
- // Conserva el manifiesto existente porque el repo anfitrión puede tener el suyo.
91
- function declareEngine(manifest, version) {
92
- let pkg = { name: path.basename(path.dirname(manifest)), private: true, version: '0.0.0' }
93
- if (fs.existsSync(manifest)) {
94
- try { pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')) } catch (error) {
95
- fail(`package.json inválido en ${manifest}: ${error.message}`)
96
- }
97
- }
98
- pkg.devDependencies = { ...pkg.devDependencies, '@ingeniomaps/cauce': version }
99
- F.atomicWriteJson(manifest, pkg)
100
- }
101
-
102
- // La inversa exacta de `declareEngine`: saca la clave que puso y nada más. El resto del manifiesto es
103
- // del repo anfitrión aunque hoy no tenga otra cosa —un `package.json` vacío puede ser lo que alguien
104
- // escribió para tener scripts— así que el archivo se borra sólo si es idéntico al que `declareEngine`
105
- // habría creado desde cero, sin dependencias, sin scripts y con su `version: 0.0.0`.
106
- //
107
- // Lo que no se toca nunca es `node_modules/` ni el lockfile: los escribe npm, pueden tener dependencias
108
- // del proyecto y borrarlos por nuestra cuenta destruye trabajo ajeno. Se nombran en la salida, que es
109
- // la mitad que faltaba: `destroy` decía «tu repositorio queda donde está» y dejaba un `package.json`
110
- // cuya única dependencia era Cauce. En un repo Rust eso es basura conspicua y nadie avisaba.
111
- function undeclareEngine(manifest) {
112
- if (!fs.existsSync(manifest)) return []
113
- let pkg
114
- try { pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')) } catch { return [] }
115
- const dev = pkg.devDependencies || {}
116
- if (!('@ingeniomaps/cauce' in dev)) return []
117
- delete dev['@ingeniomaps/cauce']
118
- pkg.devDependencies = dev
119
- const ours = !Object.keys(dev).length && !Object.keys(pkg.dependencies || {}).length
120
- && !Object.keys(pkg.scripts || {}).length && pkg.private === true && pkg.version === '0.0.0'
121
- if (ours) {
122
- fs.rmSync(manifest, { force: true })
123
- return ['package.json (lo había creado init: sin dependencias ni scripts propios)']
124
- }
125
- if (!Object.keys(dev).length) delete pkg.devDependencies
126
- F.atomicWriteJson(manifest, pkg)
127
- return ['package.json: se quitó la dependencia del motor y el resto queda como estaba']
128
- }
129
-
130
88
  // El andamiaje de una instancia, sin leer argv. `init` es la cáscara que traduce banderas a esto, y
131
89
  // el banco de evaluación lo llama directo: crear una instancia programáticamente no puede depender de
132
90
  // cómo venga escrita la línea de comandos.
@@ -155,10 +113,13 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
155
113
  if (fs.existsSync(dir)) deliveredPaths = M.record(root, relative, O.treeFiles(dir), deliveredPaths)
156
114
  }
157
115
  deliveredPaths = M.recordPaths(root, O.SYSTEM_FILES, deliveredPaths)
158
- // Lo conservado entra con el digest del molde, no con el del disco: la diferencia entre los dos es
159
- // justamente lo que `localChanges` tiene que ver para que `upgrade` se detenga antes de pisar el
160
- // archivo de la empresa. Sólo se pisan claves que ya están: el manifiesto declara lo que Cauce
161
- // rastrea, y un archivo propio fuera de esa frontera no le incumbe.
116
+ // Adoptar Cauce en un repositorio con contenido es `init --force`, y lo que se conserva ahí lo
117
+ // escribió la empresa. Registrarlo hasheando el disco lo declaraba entregado por Cauce con contenido
118
+ // que Cauce nunca entregó, así que `localChanges` no veía ninguna edición y el `upgrade` siguiente lo
119
+ // reemplazaba: entra con el digest del molde, y esa diferencia es lo que detiene al upgrade.
120
+ //
121
+ // Sólo se pisan claves que ya están: el manifiesto declara lo que Cauce rastrea, y un archivo propio
122
+ // fuera de esa frontera no le incumbe.
162
123
  for (const [file, hash] of Object.entries(preserved)) {
163
124
  const relative = path.relative(root, file).split(path.sep).join('/')
164
125
  if (relative in deliveredPaths) deliveredPaths[relative] = hash
@@ -301,6 +262,51 @@ function renameTeamsToFlows(root) {
301
262
  return moved
302
263
  }
303
264
 
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
+
304
310
  // Actualiza sólo lo que el toolkit declara suyo. Todo lo demás —planning, organization, reglas
305
311
  // propias, agentes editados— queda intacto por construcción, no por comparación.
306
312
  function upgrade(dir, cli) {
@@ -328,18 +334,19 @@ function upgrade(dir, cli) {
328
334
  const overrides = O.overrides(root)
329
335
 
330
336
  if (dry) {
331
- // Lo editado localmente se informa siempre, antes de mirar versiones: son dos preguntas distintas
332
- // —«¿hay algo más nuevo?» y «¿qué tengo editado que se perdería?»— y la segunda tiene respuesta
333
- // útil aunque la primera sea que no. Sin esto, el único modo que no toca nada era también el único
334
- // que no podía avisar del conflicto, justo en el estado normal entre actualizaciones: `init` fija
335
- // la versión exacta, así que instancia y motor coinciden casi todo el tiempo.
337
+ // Antes de mirar versiones, porque son dos preguntas distintas —«¿hay algo más nuevo?» y «¿qué
338
+ // tengo editado que se perdería?»— y la segunda tiene respuesta útil aunque la primera sea que no.
339
+ // Adentro del `if` de abajo, el único modo que no toca nada era el único que no podía avisar.
336
340
  for (const file of changed) console.log(` editado localmente: ${file}`)
337
341
  if (from === to) {
338
342
  // Contra el motor instalado, no contra lo publicado: la comparación es local y sin red. Decirlo
339
343
  // importa porque `init` fija la versión exacta, así que el motor no se mueve solo y esta línea,
340
344
  // a secas, se leía como «no hay nada nuevo» durante todas las versiones siguientes.
341
345
  console.log(`= ${to}: la instancia está al día con el motor instalado`)
342
- console.log(' para traer una versión más nueva: npm install --save-dev @ingeniomaps/cauce@latest')
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')
343
350
  if (changed.length) process.exit(1)
344
351
  return
345
352
  }
@@ -368,37 +375,9 @@ function upgrade(dir, cli) {
368
375
 
369
376
  if (changed.length && !force) {
370
377
  for (const file of changed) console.error(`✗ ${file}`)
371
- // Tres clases distintas, y antes eran dos: todo lo que no vivía bajo `system/` recibía el consejo
372
- // del runtime, así que editar el protocolo respondía con cómo desactivar un guard. Cada una tiene
373
- // su salida y decirle la ajena manda a buscar una configuración que no existe.
374
- const ruleFiles = changed.filter((file) => file.includes('/system/'))
375
- const runtime = changed.filter((file) => !file.includes('/system/')
376
- && O.RUNTIME_PATHS.some((base) => file.startsWith(`${base}/`)))
377
- const docs = changed.filter((file) => !ruleFiles.includes(file) && !runtime.includes(file))
378
- const advice = []
379
- if (ruleFiles.length) {
380
- advice.push(
381
- 'Las ruleFiles y decisiones bajo system/ son del toolkit. Para cambiar una, escribí la tuya al\n'
382
- + 'lado con el mismo ID: el proyecto manda y `check` lo reporta como override explícito.',
383
- )
384
- }
385
- if (runtime.length) {
386
- advice.push(
387
- 'El runtime es del toolkit: en vez de editarlo, agregá lo tuyo al lado con otro nombre —un\n'
388
- + 'guard propio sobrevive a cada actualización— y registralo en la configuración de tu runner,\n'
389
- + 'que sí es del proyecto. Para desactivar un guard alcanza con quitarlo de esa configuración.',
390
- )
391
- }
392
- if (docs.length) {
393
- advice.push(
394
- 'Esos docs son del toolkit y no llevan una línea de la empresa: se reemplazan enteros en\n'
395
- + 'cada actualización para que las mejoras lleguen. Lo que tu proyecto decide distinto va donde sí\n'
396
- + 'es suyo —una ADR propia, una regla propia, o `planning/delivery/project.md` para la entrega—.',
397
- )
398
- }
399
378
  fail(
400
379
  `\n${changed.length} archivo(s) que mantiene Cauce fueron editados y se perderían.\n\n` +
401
- `${advice.join('\n\n')}\n\nSi el cambio ya no te sirve, repetí con --force para descartarlo.`,
380
+ `${adviceFor(changed)}\n\nSi el cambio ya no te sirve, repetí con --force para descartarlo.`,
402
381
  )
403
382
  }
404
383
 
@@ -446,20 +425,25 @@ function upgrade(dir, cli) {
446
425
  const config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
447
426
  config.cauceVersion = to
448
427
  F.atomicWriteJson(path.join(root, 'ops.config.json'), config)
428
+ // Acá es donde las tres versiones se saben a la vez, así que es donde pueden quedar diciendo lo mismo.
429
+ const pinned = pinEngine(root, to)
449
430
 
450
431
  console.log(`✓ Cauce ${from || '(previa)'} → ${to}`)
451
432
  // Descartar con --force es legítimo; hacerlo sin dejar rastro no. Queda en la salida del comando,
452
433
  // que es la evidencia que el protocolo pide para cualquier cambio.
453
434
  for (const file of changed) console.log(`− descartado tu cambio en ${file}`)
454
435
  for (const relative of retired) console.log(`− retirado ${relative}: Cauce ya no lo distribuye`)
436
+ // Se dice porque explica un diff en un archivo que la empresa versiona, y que si no aparecería sin
437
+ // autor.
438
+ if (pinned === 'ilegible') console.log(' ⚠ package.json no se pudo leer: su versión quedó como estaba')
439
+ else if (pinned) console.log(` package.json: ${pinned} → ${to}, la versión exacta que acabás de aplicar`)
455
440
  printChangelog(from, to)
456
441
  console.log(` ${system.length} ruta(s) del sistema y ${O.RUNTIME_PATHS.length} del runtime actualizadas`)
457
442
  for (const override of overrides) {
458
443
  console.log(`= conservado ${override.collection}/${override.project}: sobrescribe ${override.system}`)
459
444
  }
460
- // Sólo cuando es cierto. Llegar acá con algo en `changed` significa que se corrió con --force y se
461
- // descartó contenido de la empresa: las líneas de arriba lo enumeran, y afirmar a continuación que
462
- // todo lo propio quedó intacto contradice a la única señal que recibe quien corrió el comando.
445
+ // Sólo cuando es cierto: llegar acá con algo en `changed` es haber descartado contenido de la
446
+ // empresa con --force, que las líneas de arriba enumeran.
463
447
  if (!changed.length) console.log(' planning, organization y todo lo propio quedaron intactos')
464
448
  // No se borra: sin la dependencia declarada, quitarle `.ops/` la dejaría sin motor. Se avisa y
465
449
  // decide una persona.
@@ -481,4 +465,4 @@ function upgrade(dir, cli) {
481
465
  for (const entry of FK.drift(root)) console.log(` ⚠ ${FK.driftLine(entry)}`)
482
466
  }
483
467
 
484
- module.exports = { copyTemplate, scaffold, providerNames, upgrade, destroy, PROJECT_ROOT }
468
+ module.exports = { copyTemplate, scaffold, providerNames, adviceFor, upgrade, destroy, PROJECT_ROOT }
package/engine/cli/ops.js CHANGED
@@ -16,21 +16,20 @@ const BOOT = require('./bootstrap')
16
16
  // Dónde aterriza una instancia cuando nadie eligió: una carpeta propia junto al código.
17
17
  const DEFAULT_TARGET = 'ops'
18
18
 
19
- // Dónde va la instancia cuando nadie eligió destino. Parada frecuente: el dev ya creó `acme-ops/` y
20
- // corre `init` adentro. Sin esto la instancia caía en `acme-ops/ops/` —una carpeta del toolkit dentro
21
- // de otra— y el proyecto quedaba llamándose «acme-ops». La carpeta que ya nombra al toolkit es la
22
- // instancia; no hay una segunda adentro.
23
- function implicitTarget(cwd) {
24
- return isInstanceDir(cwd) ? '.' : DEFAULT_TARGET
25
- }
26
-
27
- // La misma heurística, aplicada al destino ya resuelto: una carpeta que ya nombra al toolkit **es** la
28
- // instancia, y el código de la empresa vive al lado.
19
+ // Una carpeta que ya nombra al toolkit **es** la instancia; no hay una segunda adentro, y el código de
20
+ // la empresa vive al lado. Es la única regla, y de ella salen las dos decisiones que `init` toma sin
21
+ // que nadie las escriba: dónde aterriza y en qué modo.
29
22
  function isInstanceDir(dir) {
30
23
  const base = path.basename(dir)
31
24
  return base === DEFAULT_TARGET || base.endsWith('-ops')
32
25
  }
33
26
 
27
+ // Parada frecuente: el dev ya creó `acme-ops/` y corre `init` adentro. Sin esto la instancia caía en
28
+ // `acme-ops/ops/` y el proyecto quedaba llamándose «acme-ops».
29
+ function implicitTarget(cwd) {
30
+ return isInstanceDir(cwd) ? '.' : DEFAULT_TARGET
31
+ }
32
+
34
33
  // El nombre sale de la carpeta del proyecto, no de la que aloja la instancia: `ops/` y `acme-ops/`
35
34
  // nombran al toolkit, y quien lee `project` en la configuración espera leer «acme».
36
35
  function defaultName(root) {
@@ -79,11 +78,9 @@ async function init(target, cli) {
79
78
  // install` ya asume —el wiring del runner va al padre, donde se abre la herramienta—, así que lo
80
79
  // único que faltaba era que fuera lo que pasa cuando no se elige nada.
81
80
  const root = path.resolve(target || implicitTarget(process.cwd()))
82
- // El modo lo decide a dónde apunta el destino, no si alguien escribió el argumento. `init` e `init .`
83
- // resuelven el mismo directorio y elegían modos opuestos: escribir el punto —la forma más natural de
84
- // decir «acá»— daba embedded, y ahí la raíz del workspace pasa a ser la carpeta ops misma, así que
85
- // `guard-workspace-boundary` deja de reconocer los repos hermanos y el wiring del runner se escribe
86
- // adentro en vez de al lado, donde el dev abre su herramienta.
81
+ // Del destino resuelto y no de si alguien escribió el argumento: `init` e `init .` apuntan al mismo
82
+ // directorio y elegían modos opuestos, así que escribir el punto daba el layout contrario al de
83
+ // arriba sin que nadie lo pidiera.
87
84
  const mode = cli.value('--mode', isInstanceDir(root) ? 'sidecar' : 'embedded')
88
85
  if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
89
86
  const name = cli.value('--name', defaultName(root))
@@ -80,7 +80,13 @@ function check(dir, cli) {
80
80
  // Sobrescribir una entrada de system/ es legítimo y esperado; lo que no puede pasar es que
81
81
  // ocurra en silencio, porque esa entrada deja de recibir las mejoras del toolkit.
82
82
  for (const override of O.overrides(path.resolve(root, '..'))) {
83
- warnings.push(`${override.collection}/${override.project} sobrescribe ${override.system} (override explícito)`)
83
+ // Y con qué se queda el proyecto: un override sano redefine lo que reemplaza, y el que deja IDs
84
+ // afuera los retira sin decirlo. Nombrarlos es lo único que separa una decisión de un descuido.
85
+ const retired = override.collection === 'planning/rules'
86
+ ? PC.retiredByOverride(root, override.project)
87
+ : []
88
+ warnings.push(`${override.collection}/${override.project} sobrescribe ${override.system} `
89
+ + `(override explícito)${retired.length ? `; deja de regir ${retired.join(', ')}` : ''}`)
84
90
  }
85
91
  // Misma regla para los cargos, que es donde más caro sale: un fork se hace una vez y se olvida.
86
92
  const FK = require('../agents/fork')
@@ -179,7 +179,8 @@ function onboard(rootArg, cli, runner = '') {
179
179
  console.log(runner
180
180
  ? `\n→ Abrí ${runner} acá y contestale esa pregunta.`
181
181
  : '\n→ Contestá esa pregunta cuando corras el arranque.')
182
- console.log(' Con tus respuestas escribe organization/, el mapa real de AGENTS.md y la primera épica.')
182
+ console.log(' Con tus respuestas escribe organization/, el mapa real en'
183
+ + ' organization/workspace.md y la primera épica.')
183
184
  }
184
185
 
185
186
  // El registro de proveedores, leído igual por todos los que lo tocan. `list` lo parseaba suelto y un
@@ -74,7 +74,7 @@ function guide(root, services = []) {
74
74
  function missingSections(root) {
75
75
  const warnings = []
76
76
  const template = path.join(PACKAGE_ROOT, 'template', 'organization')
77
- for (const name of ['company.md', 'product.md', 'domains.md']) {
77
+ for (const name of ['company.md', 'product.md', 'domains.md', 'workspace.md']) {
78
78
  const written = path.join(root, 'organization', name)
79
79
  if (!fs.existsSync(written) || !fs.existsSync(path.join(template, name))) continue
80
80
  const headings = (text) => new Set((text.match(/^##\s+(.+)$/gm) || []).map((line) => line.trim()))
@@ -99,7 +99,11 @@ function missingSections(root) {
99
99
  // Sólo cuando la instancia ya tiene contexto escrito: antes del arranque no hay dónde estuvieran.
100
100
  function orphanCredentials(root) {
101
101
  if (guide(root).fresh) return []
102
- const contracts = ['AGENTS.md', path.join('planning', 'HUMAN_ACTIONS.md')]
102
+ // El mapa del proyecto vive en `organization/workspace.md` desde que `AGENTS.md` pasó a ser del
103
+ // toolkit entero; el viejo se sigue mirando porque una instancia anterior lo tiene ahí y una
104
+ // credencial ya declarada no debería volver a reportarse como huérfana por haber mudado el archivo.
105
+ const contracts = [path.join('organization', 'workspace.md'), 'AGENTS.md',
106
+ path.join('planning', 'HUMAN_ACTIONS.md')]
103
107
  .map((file) => { try { return fs.readFileSync(path.join(root, file), 'utf8') } catch { return '' } })
104
108
  .join('\n')
105
109
  if (!contracts) return []
@@ -172,6 +172,19 @@ function ruleIds(file) {
172
172
  return [...P.read(file).matchAll(/^##\s+([A-Z]\d+)\s+[—-]/gm)].map((match) => match[1])
173
173
  }
174
174
 
175
+ // Qué IDs deja de regir un override por nombre: los que definía el archivo del sistema y el propio no
176
+ // redefine. Reemplazar el archivo entero es la función del override y está documentada; lo que no se
177
+ // veía es la consecuencia, porque la advertencia nombraba el par de archivos y no la diferencia. El
178
+ // caso caro es una regla que el motor sigue exigiendo —R17 lo hace—: queda exigida y sin estar escrita
179
+ // en ningún lado, y quien la vea fallar la va a buscar en `rules/`, donde ya no está.
180
+ function retiredByOverride(dir, name) {
181
+ const rules = path.join(dir, 'rules')
182
+ const system = path.join(rules, 'system', name)
183
+ if (!fs.existsSync(system)) return []
184
+ const redefined = ruleIds(path.join(rules, name))
185
+ return ruleIds(system).filter((id) => !redefined.includes(id))
186
+ }
187
+
175
188
  function validateRules(dir) {
176
189
  const rules = path.join(dir, 'rules')
177
190
  const owner = new Map()
@@ -418,6 +431,7 @@ module.exports = {
418
431
  oversizedUnits,
419
432
  validateAdr,
420
433
  validateRules,
434
+ retiredByOverride,
421
435
  validateBacklogStructure,
422
436
  validCommitTrace,
423
437
  validDecisionTrace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.55.0",
3
+ "version": "0.57.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",
@@ -2,24 +2,18 @@
2
2
 
3
3
  Este archivo gobierna el qué y el cuándo. `planning/PROTOCOL.md` gobierna el flujo y
4
4
  `planning/rules/` el cómo: sus reglas rigen cada tarea y se leen antes de empezar, no cuando algo sale
5
- mal. Ahí está la conducta que no depende de este proyecto —trato con sistemas externos, qué se debe
6
- entregar aun al negarse—; acá, lo que sí depende.
5
+ mal. Los tres los mantiene Cauce y valen para cualquier proyecto. Lo que este proyecto tiene de propio
6
+ —su mapa, sus integraciones, hasta dónde llega la autonomía acá— vive en `organization/workspace.md`.
7
7
 
8
- ## Mapa real
8
+ ## Qué sabe este proyecto y no este archivo
9
9
 
10
- Completar antes de la primera tarea:
10
+ El mapa real —qué se construye, qué repos existen, qué está fuera de alcance—, las integraciones con su
11
+ entorno concreto y las excepciones de autonomía viven en **`organization/workspace.md`**, que es del
12
+ proyecto y `upgrade` no toca. Acá no: este archivo lo mantiene Cauce y se reemplaza entero en cada
13
+ actualización, así que lo que escribieras se perdería o te haría fusionar a mano todas las veces.
11
14
 
12
- - Qué producto se construye.
13
- - Qué repos o servicios existen y dónde viven.
14
- - Dónde documenta cada servicio sus comandos de test, lint y build.
15
- - Qué directorios son legacy o están fuera de alcance.
16
-
17
- El mapa no debe duplicar documentación técnica: enlaza a la fuente de verdad de cada servicio.
18
-
19
- ## Integraciones y ambientes
20
-
21
- R12 fija el trato con todo sistema externo: real y de sólo lectura mientras no se lo nombre como
22
- sandbox. Las excepciones de este proyecto —y sólo ellas— van acá, con el entorno concreto.
15
+ Es la misma separación que `planning/rules/system/` con las reglas propias y que `planning/delivery/`
16
+ con `project.md`: lo que el toolkit mejora, aparte de lo que sólo vos podés escribir.
23
17
 
24
18
  ## Cargos disponibles
25
19
 
@@ -130,6 +124,9 @@ Registra la acción exacta en `planning/HUMAN_ACTIONS.md` y, si bloquea todo, cr
130
124
  Nunca amplía el alcance, promueve sus propias ideas, reescribe el proceso durante una tarea, usa
131
125
  `git add .`/`git add -A`, hace push/force/amend, ni afirma éxito sin evidencia real.
132
126
 
127
+ Eso rige sin que nadie escriba nada. Lo que este proyecto amplíe o restrinja va en
128
+ `organization/workspace.md`, con su razón; los cuatro límites del párrafo anterior no se amplían ahí.
129
+
133
130
  ## Definición de terminado
134
131
 
135
132
  1. La aceptación se observa y queda cubierta por pruebas cuando existe superficie testeable.
package/template/Makefile CHANGED
@@ -29,7 +29,7 @@ destroy: ## Muestra qué se pierde al borrar esta instancia (borrar exige --forc
29
29
  @node tools/ops.js destroy .
30
30
 
31
31
  upgrade: ## Trae la última versión de Cauce y la aplica, sin tocar lo del proyecto
32
- @npm install --save-dev @ingeniomaps/cauce@latest
32
+ @npm install --save-exact --save-dev @ingeniomaps/cauce@latest
33
33
  @node tools/ops.js upgrade .
34
34
 
35
35
  automation-check: ## Comprueba hooks, workflows y adaptadores
@@ -7,6 +7,8 @@ Con molde, y `check` avisa si a alguno le faltan secciones del original:
7
7
  - `company.md`: misión, modelo de negocio, objetivos, estructura y derechos de decisión.
8
8
  - `product.md`: problema, usuarios, propuesta de valor y límites.
9
9
  - `domains.md`: lenguaje ubicuo, dominios y dueños.
10
+ - `workspace.md`: el mapa real, las integraciones con su entorno y las excepciones de autonomía.
11
+ Es lo que `AGENTS.md` no puede llevar: ese archivo lo mantiene Cauce y se reemplaza al actualizar.
10
12
 
11
13
  Recomendados, sin molde: escribilos con la forma que le sirva a este proyecto.
12
14
 
@@ -0,0 +1,34 @@
1
+ # Este workspace
2
+
3
+ Lo que sólo sabe este proyecto: qué hay, contra qué se conecta y hasta dónde puede decidir solo un
4
+ runner. `AGENTS.md` gobierna el qué y el cuándo para cualquier proyecto; esto es lo que cambia entre
5
+ uno y otro, y por eso vive acá, donde Cauce no escribe: `upgrade` no lo toca nunca.
6
+
7
+ ## Mapa real
8
+
9
+ Completar antes de la primera tarea:
10
+
11
+ - Qué producto se construye.
12
+ - Qué repos o servicios existen y dónde viven.
13
+ - Dónde documenta cada servicio sus comandos de test, lint y build.
14
+ - Qué directorios son legacy o están fuera de alcance.
15
+
16
+ El mapa no debe duplicar documentación técnica: enlaza a la fuente de verdad de cada servicio.
17
+
18
+ ## Integraciones y ambientes
19
+
20
+ R12 fija el trato con todo sistema externo: real y de producción mientras no se lo nombre como sandbox.
21
+ Las excepciones de este proyecto —y sólo ellas— van acá, nombrando el entorno concreto.
22
+
23
+ Las credenciales que cada integración necesita se nombran acá o en `planning/HUMAN_ACTIONS.md`, con
24
+ quién las carga y dónde: `check` avisa cuando el proyecto declara una variable que no aparece en
25
+ ninguno de los dos, porque una credencial sin dueño no rompe nada hasta el día del despliegue.
26
+
27
+ ## Excepciones de autonomía
28
+
29
+ Los límites que rigen sin escribir nada están en `AGENTS.md`, y son los mismos para todo proyecto. Acá
30
+ va lo que este proyecto amplía o restringe, con su razón: un servicio donde el runner no puede tocar
31
+ migraciones, un repo donde sí puede commitear directo, un entorno de pruebas que es seguro escribir.
32
+
33
+ Lo que no se puede ampliar acá: promover trabajo propio, prometer fechas, inventar evidencia o exceder
34
+ la autoridad de un cargo. Eso no depende del proyecto.
@@ -19,4 +19,10 @@ fuera de él para que no existan dos reglas con el mismo nombre.
19
19
  Para cambiar una regla del sistema, escribí la tuya **con el mismo nombre de archivo**: ahí redefinir
20
20
  sus números es la función, la del proyecto manda, y `check` reporta el override en vez de fallar.
21
21
 
22
+ Lo que reemplaza es **el archivo entero, no las reglas que mencionás**: las que ese archivo del sistema
23
+ definía y el tuyo no redefine dejan de regir para tu proyecto. Alguna la sigue exigiendo el motor —R17
24
+ lo hace—, y ahí queda exigida sin estar escrita en ningún lado. Por eso `check` te dice cuáles son
25
+ —«deja de regir R2, R17…»— y por eso conviene, antes de sobrescribir un archivo, mirar si lo tuyo era
26
+ una regla nueva: si lo era, va acá al lado como `P1..Pn` y no se lleva nada puesto.
27
+
22
28
  Las convenciones específicas de lenguaje viven junto al servicio que usa ese lenguaje.