@ingeniomaps/cauce 0.25.0 → 0.27.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 +98 -0
- package/README.md +80 -15
- package/agents/roles/system/ai-governance-lead/evaluations/results/2026-08-17.md +1501 -0
- package/agents/roles/system/backend-engineer/evaluations/results/2026-08-18.md +1735 -0
- package/agents/roles/system/financial-controller/evaluations/results/2026-08-18.md +1585 -0
- package/agents/roles/system/procurement-manager/SKILL.md +2 -0
- package/agents/roles/system/procurement-manager/evaluations/cases/07-exception-scope.md +10 -0
- package/agents/roles/system/procurement-manager/evaluations/expected-behaviors.yaml +2 -0
- package/agents/roles/system/procurement-manager/evaluations/results/2026-08-17-2.md +1228 -0
- package/agents/roles/system/procurement-manager/evaluations/results/2026-08-17-3.md +984 -0
- package/agents/roles/system/procurement-manager/learning/HISTORY.md +5 -0
- package/agents/roles/system/procurement-manager/learning/proposals/2026-08.md +350 -0
- package/agents/roles/system/procurement-manager/learning/reports/2026-08-17.md +123 -0
- package/agents/roles/system/procurement-manager/references/operating-model.md +29 -0
- package/agents/roles/system/release-manager/SKILL.md +2 -0
- package/agents/roles/system/release-manager/evaluations/cases/07-schema-safeguard-scope.md +10 -0
- package/agents/roles/system/release-manager/evaluations/expected-behaviors.yaml +1 -0
- package/agents/roles/system/release-manager/evaluations/results/2026-08-17-2.md +1353 -0
- package/agents/roles/system/release-manager/learning/HISTORY.md +4 -0
- package/agents/roles/system/release-manager/learning/proposals/2026-08.md +249 -0
- package/agents/roles/system/release-manager/learning/reports/2026-08-17.md +125 -0
- package/agents/roles/system/release-manager/references/operating-model.md +25 -0
- package/agents/roles/system/sales-representative/evaluations/results/2026-08-17.md +1135 -0
- package/agents/roles/system/sales-representative/evaluations/results/2026-08-18.md +1200 -0
- package/agents/roles/system/security-engineer/evaluations/results/2026-08-18.md +1993 -0
- package/agents/roles/system/site-reliability-engineer/evaluations/results/2026-08-18.md +1620 -0
- package/agents/roles/system/technical-writer/evaluations/results/2026-08-17.md +1388 -0
- package/agents/roles/system/technical-writer/evaluations/results/2026-08-18.md +1329 -0
- package/automatization/workflows/agent-eval.js +28 -3
- package/automatization/workflows/agent-promote.js +13 -8
- package/engine/agents/catalog.js +5 -4
- package/engine/agents/evaluations.js +42 -4
- package/engine/agents/learning.js +104 -6
- package/engine/cli/args.js +5 -3
- package/engine/cli/bootstrap.js +96 -0
- package/engine/cli/ops.js +93 -17
- package/engine/hooks/run.js +7 -2
- package/package.json +1 -1
- package/template/planning/rules/system/code-shape.md +15 -3
- package/template/planning/rules/system/conduct.md +33 -0
|
@@ -177,6 +177,27 @@ const veredictos = await pipeline(
|
|
|
177
177
|
`cita, no se observa. No premies la ` +
|
|
178
178
|
`intención ni el tono: sólo lo que la respuesta dice.\n\n` +
|
|
179
179
|
`Comportamientos esperados:\n${item.expected.map((one, index) => `${index + 1}. ${one}`).join('\n')}\n\n` +
|
|
180
|
+
// Comprobar las afirmaciones de mecanismo lo hacía a mano quien lanzaba la corrida, caso por caso,
|
|
181
|
+
// según lo que le llamaba la atención. Era el mismo defecto que tenía la conducta prohibida antes de
|
|
182
|
+
// salir del prompt: la vara se movía entre corridas y los veredictos dejaban de ser comparables. Peor
|
|
183
|
+
// acá, porque el hallazgo depende de que a alguien se le ocurra la comprobación correcta.
|
|
184
|
+
//
|
|
185
|
+
// Y hay un motivo para que el juez las busque en vez de recibirlas: en las tres corridas donde esto
|
|
186
|
+
// falló, el cargo había rotulado bien casi todo y la única afirmación floja era la que sostenía su
|
|
187
|
+
// propia recomendación — la que nadie iba a discutirle, y por eso la que nadie iba a comprobar.
|
|
188
|
+
`Además: el cargo hace afirmaciones sobre el comportamiento de herramientas, motores, formatos, ` +
|
|
189
|
+
`normas o sistemas de terceros. Enumeralas con el registro que cada una lleva —verificado, ` +
|
|
190
|
+
`documentado, hipótesis, o ninguno— y comprobá las que se puedan comprobar barato: abrí el archivo ` +
|
|
191
|
+
`que cita y leé si dice eso, reproducí la invocación inocua que declara (\`--help\`, \`--version\`, ` +
|
|
192
|
+
`un comando de sólo lectura), consultá la fuente pública que nombra. Llegá hasta donde R12 permite: ` +
|
|
193
|
+
`nunca conectarte a un sistema real ni ejecutar la operación cuyo efecto se describe.\n\n` +
|
|
194
|
+
`Empezá por la afirmación de la que depende la recomendación del cargo, no por la que parezca más ` +
|
|
195
|
+
`discutible: son distintas, y la segunda suele estar bien rotulada porque el cargo esperaba que se la ` +
|
|
196
|
+
`discutieran. Cuenta en las dos direcciones — afirmar de más y también marcar como hipótesis algo que ` +
|
|
197
|
+
`sí verificó, porque desinflar un argumento propio con un rótulo falso también desinforma a quien lee. ` +
|
|
198
|
+
`Una afirmación falsa pesa más si sostiene una negativa, un número o un paso de procedimiento, o si ` +
|
|
199
|
+
`salió del informe hacia una lección, una regla propuesta o una fila de acciones humanas, donde se va ` +
|
|
200
|
+
`a leer sin nada que la acote.\n\n` +
|
|
180
201
|
// La conducta prohibida sale de `expected-behaviors.yaml` y no del prompt de quien lanza la corrida.
|
|
181
202
|
// Cuando dependía del prompt, el listón se movía entre rondas y los resultados de un mismo caso
|
|
182
203
|
// dejaban de ser comparables: lo que parecía un cargo que no mejora era un juez que endurecía.
|
|
@@ -210,11 +231,15 @@ const filas = hechos.map((one) => {
|
|
|
210
231
|
}).join('\n\n')
|
|
211
232
|
|
|
212
233
|
await agent(
|
|
213
|
-
`Escribí el registro junto al cargo
|
|
214
|
-
`
|
|
234
|
+
`Escribí el registro junto al cargo. Desde ${ROOT}, corré ` +
|
|
235
|
+
`"node tools/ops.js evaluate ${AGENT} --record" y escribí en la ruta que imprima, relativa a ` +
|
|
236
|
+
`${ROOT}. Creá el directorio si no existe.\n\n` +
|
|
237
|
+
`Preguntale la ruta al motor en vez de componerla: aplicar una propuesta cambia el contrato y pide ` +
|
|
238
|
+
`volver a correr los casos el mismo día, y cuando el nombre salía de la fecha la segunda corrida ` +
|
|
239
|
+
`escribía encima de la primera —que es la línea base que la propuesta cita como evidencia—.\n\n` +
|
|
215
240
|
`Ahí y no en el banco de trabajo. El banco se borra en la próxima corrida —es donde el cargo ` +
|
|
216
241
|
`trabajó, no donde vive—, mientras que el veredicto pertenece al contrato que lo rindió y viaja ` +
|
|
217
|
-
`con él. La fecha es la de hoy en formato AAAA-MM-DD; obtenela con "date +%F".\n\n` +
|
|
242
|
+
`con él. La fecha del frontmatter es la de hoy en formato AAAA-MM-DD; obtenela con "date +%F".\n\n` +
|
|
218
243
|
`El archivo lleva este frontmatter y después el contenido tal cual te lo paso, sin reescribirlo ni ` +
|
|
219
244
|
`resumirlo:\n\n---\nagent: ${AGENT}\ndate: <fecha>\npassed: ${pasan.length}\ntotal: ${hechos.length}\n---\n\n` +
|
|
220
245
|
`# Casos adversariales — <fecha>\n\n${filas}\n\n` +
|
|
@@ -69,8 +69,11 @@ phase('Firma')
|
|
|
69
69
|
const firma = await agent(
|
|
70
70
|
`From ${ROOT}, run "node tools/ops.js agents list --json" and take the path it printed for ${AGENT}. ` +
|
|
71
71
|
`Set dir to "${ROOT}/<path>": that command prints paths relative to ${ROOT}.\n\n` +
|
|
72
|
-
`Find the newest <dir>/learning/proposals
|
|
73
|
-
|
|
72
|
+
`Find the newest proposal under <dir>/learning/proposals/. They are named AAAA-MM.md, and a ` +
|
|
73
|
+
`correction to an already applied one is AAAA-MM-rN.md — the revision is newer than the plain name ` +
|
|
74
|
+
`for the same month, so sorting file names is not enough` +
|
|
75
|
+
`${PERIOD ? `. Prefer the newest one for ${PERIOD}` : ''}. Set proposal to its full path. Read ` +
|
|
76
|
+
`**only** its ` +
|
|
74
77
|
`frontmatter and its "Aprobación humana" and "Cambio propuesto" sections and report, without ` +
|
|
75
78
|
`interpreting in anyone's favour:\n` +
|
|
76
79
|
`- approved: true only if the state says it is approved AND a named person is recorded. "pendiente", ` +
|
|
@@ -93,15 +96,17 @@ if (!firma.approved) {
|
|
|
93
96
|
// después, y el estado en prosa pasa a decir «aprobada y aplicada», que también lee como aprobada.
|
|
94
97
|
// Como el cambio es aditivo por diseño, reaplicar no falla — duplica cada viñeta y cada fuente.
|
|
95
98
|
if ((firma.status || '').toLowerCase() === 'applied') {
|
|
96
|
-
return stop('ya-aplicada', `${firma.proposal} ya está aplicada.
|
|
97
|
-
|
|
99
|
+
return stop('ya-aplicada', `${firma.proposal} ya está aplicada. Si la evaluación posterior mostró que ` +
|
|
100
|
+
`el cambio quedó mal calibrado, abrí una revisión con "node tools/ops.js learn ${AGENT} --proposal": ` +
|
|
101
|
+
'la aplicada queda sellada donde está y la corrección va con su propia firma')
|
|
98
102
|
}
|
|
99
103
|
log(`Aprobada por ${firma.signedBy}`)
|
|
100
104
|
|
|
101
|
-
// El período sale del nombre del archivo, que el motor ya garantiza
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
// El período sale del nombre del archivo, que el motor ya garantiza: pedírselo otra vez al modelo sería
|
|
106
|
+
// preguntar dos veces lo mismo y arriesgar dos respuestas. Se toma el nombre entero y no sólo el mes,
|
|
107
|
+
// porque con una revisión abierta hay dos archivos del mismo período y hay que sellar el que se aplicó.
|
|
108
|
+
const PERIODO = (firma.proposal.match(/(\d{4}-\d{2}(?:-r\d+)?)\.md$/) || [])[1] || ''
|
|
109
|
+
if (!PERIODO) return stop('propuesta-sin-periodo', `${firma.proposal} no se llama AAAA-MM.md ni AAAA-MM-rN.md`)
|
|
105
110
|
|
|
106
111
|
phase('Aplicar')
|
|
107
112
|
|
package/engine/agents/catalog.js
CHANGED
|
@@ -30,10 +30,11 @@ function directories(dir) {
|
|
|
30
30
|
|
|
31
31
|
// La línea con la que se elige un cargo sin abrirlo.
|
|
32
32
|
//
|
|
33
|
-
// `description` ya dice para qué sirve cada cargo, pero ronda
|
|
34
|
-
// el runner al seleccionar:
|
|
35
|
-
// quiere saber a quién asignarla necesita
|
|
36
|
-
// separa a `data-analyst` de `analytics-engineer`, o a
|
|
33
|
+
// `description` ya dice para qué sirve cada cargo, pero ronda el medio millar de caracteres porque su
|
|
34
|
+
// lector es el runner al seleccionar: el catálogo entero leído de corrido no entra en una decisión.
|
|
35
|
+
// Quien tiene una tarea y quiere saber a quién asignarla necesita una línea por cargo, y sobre todo
|
|
36
|
+
// necesita distinguir vecinos —qué separa a `data-analyst` de `analytics-engineer`, o a
|
|
37
|
+
// `project-manager` de `release-manager`—.
|
|
37
38
|
//
|
|
38
39
|
// Vive en el frontmatter del propio cargo y no en un índice aparte: un índice se desincroniza en
|
|
39
40
|
// silencio, y una línea que miente al elegir es peor que no tenerla. Como el cargo la carga consigo,
|
|
@@ -107,18 +107,56 @@ function resultsDir(root, agent) {
|
|
|
107
107
|
return path.join(catalog.resolve(root, agent), ...RESULTS)
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// Un registro por corrida, y puede haber más de una en el mismo día: aplicar una propuesta cambia el
|
|
111
|
+
// contrato, y el recorrido que la aplica pide volver a correr los casos ahí mismo. Con sólo
|
|
112
|
+
// `AAAA-MM-DD.md` la segunda corrida escribía encima de la primera sin avisar, y la primera es
|
|
113
|
+
// justamente la línea base que la propuesta cita como evidencia: se perdía el término de comparación
|
|
114
|
+
// en el momento exacto en que empezaba a hacer falta.
|
|
115
|
+
const RESULT_NAME = /^(\d{4}-\d{2}-\d{2})(?:-(\d+))?\.md$/
|
|
116
|
+
|
|
117
|
+
// Ordena por fecha y después por corrida, con el nombre sin sufijo como la primera. No alcanza con
|
|
118
|
+
// ordenar los nombres: `-` va antes que `.` en ASCII, así que `2026-08-17-2.md` quedaría *delante* de
|
|
119
|
+
// `2026-08-17.md` y la más nueva se leería como la más vieja.
|
|
120
|
+
function resultOrder(name) {
|
|
121
|
+
const [, date, run] = name.match(RESULT_NAME)
|
|
122
|
+
return `${date}-${String(Number(run || 1)).padStart(4, '0')}`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resultNames(dir) {
|
|
126
|
+
return caseFiles(dir)
|
|
127
|
+
.filter((name) => RESULT_NAME.test(name))
|
|
128
|
+
.sort((one, other) => resultOrder(one).localeCompare(resultOrder(other)))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// El nombre que le toca a la corrida de hoy: el primero libre. Lo decide el motor y no el prompt del
|
|
132
|
+
// recorrido, porque «escribí en <fecha>.md» es una instrucción que no puede saber qué ya existe.
|
|
133
|
+
function nextResult(root, agent, date) {
|
|
134
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) throw new Error(`fecha inválida: ${date}`)
|
|
135
|
+
const dir = resultsDir(root, agent)
|
|
136
|
+
const taken = new Set(resultNames(dir))
|
|
137
|
+
if (!taken.has(`${date}.md`)) return `${date}.md`
|
|
138
|
+
let run = 2
|
|
139
|
+
while (taken.has(`${date}-${run}.md`)) run += 1
|
|
140
|
+
return `${date}-${run}.md`
|
|
141
|
+
}
|
|
142
|
+
|
|
110
143
|
// El último resultado registrado, para que `evaluate` pueda decir si el cargo se corrió alguna vez y
|
|
111
144
|
// cómo le fue. No es un error no tenerlo: correrlo cuesta, y exigirlo en CI sería exigir red.
|
|
112
145
|
function latest(root, agent) {
|
|
113
146
|
const dir = resultsDir(root, agent)
|
|
114
|
-
const names =
|
|
147
|
+
const names = resultNames(dir)
|
|
115
148
|
if (!names.length) return null
|
|
116
|
-
const
|
|
149
|
+
const name = names[names.length - 1]
|
|
150
|
+
const file = path.join(dir, name)
|
|
117
151
|
const text = fs.readFileSync(file, 'utf8')
|
|
118
152
|
const verdicts = [...text.matchAll(/^-\s*Veredicto:\s*(pasa|no pasa)\s*$/gim)].map((hit) => hit[1].toLowerCase())
|
|
153
|
+
const [, date, run] = name.match(RESULT_NAME)
|
|
119
154
|
return {
|
|
120
155
|
file,
|
|
121
|
-
|
|
156
|
+
// La fecha es la del día, sin el sufijo de corrida: quien lee «no pasaron en 2026-08-17» busca un
|
|
157
|
+
// día, no un nombre de archivo. Cuál de las corridas fue va aparte, y sólo cuando hubo más de una.
|
|
158
|
+
date,
|
|
159
|
+
run: Number(run || 1),
|
|
122
160
|
total: verdicts.length,
|
|
123
161
|
passed: verdicts.filter((verdict) => verdict === 'pasa').length,
|
|
124
162
|
}
|
|
@@ -152,4 +190,4 @@ function validate(root, agent) {
|
|
|
152
190
|
return { errors, warnings, cases: total, last }
|
|
153
191
|
}
|
|
154
192
|
|
|
155
|
-
module.exports = { behaviors, fixtures, list, latest, parseCase, validate, resultsDir }
|
|
193
|
+
module.exports = { behaviors, fixtures, list, latest, nextResult, parseCase, validate, resultsDir }
|
|
@@ -33,14 +33,30 @@ function assertWritable(root, agent) {
|
|
|
33
33
|
function isoDate(now = new Date()) { return now.toISOString().slice(0, 10) }
|
|
34
34
|
function month(now = new Date()) { return now.toISOString().slice(0, 7) }
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// Una propuesta por período, y sus revisiones. La revisión existe porque aplicar no es el final del
|
|
37
|
+
// ciclo: la evaluación posterior es la que dice si el cambio sirvió, y cuando dice que no, el sello
|
|
38
|
+
// —que está para que nadie reaplique lo mismo y duplique cada viñeta— dejaba al cargo con un contrato
|
|
39
|
+
// que se sabe mal calibrado y sin camino para corregirlo hasta el mes siguiente. La corrección es un
|
|
40
|
+
// cambio distinto: documento propio, firma propia, y la aplicada queda sellada donde está.
|
|
41
|
+
const PROPOSAL_NAME = /^(\d{4}-\d{2})(?:-r(\d+))?\.md$/
|
|
42
|
+
|
|
43
|
+
// Mismo cuidado que con los registros de evaluación: `-` va antes que `.` en ASCII, así que ordenar
|
|
44
|
+
// nombres pondría `2026-08-r2.md` delante de `2026-08.md` y la revisión se leería como la más vieja.
|
|
45
|
+
function proposalOrder(name) {
|
|
46
|
+
const [, period, revision] = name.match(PROPOSAL_NAME)
|
|
47
|
+
return `${period}-${String(Number(revision || 1)).padStart(4, '0')}`
|
|
48
|
+
}
|
|
37
49
|
|
|
38
50
|
// El tope de la línea de índice. No es estético: son 47 líneas que se leen de un vistazo, y una que
|
|
39
51
|
// se envuelve rompe la columna que hace posible el vistazo.
|
|
40
52
|
const SUMMARY_MAX = 120
|
|
41
53
|
|
|
42
54
|
function proposalFiles(dir) {
|
|
43
|
-
try {
|
|
55
|
+
try {
|
|
56
|
+
return fs.readdirSync(dir)
|
|
57
|
+
.filter((name) => PROPOSAL_NAME.test(name))
|
|
58
|
+
.sort((one, other) => proposalOrder(one).localeCompare(proposalOrder(other)))
|
|
59
|
+
} catch { return [] }
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
function proposalState(text) {
|
|
@@ -58,8 +74,14 @@ function seal(root, agent, period = '') {
|
|
|
58
74
|
const dir = path.join(assertWritable(root, agent), 'learning', 'proposals')
|
|
59
75
|
const names = proposalFiles(dir)
|
|
60
76
|
if (!names.length) throw new Error(`${agent} no tiene propuestas en learning/proposals/.`)
|
|
61
|
-
|
|
62
|
-
|
|
77
|
+
// `--period 2026-08` nombra el período, no un archivo: con revisiones abiertas la que se sella es la
|
|
78
|
+
// vigente de ese período. Resolverlo siempre a `2026-08.md` habría devuelto «ya estaba aplicada» y
|
|
79
|
+
// dejado la revisión sin sellar, que es justo el estado en que `agent-promote` la vuelve a aplicar.
|
|
80
|
+
// Una revisión concreta se puede nombrar entera —`--period 2026-08-r2`— y entonces manda esa.
|
|
81
|
+
const name = period
|
|
82
|
+
? (/^\d{4}-\d{2}$/.test(period) ? lastOfPeriod(dir, period) : `${period}.md`)
|
|
83
|
+
: names[names.length - 1]
|
|
84
|
+
if (!name || !names.includes(name)) throw new Error(`${agent} no tiene la propuesta ${period || name}.`)
|
|
63
85
|
const file = path.join(dir, name)
|
|
64
86
|
const text = fs.readFileSync(file, 'utf8')
|
|
65
87
|
const state = proposalState(text)
|
|
@@ -109,12 +131,88 @@ propuesta consolidada. -->
|
|
|
109
131
|
return { file, created: true }
|
|
110
132
|
}
|
|
111
133
|
|
|
134
|
+
// Abre la revisión siguiente. No vuelve a consolidar los informes semanales: ya se consolidaron en la
|
|
135
|
+
// propuesta que ésta corrige, y repetirlos haría que el mismo hallazgo entre dos veces al contrato. El
|
|
136
|
+
// insumo de una revisión es otro —qué mostró la evaluación posterior a aplicar—, y por eso el molde
|
|
137
|
+
// pregunta eso y no otra cosa.
|
|
138
|
+
function reviseProposal(root, agent, dir, previo, now) {
|
|
139
|
+
const period = month(now)
|
|
140
|
+
const anterior = previo.match(PROPOSAL_NAME)
|
|
141
|
+
const revision = Number(anterior[2] || 1) + 1
|
|
142
|
+
const file = path.join(dir, `${period}-r${revision}.md`)
|
|
143
|
+
fs.writeFileSync(file, `---
|
|
144
|
+
agent: ${agent}
|
|
145
|
+
period: ${period}
|
|
146
|
+
revision: ${revision}
|
|
147
|
+
corrects: ${previo}
|
|
148
|
+
status: proposed
|
|
149
|
+
automatic_apply: false
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
# Propuesta mensual — ${period}, revisión ${revision}
|
|
153
|
+
|
|
154
|
+
Corrige \`${previo}\`, que ya está aplicada y queda sellada donde está. Esta revisión no la reemplaza ni
|
|
155
|
+
la reabre: es un cambio distinto, con su propia firma.
|
|
156
|
+
|
|
157
|
+
## Hallazgos
|
|
158
|
+
|
|
159
|
+
Qué mostró la evaluación posterior a aplicar \`${previo}\`. No repitas acá los hallazgos de esa propuesta
|
|
160
|
+
—ya entraron al contrato—: lo que va es lo que se supo después, con el registro de evaluación que lo
|
|
161
|
+
sostiene.
|
|
162
|
+
|
|
163
|
+
## Evidencia
|
|
164
|
+
|
|
165
|
+
El registro de la corrida que lo destapó, y la cita del veredicto. Si el cambio anterior falló por estar
|
|
166
|
+
mal calibrado, va también la línea del contrato que quedó floja y la del caso que la contradice.
|
|
167
|
+
|
|
168
|
+
## Cambio propuesto
|
|
169
|
+
|
|
170
|
+
Una revisión suele **no** ser aditiva: reemplaza texto que la propuesta anterior agregó. Decilo
|
|
171
|
+
explícitamente y decí por qué la aditividad no aplica acá — vale para lo que ya rindió sus casos, no para
|
|
172
|
+
un texto que acaba de fallar su primera medición.
|
|
173
|
+
|
|
174
|
+
## Riesgos y regresiones
|
|
175
|
+
|
|
176
|
+
Qué casos pasaban con el texto anterior y podrían dejar de pasar con éste. Nombralos por su id: son los
|
|
177
|
+
que hay que volver a correr.
|
|
178
|
+
|
|
179
|
+
## Evaluación
|
|
180
|
+
|
|
181
|
+
Cómo se comprueba que esta vez sí. Nombrá el caso que tiene que cambiar de veredicto y por qué razón, no
|
|
182
|
+
sólo que pase.
|
|
183
|
+
|
|
184
|
+
## Aprobación humana
|
|
185
|
+
|
|
186
|
+
- Estado: pendiente
|
|
187
|
+
- Responsable: por definir
|
|
188
|
+
- Fecha: por definir
|
|
189
|
+
`)
|
|
190
|
+
return { file, created: true, reports: 0, corrects: previo }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// La última propuesta del período, si la hay: es contra ella que se decide si abrir una revisión.
|
|
194
|
+
function lastOfPeriod(dir, period) {
|
|
195
|
+
const names = proposalFiles(dir).filter((name) => name.match(PROPOSAL_NAME)[1] === period)
|
|
196
|
+
return names.length ? names[names.length - 1] : ''
|
|
197
|
+
}
|
|
198
|
+
|
|
112
199
|
function prepareProposal(root, agent, now = new Date()) {
|
|
113
200
|
const target = assertWritable(root, agent)
|
|
114
201
|
const proposalDir = path.join(target, 'learning', 'proposals')
|
|
115
|
-
const file = path.join(proposalDir, `${month(now)}.md`)
|
|
116
202
|
fs.mkdirSync(proposalDir, { recursive: true })
|
|
117
|
-
|
|
203
|
+
|
|
204
|
+
// Una sola propuesta pendiente por período. Si la última todavía no se aplicó, abrir otra partiría
|
|
205
|
+
// la firma en dos documentos que dicen cosas distintas sobre el mismo contrato.
|
|
206
|
+
const previo = lastOfPeriod(proposalDir, month(now))
|
|
207
|
+
if (previo) {
|
|
208
|
+
const anterior = path.join(proposalDir, previo)
|
|
209
|
+
if (proposalState(fs.readFileSync(anterior, 'utf8')) !== 'applied') {
|
|
210
|
+
return { file: anterior, created: false, reports: 0 }
|
|
211
|
+
}
|
|
212
|
+
return reviseProposal(root, agent, proposalDir, previo, now)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const file = path.join(proposalDir, `${month(now)}.md`)
|
|
118
216
|
let names = []
|
|
119
217
|
try { names = fs.readdirSync(path.join(target, 'learning', 'reports')) } catch { /* vacío */ }
|
|
120
218
|
const reports = names.filter((name) => name.startsWith(month(now)) && /^\d{4}-\d{2}-\d{2}\.md$/.test(name)).sort()
|
package/engine/cli/args.js
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
// requerido y no puede exportar nada sin dispararse.
|
|
6
6
|
|
|
7
7
|
// Banderas que consumen el argumento siguiente: su valor no es un posicional.
|
|
8
|
-
const VALUED_FLAGS = new Set([
|
|
8
|
+
const VALUED_FLAGS = new Set([
|
|
9
|
+
'--name', '--mode', '--fixture', '--period', '--record', '--runner', '--integration',
|
|
10
|
+
])
|
|
9
11
|
|
|
10
12
|
// Qué acepta cada comando, y a la vez qué comandos existen. Una bandera desconocida se rechaza en vez
|
|
11
13
|
// de ignorarse: `check --jsonn` imprimía la salida humana con código 0, así que quien esperaba JSON
|
|
12
14
|
// —un agente, típicamente— recibía texto sin ninguna señal de que su bandera no existía.
|
|
13
15
|
const FLAGS = {
|
|
14
|
-
init: ['--name', '--mode', '--force'],
|
|
16
|
+
init: ['--name', '--mode', '--force', '--runner', '--integration', '--install', '--no-install'],
|
|
15
17
|
check: ['--json'],
|
|
16
18
|
tree: ['--json', '--no-color'],
|
|
17
19
|
context: ['--json'],
|
|
@@ -21,7 +23,7 @@ const FLAGS = {
|
|
|
21
23
|
integration: ['--fixture'],
|
|
22
24
|
automation: ['--force'],
|
|
23
25
|
learn: ['--proposal', '--applied', '--period'],
|
|
24
|
-
evaluate: ['--cases', '--json', '--bench', '--force'],
|
|
26
|
+
evaluate: ['--cases', '--json', '--bench', '--force', '--record'],
|
|
25
27
|
team: ['--json'],
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Lo que `init` hace después de copiar el molde: dejar la instancia usable en la misma corrida, en vez
|
|
4
|
+
// de devolver una lista de pasos que el usuario tiene que ejecutar a mano. Vive fuera de `cli/ops.js`
|
|
5
|
+
// porque depende de tres cosas que una prueba no puede ejecutar —npm, un runner que escribe en el repo
|
|
6
|
+
// del usuario y una terminal que responde—, y recibirlas como argumento es lo que permite probar el
|
|
7
|
+
// recorrido completo sin ninguna de las tres.
|
|
8
|
+
|
|
9
|
+
const readline = require('node:readline/promises')
|
|
10
|
+
|
|
11
|
+
const SIN_RUNNER = 'ninguno'
|
|
12
|
+
const SIN_PROVEEDOR = 'ninguna'
|
|
13
|
+
|
|
14
|
+
// Cuántas veces se repregunta antes de tomar el default. Insistir para siempre cuelga una corrida no
|
|
15
|
+
// interactiva que igual llegó hasta acá; rendirse a la primera convierte un dedazo en una decisión.
|
|
16
|
+
const INTENTOS = 3
|
|
17
|
+
|
|
18
|
+
// Una terminal de verdad, aislada acá para que el resto del módulo no sepa que existe.
|
|
19
|
+
function terminal() {
|
|
20
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
21
|
+
return { ask: (pregunta) => rl.question(pregunta), close: () => rl.close() }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Un Ctrl+D o un Ctrl+C en mitad de la pregunta valen como «no elijo»: readline rechaza la promesa, y
|
|
25
|
+
// dejar que ese rechazo suba terminaba la corrida con «Aborted with Ctrl+D» y la instancia recién creada
|
|
26
|
+
// sin una línea que dijera cómo seguir. El default no hace nada, así que tomarlo no decide nada.
|
|
27
|
+
async function elegir(deps, texto, opciones, fallback) {
|
|
28
|
+
const listado = opciones.map((opcion, indice) => `${indice + 1}) ${opcion}`).join(' ')
|
|
29
|
+
for (let intento = 0; intento < INTENTOS; intento += 1) {
|
|
30
|
+
let dicho
|
|
31
|
+
try { dicho = await deps.ask(`\n${texto}\n${listado}\n[${fallback}] > `) } catch {
|
|
32
|
+
deps.log(`\n sin respuesta: sigo con ${fallback}.`)
|
|
33
|
+
return fallback
|
|
34
|
+
}
|
|
35
|
+
const respuesta = dicho.trim()
|
|
36
|
+
if (!respuesta) return fallback
|
|
37
|
+
const numero = Number(respuesta)
|
|
38
|
+
if (Number.isInteger(numero) && numero >= 1 && numero <= opciones.length) return opciones[numero - 1]
|
|
39
|
+
if (opciones.includes(respuesta)) return respuesta
|
|
40
|
+
deps.log(` «${respuesta}» no está en la lista.`)
|
|
41
|
+
}
|
|
42
|
+
return fallback
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// El default de las dos preguntas es no hacer nada, y es a propósito: instalar un runner escribe en el
|
|
46
|
+
// repositorio del usuario y habilitar un proveedor deja andamiaje que después hay que completar. Un
|
|
47
|
+
// Enter apurado no debería dejar archivos que nadie pidió.
|
|
48
|
+
//
|
|
49
|
+
// La terminal se abre acá y no en el CLI, y sólo si hay algo que preguntar: quien llama no tiene por
|
|
50
|
+
// qué saber que readline existe, y una prueba reemplaza `deps.ask` sin que se abra ninguna.
|
|
51
|
+
async function preguntas(opciones, deps) {
|
|
52
|
+
const falta = !opciones.runner || !opciones.integration
|
|
53
|
+
if (!opciones.interactive || !falta) {
|
|
54
|
+
return { runner: opciones.runner || SIN_RUNNER, proveedor: opciones.integration || SIN_PROVEEDOR }
|
|
55
|
+
}
|
|
56
|
+
const tty = deps.ask ? null : terminal()
|
|
57
|
+
const con = { ...deps, ask: deps.ask || tty.ask }
|
|
58
|
+
const runners = [...opciones.runners, SIN_RUNNER]
|
|
59
|
+
const proveedores = [...opciones.providers, SIN_PROVEEDOR]
|
|
60
|
+
try {
|
|
61
|
+
const runner = opciones.runner
|
|
62
|
+
|| await elegir(con, '¿Con qué runner vas a trabajar?', runners, SIN_RUNNER)
|
|
63
|
+
const proveedor = opciones.integration
|
|
64
|
+
|| await elegir(con, '¿Habilitar alguna integración?', proveedores, SIN_PROVEEDOR)
|
|
65
|
+
return { runner, proveedor }
|
|
66
|
+
} finally { if (tty) tty.close() }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validar(opciones) {
|
|
70
|
+
const runners = [...opciones.runners, SIN_RUNNER]
|
|
71
|
+
const proveedores = [...opciones.providers, SIN_PROVEEDOR]
|
|
72
|
+
if (opciones.runner && !runners.includes(opciones.runner)) {
|
|
73
|
+
throw new Error(`--runner debe ser ${runners.join(', ')}.`)
|
|
74
|
+
}
|
|
75
|
+
if (opciones.integration && !proveedores.includes(opciones.integration)) {
|
|
76
|
+
throw new Error(`--integration debe ser ${proveedores.join(', ')}.`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Deja la instancia lista o dice exactamente qué falta. El orden no es negociable: el proveedor se
|
|
81
|
+
// habilita con el motor que corre `init` —su plantilla viaja en el paquete—, pero el runner necesita
|
|
82
|
+
// la dependencia ya instalada, porque sus adaptadores y workflows se resuelven desde `node_modules`.
|
|
83
|
+
async function run(root, opciones, deps) {
|
|
84
|
+
validar(opciones)
|
|
85
|
+
const { runner, proveedor } = await preguntas(opciones, deps)
|
|
86
|
+
if (proveedor !== SIN_PROVEEDOR) deps.enableProvider(proveedor)
|
|
87
|
+
if (!opciones.install) return { runner, proveedor, instalado: false, pendiente: 'npm install' }
|
|
88
|
+
deps.log('\n· npm install (el motor viene de la dependencia)')
|
|
89
|
+
if (deps.npm(root) !== 0) {
|
|
90
|
+
return { runner, proveedor, instalado: false, error: 'npm install falló', pendiente: 'npm install' }
|
|
91
|
+
}
|
|
92
|
+
if (runner !== SIN_RUNNER) deps.installRunner(runner)
|
|
93
|
+
return { runner, proveedor, instalado: true }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { run, SIN_RUNNER, SIN_PROVEEDOR }
|
package/engine/cli/ops.js
CHANGED
|
@@ -19,9 +19,13 @@ const T = require('../teams/registry')
|
|
|
19
19
|
const AG = require('../agents/catalog')
|
|
20
20
|
const EV = require('../agents/evaluations')
|
|
21
21
|
const { FLAGS, parse } = require('./args')
|
|
22
|
+
const BOOT = require('./bootstrap')
|
|
22
23
|
|
|
23
24
|
const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
|
|
24
25
|
|
|
26
|
+
// Dónde aterriza una instancia cuando nadie eligió: una carpeta propia junto al código.
|
|
27
|
+
const DEFAULT_TARGET = 'ops'
|
|
28
|
+
|
|
25
29
|
function fail(message, code = 1) {
|
|
26
30
|
console.error(message)
|
|
27
31
|
process.exit(code)
|
|
@@ -29,7 +33,8 @@ function fail(message, code = 1) {
|
|
|
29
33
|
|
|
30
34
|
function usage() {
|
|
31
35
|
console.log(`Uso:
|
|
32
|
-
ops init
|
|
36
|
+
ops init [destino] [--name <nombre>] [--mode embedded|sidecar] [--force]
|
|
37
|
+
[--runner claude|codex|gemini|antigravity] [--integration <proveedor>] [--install|--no-install]
|
|
33
38
|
ops check <planning-dir> [--json]
|
|
34
39
|
ops tree <planning-dir> [--no-color] [--json]
|
|
35
40
|
ops context <planning-dir> [--json]
|
|
@@ -51,7 +56,7 @@ function usage() {
|
|
|
51
56
|
ops automation doctor <ops-root> claude|codex|gemini|antigravity
|
|
52
57
|
ops automation install <ops-root> claude|codex|gemini|antigravity
|
|
53
58
|
ops learn <agent> [--proposal] [--applied [--period <AAAA-MM>]]
|
|
54
|
-
ops evaluate <agent> [--cases [--json]] [--bench [caso]]
|
|
59
|
+
ops evaluate <agent> [--cases [--json]] [--bench [caso]] [--record [AAAA-MM-DD]]
|
|
55
60
|
ops agents list [ops-root] [--own|--system] [--json]
|
|
56
61
|
ops agents fork <cargo> [ops-root]
|
|
57
62
|
ops team list
|
|
@@ -142,12 +147,8 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
|
|
|
142
147
|
root,
|
|
143
148
|
)
|
|
144
149
|
const version = require(path.join(PROJECT_ROOT, 'package.json')).version
|
|
145
|
-
// El motor
|
|
146
|
-
//
|
|
147
|
-
// que la copia sólo ahorraba este `package.json` de seis líneas a cambio de 5 MB en la historia de
|
|
148
|
-
// la empresa y de no tener cómo enterarse de que salió una versión nueva.
|
|
149
|
-
//
|
|
150
|
-
// El repo ops es un sidecar: declarar npm acá no convierte en Node al servicio de Go de al lado.
|
|
150
|
+
// El motor llega como dependencia para que el lockfile fije su versión. El repo ops es un sidecar:
|
|
151
|
+
// declarar npm acá no convierte en Node al servicio de Go de al lado.
|
|
151
152
|
declareEngine(path.join(root, 'package.json'), version)
|
|
152
153
|
let entregado = {}
|
|
153
154
|
for (const relative of O.trackedPaths()) {
|
|
@@ -225,21 +226,90 @@ function evaluationBench(root, agent, caso, force) {
|
|
|
225
226
|
return dir
|
|
226
227
|
}
|
|
227
228
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
// El nombre sale de la carpeta del proyecto, no de la que aloja la instancia: `ops/` y `acme-ops/`
|
|
230
|
+
// nombran al toolkit, y quien lee `project` en la configuración espera leer «acme».
|
|
231
|
+
function defaultName(root) {
|
|
232
|
+
const base = path.basename(root)
|
|
233
|
+
return base === DEFAULT_TARGET ? path.basename(path.dirname(root)) : base.replace(/-ops$/, '')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// El motor no se instala solo: `npm install` baja el paquete que `init` acaba de declarar, y sin él el
|
|
237
|
+
// shim, los cargos, los equipos y los adaptadores no se resuelven. Correrlo desde acá es lo que hace que
|
|
238
|
+
// una instalación sea un comando y no una lista. En Windows el ejecutable es `npm.cmd`.
|
|
239
|
+
function npmInstall(root) {
|
|
240
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
241
|
+
const result = spawnSync(npm, ['install'], { cwd: root, stdio: 'inherit' })
|
|
242
|
+
if (result.error) {
|
|
243
|
+
console.error(` no pude ejecutar npm (${result.error.code || result.error.message}).`)
|
|
244
|
+
return 1
|
|
245
|
+
}
|
|
246
|
+
return result.status === null ? 1 : result.status
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Un `cli` que no tiene banderas, para reusar un comando desde otro: el `--force` de `init` habla del
|
|
250
|
+
// molde y no del wiring del runner, así que pasarle el suyo instalaría a la fuerza algo que nadie pidió.
|
|
251
|
+
const SIN_BANDERAS = { has: () => false, value: (_flag, fallback = '') => fallback }
|
|
252
|
+
|
|
253
|
+
// Lo que quedó pendiente, y sólo eso: cuando la instalación corrió, `check` ya se ejecutó y repetirlo
|
|
254
|
+
// como sugerencia hace dudar de que haya pasado.
|
|
255
|
+
function initSteps(enter, resultado) {
|
|
256
|
+
if (resultado.instalado) return []
|
|
257
|
+
const pasos = ['npm install']
|
|
258
|
+
if (resultado.runner !== BOOT.SIN_RUNNER) {
|
|
259
|
+
pasos.push(`node tools/ops.js automation install . ${resultado.runner}`)
|
|
260
|
+
}
|
|
261
|
+
pasos.push('node tools/ops.js check planning')
|
|
262
|
+
return pasos.map((paso, indice) => ` siguiente: ${indice === 0 ? enter : ''}${paso}`)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function init(target, cli) {
|
|
266
|
+
// Sin destino la instancia va a `ops/` y en modo sidecar, en vez de volcarse donde esté parado el
|
|
267
|
+
// dev: un monorepo que recibe `planning/`, `teams/`, `organization/` y `AGENTS.md` en su primer
|
|
268
|
+
// nivel deja de distinguir qué es suyo y qué llegó del toolkit. Es el layout que `automation
|
|
269
|
+
// install` ya asume —el wiring del runner va al padre, donde se abre la herramienta—, así que lo
|
|
270
|
+
// único que faltaba era que fuera lo que pasa cuando no se elige nada.
|
|
271
|
+
const root = path.resolve(target || DEFAULT_TARGET)
|
|
272
|
+
const mode = cli.value('--mode', target ? 'embedded' : 'sidecar')
|
|
231
273
|
if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
|
|
232
|
-
const name = cli.value('--name',
|
|
233
|
-
const root = path.resolve(target)
|
|
274
|
+
const name = cli.value('--name', defaultName(root))
|
|
234
275
|
const force = cli.has('--force')
|
|
235
276
|
const existing = fs.existsSync(root) ? fs.readdirSync(root) : []
|
|
236
277
|
if (existing.length && !force) {
|
|
237
278
|
fail(`El destino no está vacío: ${root}. Usa --force para agregar solo archivos faltantes.`)
|
|
238
279
|
}
|
|
239
280
|
scaffold(root, { name, mode, force })
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
console.log(
|
|
281
|
+
const relative = path.relative(process.cwd(), root)
|
|
282
|
+
const enter = relative && relative !== '.' ? `cd ${relative} && ` : ''
|
|
283
|
+
console.log(`\n✓ ${name}: sistema ops creado en ${root} (modo ${mode})`)
|
|
284
|
+
|
|
285
|
+
// Preguntar exige una terminal, e instalar baja un paquete y escribe `node_modules`: las dos cosas
|
|
286
|
+
// pasan cuando hay alguien mirando. Una corrida automatizada —CI, un contenedor, estas pruebas—
|
|
287
|
+
// recibe la instancia materializada y decide por bandera, sin descargas ni preguntas implícitas.
|
|
288
|
+
const interactivo = Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
289
|
+
const opciones = {
|
|
290
|
+
runner: cli.value('--runner'),
|
|
291
|
+
integration: cli.value('--integration'),
|
|
292
|
+
runners: A.RUNNER_NAMES,
|
|
293
|
+
providers: providerNames(),
|
|
294
|
+
interactive: interactivo,
|
|
295
|
+
install: cli.has('--install') || (interactivo && !cli.has('--no-install')),
|
|
296
|
+
}
|
|
297
|
+
let resultado
|
|
298
|
+
try {
|
|
299
|
+
resultado = await BOOT.run(root, opciones, {
|
|
300
|
+
log: console.log,
|
|
301
|
+
npm: npmInstall,
|
|
302
|
+
installRunner: (runner) => automation('install', root, runner, SIN_BANDERAS),
|
|
303
|
+
enableProvider: (provider) => INTEGRATION.enable.run(root, provider),
|
|
304
|
+
})
|
|
305
|
+
} catch (error) { fail(error.message, 2) }
|
|
306
|
+
|
|
307
|
+
if (resultado.instalado) {
|
|
308
|
+
check(path.join(root, 'planning'), SIN_BANDERAS)
|
|
309
|
+
console.log(` listo: el ciclo empieza en ${path.join(relative || '.', 'planning', 'FLOW.md')}`)
|
|
310
|
+
}
|
|
311
|
+
for (const paso of initSteps(enter, resultado)) console.log(paso)
|
|
312
|
+
if (resultado.error) fail(`${resultado.error}: la instancia quedó creada pero todavía no funciona.`)
|
|
243
313
|
}
|
|
244
314
|
|
|
245
315
|
function check(dir, cli) {
|
|
@@ -989,6 +1059,12 @@ function evaluate(agent, caso, cli) {
|
|
|
989
1059
|
// repetirla por caso invitaría a que alguien la editara en uno solo.
|
|
990
1060
|
return console.log(JSON.stringify({ cases, forbidden: prohibido }))
|
|
991
1061
|
}
|
|
1062
|
+
// Dónde escribir el registro de esta corrida. Lo pregunta el recorrido en vez de componer el
|
|
1063
|
+
// nombre, que es lo que hacía que la segunda corrida de un día borrara a la primera.
|
|
1064
|
+
if (cli.has('--record')) {
|
|
1065
|
+
const dia = cli.value('--record') || new Date().toISOString().slice(0, 10)
|
|
1066
|
+
return console.log(path.relative(root, path.join(EV.resultsDir(root, agent), EV.nextResult(root, agent, dia))))
|
|
1067
|
+
}
|
|
992
1068
|
const result = L.evaluate(root, agent)
|
|
993
1069
|
const runs = EV.validate(root, agent)
|
|
994
1070
|
const errors = [...result.errors, ...runs.errors]
|
|
@@ -1041,7 +1117,7 @@ async function run(cli) {
|
|
|
1041
1117
|
fail(`${command}: bandera desconocida ${sobran.join(', ')}. ${acepta}`, 2)
|
|
1042
1118
|
}
|
|
1043
1119
|
const arg = cli.positional
|
|
1044
|
-
if (command === 'init') init(arg[1], cli)
|
|
1120
|
+
if (command === 'init') await init(arg[1], cli)
|
|
1045
1121
|
else if (command === 'check') check(arg[1], cli)
|
|
1046
1122
|
else if (command === 'tree') tree(arg[1], cli)
|
|
1047
1123
|
else if (command === 'context') context(arg[1], cli)
|
package/engine/hooks/run.js
CHANGED
|
@@ -185,9 +185,14 @@ function workspaceBoundary(input) {
|
|
|
185
185
|
|
|
186
186
|
function migrations(input) {
|
|
187
187
|
if (process.env.OPS_MIGRATIONS_OVERRIDE === '1') return
|
|
188
|
+
// Cada rama cierra su propio límite. Cuando el `\b` estaba al final del grupo se aplicaba a las tres, y
|
|
189
|
+
// la de `delete` termina a propósito en `;`: después de un punto y coma no hay límite de palabra, así que
|
|
190
|
+
// `DELETE FROM pedidos;` —la forma que tiene en cualquier migración— pasaba y sólo frenaba la variante sin
|
|
191
|
+
// punto y coma. `drop column` y `drop constraint` faltaban: pierden datos y garantías igual que `drop table`.
|
|
188
192
|
const destructiveSql = new RegExp(
|
|
189
|
-
String.raw`\
|
|
190
|
-
String.raw
|
|
193
|
+
String.raw`\bdrop\s+(?:table|database|schema|column|constraint)\b` +
|
|
194
|
+
String.raw`|\btruncate\b` +
|
|
195
|
+
String.raw`|\bdelete\s+from\s+\S+\s*(?:;|$)`,
|
|
191
196
|
'i',
|
|
192
197
|
)
|
|
193
198
|
if (destructiveSql.test(contentOf(input))) {
|