@ingeniomaps/cauce 0.94.0 → 0.95.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 +196 -0
- package/automatization/workflows/autobuild.js +28 -1
- package/engine/agents/evaluations.js +20 -5
- package/engine/agents/fork.js +21 -8
- package/engine/agents/learning-sources.js +7 -7
- package/engine/agents/learning.js +15 -6
- package/engine/automation/check.js +2 -2
- package/engine/automation/config.js +9 -9
- package/engine/automation/index.js +19 -8
- package/engine/cli/archive.js +1 -4
- package/engine/cli/args.js +16 -2
- package/engine/cli/bench.js +3 -3
- package/engine/cli/catalog.js +3 -3
- package/engine/cli/claims.js +21 -23
- package/engine/cli/contract.js +24 -7
- package/engine/cli/instance.js +19 -19
- package/engine/cli/ops.js +7 -3
- package/engine/cli/planning.js +4 -4
- package/engine/cli/upgrade-report.js +10 -10
- package/engine/cli/validate.js +7 -7
- package/engine/core/onboarding.js +4 -1
- package/engine/core/ownership.js +2 -2
- package/engine/core/repos.js +18 -12
- package/engine/hooks/approval.js +9 -1
- package/engine/hooks/chat.js +27 -5
- package/engine/hooks/files.js +6 -6
- package/engine/hooks/input.js +5 -2
- package/engine/hooks/run.js +3 -3
- package/engine/hooks/shell.js +20 -6
- package/engine/hooks/verify.js +7 -7
- package/engine/integrations/proposals.js +5 -1
- package/engine/integrations/registry.js +7 -0
- package/engine/integrations/state.js +6 -3
- package/engine/planning/adoption.js +2 -2
- package/engine/planning/claims.js +8 -8
- package/engine/planning/parser.js +8 -8
- package/engine/planning/state.js +2 -2
- package/engine/planning/structure.js +7 -7
- package/package.json +1 -1
package/engine/hooks/input.js
CHANGED
|
@@ -157,7 +157,10 @@ function gitDirectory(command, cwd) {
|
|
|
157
157
|
// de correr: gobernanza, dependencias y generados. Cualquier variable delante alcanza, y la ironía es
|
|
158
158
|
// que el prefijo que el procedimiento manda escribir para un commit de gobernanza es
|
|
159
159
|
// `OPS_GOVERNANCE_OVERRIDE=1`: escrito ahí, el guard no lee el override, directamente no se ejecuta.
|
|
160
|
-
|
|
160
|
+
// El ancla admite el salto de línea y el `(` por lo mismo que admite `;`: los tres abren un comando. El
|
|
161
|
+
// salto es el que más caro salía —dos líneas en un solo Bash es como se escriben dos pasos— y el léxico
|
|
162
|
+
// de `shell.js` ya lo declaraba separador; acá faltaba (caso 165).
|
|
163
|
+
const PREFIX = String.raw`(?:^|[;&|(\n]\s*)(?:(?:env|sudo)\s+)*`
|
|
161
164
|
+ String.raw`(?:[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S*)\s+)*`
|
|
162
165
|
const COMMIT = new RegExp(PREFIX + String.raw`git\s+commit(?:\s|$)`)
|
|
163
166
|
|
|
@@ -230,7 +233,7 @@ function findOpsRoot(start) {
|
|
|
230
233
|
// Sin raíz legible no hay lista, y quien pregunta se abstiene: el guard que no sabe dónde está no
|
|
231
234
|
// inventa un límite.
|
|
232
235
|
function writableRoots(input) {
|
|
233
|
-
const root =
|
|
236
|
+
const root = opsRoot(input)
|
|
234
237
|
if (!root) return null
|
|
235
238
|
const config = configOf(root)
|
|
236
239
|
return [
|
package/engine/hooks/run.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
const fs = require('node:fs')
|
|
10
10
|
const os = require('node:os')
|
|
11
11
|
const path = require('node:path')
|
|
12
|
-
const { readInput,
|
|
12
|
+
const { readInput, block, opsRoot } = require('./input')
|
|
13
13
|
const shell = require('./shell')
|
|
14
14
|
const { verify } = require('./verify')
|
|
15
15
|
const files = require('./files')
|
|
@@ -18,7 +18,7 @@ const { secretsShell } = require('./secrets-shell')
|
|
|
18
18
|
const { opsConfig, opsConfigShell } = require('./ops-config')
|
|
19
19
|
|
|
20
20
|
function planningDrift(input) {
|
|
21
|
-
const root =
|
|
21
|
+
const root = opsRoot(input)
|
|
22
22
|
if (!root) return
|
|
23
23
|
const local = path.join(root, 'tools', 'ops.js')
|
|
24
24
|
const source = path.join(root, 'engine', 'cli', 'ops.js')
|
|
@@ -202,5 +202,5 @@ if (require.main === module) {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
module.exports = {
|
|
205
|
-
execute, executeAll,
|
|
205
|
+
execute, executeAll, guards, hookGroups, hookMetadata,
|
|
206
206
|
}
|
package/engine/hooks/shell.js
CHANGED
|
@@ -120,7 +120,13 @@ function destructive(input) {
|
|
|
120
120
|
'Operación destructiva sobre disco o dispositivo.',
|
|
121
121
|
],
|
|
122
122
|
[
|
|
123
|
-
|
|
123
|
+
// El destino se reconoce como lo escribe una persona y no sólo desnudo: entre comillas, con llaves,
|
|
124
|
+
// detrás de `--` y con barra final. Las comillas importan más que las otras tres — `rm -rf "$HOME"`
|
|
125
|
+
// es la forma *correcta* de escribirlo en bash, así que sin esto el hueco premiaba al que cita bien
|
|
126
|
+
// sus variables. Y `destructive` no desentrecomilla fuera de un commit, a propósito, así que la
|
|
127
|
+
// comilla tiene que entrar en el patrón (caso 167).
|
|
128
|
+
new RegExp(String.raw`\brm\s+(?:-[^\s]*r[^\s]*\s+)+(?:--\s+)?['"]?`
|
|
129
|
+
+ String.raw`(?:\/\*?|~\/?|\$\{?HOME\}?\/?|\.\.)(?=${PALABRA})`),
|
|
124
130
|
"'rm -r' sobre /, home o el directorio padre es catastrófico.",
|
|
125
131
|
],
|
|
126
132
|
]
|
|
@@ -252,7 +258,12 @@ function dependencies(input) {
|
|
|
252
258
|
// era la causa —sólo hacía que la lectura frenara en un token que sobrevive—: sin él la lista cruzaba
|
|
253
259
|
// igual y el último token era la marca de lo entrecomillado, que el filtro final descarta. O sea que
|
|
254
260
|
// pasaba de casualidad, y aserciar que pasa no fijaba nada.
|
|
255
|
-
|
|
261
|
+
// Y el `|` después de `>` es el override de `noclobber`, no una tubería: `>| destino` escribe igual que
|
|
262
|
+
// `> destino` aunque el shell esté configurado para no pisar. Sin admitirlo acá el guard no veía ese
|
|
263
|
+
// destino —el `|` está fuera de la clase que captura la ruta, así que la coincidencia moría—, y con él
|
|
264
|
+
// se perdían los tres guards que salen de `writesWithBase` a la vez. Lo que un agente se escribía por
|
|
265
|
+
// ese hueco era su propia `.ops-approval` (caso 164).
|
|
266
|
+
const REDIRECT = /(?:^|[\s(])&?\d*>>?\|?\s*(?![&(])([^\s;|&<>()]+)/g
|
|
256
267
|
const EVERY_ARG = /(?:^|[\s;|&(])(tee|truncate)\s+([^;|&<>()\n]+)/g
|
|
257
268
|
const LAST_ARG = /(?:^|[\s;|&(])(cp|mv|install|rsync)\s+([^;|&<>()\n]+)/g
|
|
258
269
|
const SED = /(?:^|[\s;|&(])sed\s+([^;|&<>()\n]+)/g
|
|
@@ -315,7 +326,10 @@ function cdTarget(argument, base) {
|
|
|
315
326
|
function writesWithBase(command, cwd) {
|
|
316
327
|
const found = []
|
|
317
328
|
let base = cwd
|
|
318
|
-
|
|
329
|
+
// El `|` que sigue a un `>` no parte nada: es el override de `noclobber`, no una tubería. Partir ahí
|
|
330
|
+
// separaba la redirección de su destino —`echo x >| ruta` quedaba como `echo x >` y ` ruta`— y el
|
|
331
|
+
// destino no lo veía nadie, que es por donde se colaba escribir la propia `.ops-approval` (caso 164).
|
|
332
|
+
for (const segment of unquoted(command).split(/[;&\n]+|(?<!>)\|+/)) {
|
|
319
333
|
const cd = segment.match(/^\s*cd(?:\s+(\S+))?\s*$/)
|
|
320
334
|
if (cd) { base = base === null ? null : cdTarget(cd[1], base); continue }
|
|
321
335
|
for (const raw of writeTargets(segment)) found.push({ raw, base })
|
|
@@ -380,9 +394,9 @@ function governance(input) {
|
|
|
380
394
|
// La aprobación vale para lo que nombra y para nada más: lo que quede sin cubrir es lo que se
|
|
381
395
|
// reporta. Así una aprobación vieja no autoriza el archivo que se sumó después, que es la diferencia
|
|
382
396
|
// entre una llave por operación y una puerta que quedó abierta.
|
|
383
|
-
const
|
|
384
|
-
if (!
|
|
385
|
-
block(`El commit toca gobernanza protegida.\n${AP.HOW('OPS_GOVERNANCE_OVERRIDE',
|
|
397
|
+
const pending = AP.pendingNow(opsRoot(input), governed, input)
|
|
398
|
+
if (!pending.length) return
|
|
399
|
+
block(`El commit toca gobernanza protegida.\n${AP.HOW('OPS_GOVERNANCE_OVERRIDE', pending, input)}`)
|
|
386
400
|
}
|
|
387
401
|
|
|
388
402
|
function run(program, args, cwd, extra = {}) {
|
package/engine/hooks/verify.js
CHANGED
|
@@ -216,15 +216,15 @@ function fallo(gate, result) {
|
|
|
216
216
|
// Un gate que vuelve en menos de esto no corrió una suite. No se afirma que **no** haya corrido —un
|
|
217
217
|
// lint puede fallar rápido y de verdad— y por eso lo que se agrega es el número, no un veredicto: los
|
|
218
218
|
// tres gates del caso 068 volvieron a un segundo uno de otro contra los trece de la corrida real.
|
|
219
|
-
const
|
|
220
|
-
function
|
|
221
|
-
const
|
|
219
|
+
const TOO_FAST = 2000
|
|
220
|
+
function howItReads(failures) {
|
|
221
|
+
const summary = failures
|
|
222
222
|
.map((one) => `${one.gate} (exit ${one.status}, ${(one.ms / 1000).toFixed(1)} s)`
|
|
223
223
|
+ `${one.line ? `: ${one.line}` : ''}`)
|
|
224
224
|
.join('; ')
|
|
225
|
-
if (!failures.every((one) => one.ms <
|
|
226
|
-
const
|
|
227
|
-
return `${
|
|
225
|
+
if (!failures.every((one) => one.ms < TOO_FAST)) return summary
|
|
226
|
+
const lead = failures.length === 1 ? 'Volvió' : `Los ${failures.length} volvieron`
|
|
227
|
+
return `${summary}\n${lead} en menos de ${TOO_FAST / 1000} s: eso no alcanza para correr `
|
|
228
228
|
+ 'una suite, así que mirá si llegaron a ejecutarse antes de aprobar esto como un rojo conocido.'
|
|
229
229
|
}
|
|
230
230
|
|
|
@@ -268,7 +268,7 @@ function verifyGates(root, dir, sinAprobar, env, input) {
|
|
|
268
268
|
// comando a mano se lee como que el guard miente, y lo que pasó es que midió lo que se va a grabar.
|
|
269
269
|
const donde = root === dir ? '' : '\nCorrió sobre el índice, que es lo que el commit graba: si en tu '
|
|
270
270
|
+ 'directorio pasa, es que en disco tenés algo que no está staged.'
|
|
271
|
-
block(`Verify falló en ${path.basename(dir)}: ${
|
|
271
|
+
block(`Verify falló en ${path.basename(dir)}: ${howItReads(failures)}\nNo se commitea en rojo.${donde}\n`
|
|
272
272
|
+ AP.HOW('OPS_SKIP_VERIFY', sinAprobar, input))
|
|
273
273
|
}
|
|
274
274
|
|
|
@@ -40,7 +40,11 @@ function validate(root, provider, workspaces = []) {
|
|
|
40
40
|
let maxHours = 4
|
|
41
41
|
try {
|
|
42
42
|
const config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
|
|
43
|
-
maxHours
|
|
43
|
+
// `|| maxHours` y no a secas: un `runner` sin `maxTaskHours` no lanza, así que el `catch` no lo ve
|
|
44
|
+
// y el tope quedaba en `undefined`. `estimate > undefined` es falso siempre, o sea que el límite
|
|
45
|
+
// desaparecía entero y en silencio — el modo de fallo que menos se nota, porque lo que deja de
|
|
46
|
+
// pasar es un rechazo.
|
|
47
|
+
maxHours = config.runner.maxTaskHours || maxHours
|
|
44
48
|
} catch { /* reported by the main validator */ }
|
|
45
49
|
for (const proposal of read(root, provider)) {
|
|
46
50
|
const at = `integrations/${provider}/proposed/${proposal.file}`
|
|
@@ -197,6 +197,13 @@ function validate(root, onlyProvider = '') {
|
|
|
197
197
|
if (fields.promotionKind === 'story' && (!/^\d{3}$/.test(fields.promotionEpic) || !fields.promotionCriteria)) {
|
|
198
198
|
errors.push(`${at}: story exige promotionEpic NNN y promotionCriteria`)
|
|
199
199
|
}
|
|
200
|
+
// Para una épica el campo es opcional —sin él el número lo elige el motor—, y por eso nadie lo
|
|
201
|
+
// miraba: el único caso sin validar era justo el que arma el nombre del archivo con lo que diga.
|
|
202
|
+
// `promotionEpic: 7` producía `epic-7-…`, que el roadmap no reconoce como épica, y un `..` sacaba
|
|
203
|
+
// la épica de `roadmap/` entero con `promote` devolviendo 0 y anunciando que promovió.
|
|
204
|
+
if (fields.promotionKind === 'epic' && fields.promotionEpic && !/^\d{3}$/.test(fields.promotionEpic)) {
|
|
205
|
+
errors.push(`${at}: promotionEpic debe ser NNN`)
|
|
206
|
+
}
|
|
200
207
|
if (!sections.Aceptación || /^(por definir|n\/a|pendiente)\.?$/i.test(sections.Aceptación)) {
|
|
201
208
|
errors.push(`${at}: ready exige aceptación concreta`)
|
|
202
209
|
}
|
|
@@ -107,9 +107,12 @@ function derive(snapshot, draft) {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
function renderDraft(item, config, state = 'pending') {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
// `|| []` porque el item lo arma el adaptador y el README declara el contrato por sus tres funciones,
|
|
111
|
+
// sin decir qué campos trae un item. Uno propio —que es lo que ese README invita a escribir— puede no
|
|
112
|
+
// traer `components`, y con `serviceFrom: "component"`, que es lo que trae el molde, eso era un
|
|
113
|
+
// TypeError sin nada que lo atribuya. Sin campo la respuesta es la que el borrador ya sabe dar.
|
|
114
|
+
const components = item.components || []
|
|
115
|
+
const service = config.serviceFrom === 'component' && components.length === 1 ? components[0] : ''
|
|
113
116
|
const acceptance = item.acceptance || 'Por definir.'
|
|
114
117
|
const issues = []
|
|
115
118
|
if (!item.acceptance) issues.push('La incidencia no contiene aceptación concreta.')
|
|
@@ -74,9 +74,9 @@ function sealWarnings(dir) {
|
|
|
74
74
|
if (extra > 0) {
|
|
75
75
|
// Cuáles sobran no se puede decir sin el conjunto original, y la cuenta sí: se nombran los últimos,
|
|
76
76
|
// que es donde se agrega a mano. Decir «alguna de éstas» es más honesto que señalar la equivocada.
|
|
77
|
-
const
|
|
77
|
+
const lastOnes = slugs.slice(-extra).join(', ')
|
|
78
78
|
return [`${BASELINE}: la lista creció. La huella sella ${seal[1]} entrada(s) y hay ${slugs.length}; `
|
|
79
|
-
+ `sobra(n) ${extra}, probablemente ${
|
|
79
|
+
+ `sobra(n) ${extra}, probablemente ${lastOnes}. Una entrada agregada a mano queda exenta para `
|
|
80
80
|
+ 'siempre y nada más lo dice.']
|
|
81
81
|
}
|
|
82
82
|
return [`${BASELINE}: la huella no coincide con la lista. Sella ${seal[1]} entrada(s) y hay `
|
|
@@ -109,7 +109,7 @@ function validate({ claims, milestones, done }) {
|
|
|
109
109
|
return errors
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
const
|
|
112
|
+
const daysBetween = (from, to) => Math.round((Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`))
|
|
113
113
|
/ 86400000)
|
|
114
114
|
|
|
115
115
|
// `activity` mapea el slug de una tarea a la fecha del último commit de su rama. Llega de afuera porque
|
|
@@ -125,14 +125,14 @@ function warnings({ claims, done, today, activity = new Map() }) {
|
|
|
125
125
|
}
|
|
126
126
|
if (!DATE.test(claim.started)) continue
|
|
127
127
|
const commit = activity.get(claim.slug) || ''
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
const
|
|
132
|
-
? `último commit hace ${
|
|
128
|
+
const lastSeen = commit && commit > claim.started ? commit : claim.started
|
|
129
|
+
const idleDays = daysBetween(lastSeen, today)
|
|
130
|
+
if (idleDays > STALE_DAYS) {
|
|
131
|
+
const signal = commit
|
|
132
|
+
? `último commit hace ${daysBetween(commit, today)} días`
|
|
133
133
|
: 'la rama de la tarea no tiene commits'
|
|
134
|
-
lines.push(`${claim.at}: ${claim.slug} sin avanzar hace ${
|
|
135
|
-
+ `(${claim.owner}, tomada hace ${
|
|
134
|
+
lines.push(`${claim.at}: ${claim.slug} sin avanzar hace ${idleDays} días `
|
|
135
|
+
+ `(${claim.owner}, tomada hace ${daysBetween(claim.started, today)}; ${signal}); mirá si sigue viva`)
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
// Dos tareas del mismo servicio pueden tocar los mismos archivos, y eso no se puede saber antes de
|
|
@@ -315,13 +315,13 @@ const HUMAN_ACTION_STATES = ['pendiente', 'resuelta']
|
|
|
315
315
|
// Lo que no cubre: una celda que termine en una barra invertida literal. En markdown eso se escribe
|
|
316
316
|
// `\\` y acá se leería como escape del separador. Es un borde que nadie escribe y taparlo pedía un
|
|
317
317
|
// parser de verdad; queda dicho en vez de supuesto.
|
|
318
|
-
const
|
|
319
|
-
const
|
|
318
|
+
const CELL_SPLIT = /(?<!\\)\|/
|
|
319
|
+
const SEPARATORS = /^\|\s*:?-+/
|
|
320
320
|
|
|
321
321
|
// El escape se quita al normalizar. Esta columna existe para que una persona lea qué tiene que hacer, y
|
|
322
322
|
// `\|` no es parte de lo que quiso decir: es cómo markdown escribe un pipe. La fila archivada no se ve
|
|
323
323
|
// afectada —`archive` reescribe `raw`, la línea original, no las celdas—, así que quitarlo no pierde nada.
|
|
324
|
-
const
|
|
324
|
+
const cellOf = (cell) => cell.trim().replace(/\\\|/g, '|')
|
|
325
325
|
|
|
326
326
|
// Las filas de datos de las tablas de un texto, con las celdas ya normalizadas. Vive acá y no en cada
|
|
327
327
|
// lector porque el escape del pipe tiene que leerse igual en los dos archivos que traen tabla: escrito
|
|
@@ -332,11 +332,11 @@ const celda = (cell) => cell.trim().replace(/\\\|/g, '|')
|
|
|
332
332
|
// y con `findIndex` la segunda y la tercera vuelven a leerse como datos. Nada más se mueve, porque una
|
|
333
333
|
// fila de datos nunca está inmediatamente antes de los guiones.
|
|
334
334
|
function tableRows(text) {
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
return
|
|
338
|
-
.filter((line, i) => /^\|/.test(line) && !
|
|
339
|
-
.map((line) => ({ line, cells: line.split(
|
|
335
|
+
const lines = text.split('\n')
|
|
336
|
+
const headers = new Set(lines.map((line, i) => (SEPARATORS.test(line) ? i - 1 : -1)))
|
|
337
|
+
return lines
|
|
338
|
+
.filter((line, i) => /^\|/.test(line) && !SEPARATORS.test(line) && !headers.has(i))
|
|
339
|
+
.map((line) => ({ line, cells: line.split(CELL_SPLIT).slice(1, -1).map(cellOf) }))
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
function readHumanActions(dir) {
|
package/engine/planning/state.js
CHANGED
|
@@ -86,8 +86,8 @@ function currentTask({ milestones, done, wips = [], claims = [] }, blockers = []
|
|
|
86
86
|
// estaría lista y no acá—, así que buscarla no puede fallar y no lleva defensa.
|
|
87
87
|
waiting: pending.filter((task) => !others.has(task.slug) && !ready(task)).map((task) => {
|
|
88
88
|
const dep = task.depends.find((one) => !done.set.has(one))
|
|
89
|
-
const
|
|
90
|
-
return { slug: task.slug, dep, owner:
|
|
89
|
+
const other = others.get(dep)
|
|
90
|
+
return { slug: task.slug, dep, owner: other ? other.owner : (mine.has(dep) ? 'vos' : '') }
|
|
91
91
|
}),
|
|
92
92
|
}
|
|
93
93
|
}
|
|
@@ -98,23 +98,23 @@ function ruleIds(file) {
|
|
|
98
98
|
// escribir, y ahí el aviso es lo único que lo dice.
|
|
99
99
|
function competingSections(dir) {
|
|
100
100
|
const roadmap = path.join(dir, 'roadmap')
|
|
101
|
-
const
|
|
101
|
+
const warnings = []
|
|
102
102
|
let files = []
|
|
103
103
|
try { files = fs.readdirSync(roadmap).filter((file) => /^epic-\d{3}-/.test(file)) } catch { return [] }
|
|
104
104
|
for (const file of files.sort()) {
|
|
105
105
|
const text = P.read(path.join(roadmap, file))
|
|
106
106
|
const titles = [...text.matchAll(/^##\s+(.+)$/gm)].map((hit) => hit[1].trim())
|
|
107
107
|
for (const role of [/Criterios/i, /Historias/i]) {
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
108
|
+
const matching = titles.filter((title) => role.test(title))
|
|
109
|
+
if (matching.length < 2) continue
|
|
110
110
|
const exact = new RegExp(`^${role.source}$`, role.flags)
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
111
|
+
const winner = matching.find((title) => exact.test(title)) || matching[0]
|
|
112
|
+
const ignored = matching.filter((title) => title !== winner)
|
|
113
|
+
warnings.push(`roadmap/${file}: "## ${winner}" convive con "## ${ignored.join('", "## ')}"; `
|
|
114
114
|
+ 'sólo se lee la primera y el resto se ignora entero')
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
return
|
|
117
|
+
return warnings
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
function retiredByOverride(dir, name) {
|