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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openprompt-lang",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "PromptLang CLI — Context Engine de anotaciones para desarrollo asistido por IA",
5
5
  "type": "module",
6
6
  "main": "./bin/cli.js",
@@ -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
@@ -224,18 +224,32 @@ export async function validate(options) {
224
224
  annotationErrors++
225
225
  console.log(chalk.red(` ❌ ${relPath}: ${err}`))
226
226
  }
227
- // En modo friendly: @limit es "deuda documentada" (info), no error
228
- // En modo strict: warnings ya se mostraron como [strict] errors evitamos duplicado
229
- if (!strictMode) {
230
- for (const warn of warnings) {
231
- if (warn.includes("@limit(lines)")) {
232
- annotationDebt++
233
- console.log(chalk.dim(` 📋 ${relPath}: ${warn}`))
234
- } else {
235
- annotationWarnings++
236
- console.log(chalk.yellow(` ⚠️ ${relPath}: ${warn}`))
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, { cwd: baseDir, stdio: "pipe", timeout: 60000 })
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
@@ -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
- warnings.push(`@kind(${kindValue}) @limit(lines) debería ser ${expected} (actual: ${linesArg.value})`)
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
- if (strict && allWarnings.length > 0) {
270
- allErrors.push(...allWarnings.map((w) => `[strict] ${w}`))
271
- return { annotations, errors: allErrors, warnings: allWarnings }
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: allWarnings }
286
+ return { annotations, errors: allErrors, warnings: [...debtWarnings, ...regularWarnings] }
275
287
  }