openprompt-lang 1.4.0 → 1.5.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/package.json +1 -1
- package/src/boost/validation-loop.js +8 -1
- package/src/commands/validate.js +54 -12
- package/src/utils/annotations.js +18 -6
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @use(kind, contract, limit, error)
|
|
1
|
+
// @use(kind, contract, limit, error, learn-error)
|
|
2
2
|
// @kind(util)
|
|
3
3
|
// @contract(in: code, kind, profile -> out: feedbackLoop -> { code, history, report } @error: ValidationFailedError, EscalationFailedError)
|
|
4
4
|
// @limit(lines: 300)
|
|
@@ -78,12 +78,19 @@ export function validateCode(code, kind) {
|
|
|
78
78
|
writeFileSync(tempFile, code, "utf-8")
|
|
79
79
|
|
|
80
80
|
// Ejecutar validación
|
|
81
|
+
// @learn-error id=BOOST-VALIDATE-001
|
|
82
|
+
// input='validation-loop.js llamaba npx openprompt-lang validate sin guarda de recursión'
|
|
83
|
+
// expected='Si el pipeline ejecuta validate indirectamente, no debe crear fork bomb'
|
|
84
|
+
// actual='validate podía llamarse a sí mismo recursivamente si Boost estaba activo y el pipeline contenía scripts que llevaran a validate'
|
|
85
|
+
// fix='Agregar OPL_VALIDATE_RECURSION_GUARD al entorno del proceso hijo para que validate.js sepa que ya está en contexto anidado'
|
|
86
|
+
// category=recursion
|
|
81
87
|
let output
|
|
82
88
|
try {
|
|
83
89
|
output = execSync(`${VALIDATION_CMD} ${tempFile} 2>&1`, {
|
|
84
90
|
encoding: "utf-8",
|
|
85
91
|
timeout: 10000,
|
|
86
92
|
cwd: process.cwd(),
|
|
93
|
+
env: { ...process.env, OPL_VALIDATE_RECURSION_GUARD: "1" },
|
|
87
94
|
})
|
|
88
95
|
} catch (execErr) {
|
|
89
96
|
// validate devuelve código de error si hay problemas
|
package/src/commands/validate.js
CHANGED
|
@@ -224,18 +224,32 @@ export async function validate(options) {
|
|
|
224
224
|
annotationErrors++
|
|
225
225
|
console.log(chalk.red(` ❌ ${relPath}: ${err}`))
|
|
226
226
|
}
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
227
|
+
// Clasificar warnings según tipo:
|
|
228
|
+
// - [DEBT-reason: ...] → deuda documentada con justificación (info, incluso en strict)
|
|
229
|
+
// - @limit(lines) sin reason → deuda genérica (info, no error en modo friendly)
|
|
230
|
+
// - otros warnings → advertencia normal
|
|
231
|
+
for (const warn of warnings) {
|
|
232
|
+
const debtReasonMatch = warn.match(/^\[DEBT-reason:\s*(.+?)\]\s*(.*)/)
|
|
233
|
+
if (debtReasonMatch) {
|
|
234
|
+
// Deuda documentada CON justificación explícita
|
|
235
|
+
// Siempre se muestra como info, incluso en strict mode
|
|
236
|
+
const reason = debtReasonMatch[1]
|
|
237
|
+
const detail = debtReasonMatch[2]
|
|
238
|
+
annotationDebt++
|
|
239
|
+
console.log(chalk.dim(` 📋 ${relPath}: ${detail}`))
|
|
240
|
+
console.log(chalk.dim(` 🔍 Razón: ${reason}`))
|
|
241
|
+
} else if (warn.includes("@limit(lines)") && !strictMode) {
|
|
242
|
+
// Deuda genérica sin razón (solo en modo friendly)
|
|
243
|
+
annotationDebt++
|
|
244
|
+
console.log(chalk.dim(` 📋 ${relPath}: ${warn}`))
|
|
245
|
+
} else if (!strictMode) {
|
|
246
|
+
// Advertencia normal (solo en modo friendly)
|
|
247
|
+
annotationWarnings++
|
|
248
|
+
console.log(chalk.yellow(` ⚠️ ${relPath}: ${warn}`))
|
|
238
249
|
}
|
|
250
|
+
// En strict mode: los warnings sin [DEBT-reason] ya se mostraron como [strict] errors
|
|
251
|
+
// gracias al lintFile(strictMode) que los promociona — evitamos duplicado.
|
|
252
|
+
// Los [DEBT-reason] siempre pasan porque son deuda documentada.
|
|
239
253
|
}
|
|
240
254
|
}
|
|
241
255
|
} catch {
|
|
@@ -282,7 +296,21 @@ export async function validate(options) {
|
|
|
282
296
|
// ── Execute Phase ───────────────────────────────────────────────────────
|
|
283
297
|
|
|
284
298
|
// [4/5] Pipeline scripts with auto-healing
|
|
299
|
+
// @learn-error id=VALIDATE-001
|
|
300
|
+
// input='El pipeline pre-commit incluía validate:strict que ejecuta npx openprompt-lang validate --strict'
|
|
301
|
+
// expected='Los pasos del pipeline deben ser herramientas externas, no el mismo validate'
|
|
302
|
+
// actual='validate.js se llamaba a sí mismo recursivamente vía execSync, creando un fork bomb'
|
|
303
|
+
// fix='Eliminar validate:strict de los pasos del pipeline y agregar guarda de recursión OPL_VALIDATE_RECURSION_GUARD'
|
|
304
|
+
// category=recursion
|
|
285
305
|
pipeline.use(async (ctx) => {
|
|
306
|
+
// ── RECURSION GUARD: evitar que validate se llame a sí mismo ──
|
|
307
|
+
// Si este proceso ya es hijo de otro validate (detectado por env var),
|
|
308
|
+
// saltamos el paso [4/5] para romper la cadena de recursión.
|
|
309
|
+
if (process.env.OPL_VALIDATE_RECURSION_GUARD) {
|
|
310
|
+
console.log(chalk.dim(" ↪ Guard recursivo activo: saltando pipeline interno para evitar fork bomb"))
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
286
314
|
console.log(chalk.yellow("\n[4/5] Ejecutando pipeline..."))
|
|
287
315
|
const scripts = config.pipeline?.scripts || {}
|
|
288
316
|
const pipelineSteps = config.pipeline?.["pre-commit"] || []
|
|
@@ -301,6 +329,15 @@ export async function validate(options) {
|
|
|
301
329
|
continue
|
|
302
330
|
}
|
|
303
331
|
|
|
332
|
+
// ── SELF-REFERENCE GUARD: evitar que validate se llame a sí mismo ──
|
|
333
|
+
// Detecta si el script contiene "openprompt-lang validate" (con cualquier flag)
|
|
334
|
+
// y previene la ejecución recursiva.
|
|
335
|
+
if (/openprompt-lang\s+validate/.test(scriptCmd)) {
|
|
336
|
+
console.log(chalk.yellow(` ⚠️ "${step}" → "${scriptCmd}" es auto-referencia. Saltando para evitar recursión.`))
|
|
337
|
+
console.log(chalk.dim(" ℹ️ El validate externo ya ejecutó la validación. No necesita re-validarse."))
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
|
|
304
341
|
let success = false
|
|
305
342
|
let lastError = ""
|
|
306
343
|
|
|
@@ -325,7 +362,12 @@ export async function validate(options) {
|
|
|
325
362
|
|
|
326
363
|
try {
|
|
327
364
|
process.stdout.write(` ▶️ ${step}... `)
|
|
328
|
-
execSync(scriptCmd, {
|
|
365
|
+
execSync(scriptCmd, {
|
|
366
|
+
cwd: baseDir,
|
|
367
|
+
stdio: "pipe",
|
|
368
|
+
timeout: 60000,
|
|
369
|
+
env: { ...process.env, OPL_VALIDATE_RECURSION_GUARD: "1" },
|
|
370
|
+
})
|
|
329
371
|
console.log(chalk.green("✅"))
|
|
330
372
|
success = true
|
|
331
373
|
break
|
package/src/utils/annotations.js
CHANGED
|
@@ -78,7 +78,14 @@ export function validateAnnotations(annotations, validTags, options = {}) {
|
|
|
78
78
|
const expected = maxLines[kindValue]
|
|
79
79
|
const numVal = parseInt(linesArg.value)
|
|
80
80
|
if (expected && !isNaN(numVal) && numVal > expected) {
|
|
81
|
-
|
|
81
|
+
// @limit(lines: N, reason: "...") permite documentar deuda técnica
|
|
82
|
+
// Cuando hay `reason`, se marca como [DEBT] en vez de warning/error
|
|
83
|
+
const reasonArg = limitTag.args.find((a) => a.key === 'reason')
|
|
84
|
+
if (reasonArg && reasonArg.value) {
|
|
85
|
+
warnings.push(`[DEBT-reason: ${reasonArg.value}] @kind(${kindValue}) @limit(lines) excede el límite recomendado (${expected}). Nuevo límite: ${linesArg.value}.`)
|
|
86
|
+
} else {
|
|
87
|
+
warnings.push(`@kind(${kindValue}) @limit(lines) debería ser ≤ ${expected} (actual: ${linesArg.value})`)
|
|
88
|
+
}
|
|
82
89
|
}
|
|
83
90
|
if (!isNaN(numVal) && numVal <= 0) {
|
|
84
91
|
errors.push(`@limit(lines: ${numVal}) debe ser mayor a 0`)
|
|
@@ -264,12 +271,17 @@ export function lintFile(code, strict = false, options = {}) {
|
|
|
264
271
|
|
|
265
272
|
const { errors, warnings } = validateAnnotations(annotations, validTags)
|
|
266
273
|
const allErrors = [...parseErrors, ...errors]
|
|
267
|
-
const allWarnings = [...warnings]
|
|
268
274
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
275
|
+
// Separar deuda documentada con razón explícita del resto de warnings
|
|
276
|
+
// [DEBT-reason] es deuda técnica formalmente justificada — NO debe bloquear commits
|
|
277
|
+
// incluso en strict mode. El desarrollador documentó por qué el límite se excede.
|
|
278
|
+
const debtWarnings = warnings.filter((w) => w.startsWith('[DEBT-reason'))
|
|
279
|
+
const regularWarnings = warnings.filter((w) => !w.startsWith('[DEBT-reason'))
|
|
280
|
+
|
|
281
|
+
if (strict && regularWarnings.length > 0) {
|
|
282
|
+
allErrors.push(...regularWarnings.map((w) => `[strict] ${w}`))
|
|
283
|
+
return { annotations, errors: allErrors, warnings: [...debtWarnings, ...regularWarnings] }
|
|
272
284
|
}
|
|
273
285
|
|
|
274
|
-
return { annotations, errors: allErrors, warnings:
|
|
286
|
+
return { annotations, errors: allErrors, warnings: [...debtWarnings, ...regularWarnings] }
|
|
275
287
|
}
|