@saulwade/swl-ses 1.3.8 → 1.4.1

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.
Files changed (148) hide show
  1. package/CLAUDE.md +15 -6
  2. package/README.md +15 -14
  3. package/agentes/nemesis-auditor-swl.md +161 -0
  4. package/bin/swl-mcp-server.js +187 -187
  5. package/bin/swl-webhook-server.js +198 -0
  6. package/comandos/swl/.evolved.json +22 -22
  7. package/comandos/swl/adoptar-proyecto.md +21 -1
  8. package/comandos/swl/claudemd.md +14 -1
  9. package/comandos/swl/contribuir.md +233 -233
  10. package/comandos/swl/exportar-vault.md +108 -0
  11. package/comandos/swl/nemesis.md +122 -0
  12. package/comandos/swl/nuevo-proyecto.md +24 -2
  13. package/comandos/swl/salud.md +34 -0
  14. package/comandos/swl/verificar.md +45 -0
  15. package/gateway/adapters/base.js +109 -0
  16. package/gateway/adapters/discord.js +167 -0
  17. package/gateway/adapters/email.js +221 -0
  18. package/gateway/adapters/slack.js +192 -0
  19. package/gateway/adapters/telegram.js +183 -0
  20. package/gateway/adapters/webhook.js +113 -0
  21. package/gateway/adapters/whatsapp.js +214 -0
  22. package/gateway/agent-executor.js +322 -0
  23. package/gateway/command-relay.js +271 -0
  24. package/gateway/cron/jobs.js +263 -0
  25. package/gateway/cron/scheduler.js +322 -0
  26. package/gateway/cron/store.js +335 -0
  27. package/gateway/index.js +320 -0
  28. package/gateway/lib/event-channel.js +191 -0
  29. package/gateway/session.js +131 -0
  30. package/gateway/webhook-server.js +324 -0
  31. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  32. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  33. package/habilidades/build-errors-nextjs/SKILL.md +55 -1
  34. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  35. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  36. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  37. package/habilidades/eval-framework/SKILL.md +212 -212
  38. package/habilidades/extractor-de-aprendizajes/SKILL.md +20 -10
  39. package/habilidades/feynman-auditor-swl/SKILL.md +123 -0
  40. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -0
  41. package/habilidades/harness-claude-code/SKILL.md +299 -299
  42. package/habilidades/infra-github-actions/SKILL.md +166 -166
  43. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  44. package/habilidades/manejo-errores/.evolved.json +8 -8
  45. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  46. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  47. package/habilidades/nextjs-testing/SKILL.md +89 -5
  48. package/habilidades/node-experto/SKILL.md +37 -1
  49. package/habilidades/patrones-python/SKILL.md +229 -229
  50. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  51. package/habilidades/planear-fase/SKILL.md +319 -319
  52. package/habilidades/react-experto/SKILL.md +45 -4
  53. package/habilidades/release-semver/.evolved.json +8 -8
  54. package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -0
  55. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -0
  56. package/habilidades/tdd-workflow/SKILL.md +36 -4
  57. package/habilidades/testing-python/SKILL.md +340 -340
  58. package/habilidades/web-fetcher-routing/SKILL.md +75 -0
  59. package/hooks/claudemd-bloat-detector.js +161 -161
  60. package/hooks/inyeccion-contexto.js +8 -3
  61. package/hooks/lib/agent-routing.js +107 -107
  62. package/hooks/lib/auto-consolidator.js +335 -335
  63. package/hooks/lib/error-classifier.js +308 -308
  64. package/hooks/lib/merkle-audit.js +96 -96
  65. package/hooks/lib/provenance-tracker.js +191 -191
  66. package/hooks/lib/rate-limit-ip.js +177 -0
  67. package/hooks/lib/rate-limit-tracker.js +253 -253
  68. package/hooks/lib/resource-quota.js +122 -122
  69. package/hooks/lib/retry-jitter.js +165 -165
  70. package/hooks/lib/security-net.js +201 -0
  71. package/hooks/lib/skill-auditor.js +588 -588
  72. package/hooks/lib/sync-status.js +228 -228
  73. package/hooks/lib/taint-tracker.js +107 -107
  74. package/hooks/lib/text-similarity.js +241 -241
  75. package/hooks/lib/toon-compressor.js +245 -245
  76. package/hooks/lib/webhook-dedup.js +184 -0
  77. package/hooks/lib/webhook-verify.js +123 -0
  78. package/hooks/proteccion-rutas.js +120 -15
  79. package/hooks/registro-turnos.js +209 -209
  80. package/hooks/sugerir-regenerar-inventario.js +170 -170
  81. package/hooks/validar-formato-post-subagente.js +140 -140
  82. package/hooks/validar-memoria-hook.js +218 -218
  83. package/instintos/prompt-appendices.yaml +57 -57
  84. package/manifiestos/agent-output-schemas.json +57 -57
  85. package/manifiestos/modulos.json +31 -0
  86. package/manifiestos/skills-lock.json +1114 -1093
  87. package/package.json +6 -4
  88. package/plantillas/auditor-veto-template.md +105 -105
  89. package/plantillas/github-workflows/README.md +47 -47
  90. package/plantillas/github-workflows/release-please.yml +44 -44
  91. package/plantillas/github-workflows/swl-ci.yml +107 -107
  92. package/plantillas/github-workflows/swl-security.yml +51 -51
  93. package/plugin.json +2 -2
  94. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  95. package/reglas/arreglar-al-detectar.md +147 -147
  96. package/reglas/fragmentos-compartidos.md +152 -152
  97. package/reglas/harness-claude-code.md +213 -213
  98. package/reglas/usar-context7.md +226 -226
  99. package/reglas/usar-sistema-swl.md +251 -0
  100. package/schemas/diary-entry.schema.json +80 -80
  101. package/scripts/audit-tools/audit-history.js +330 -0
  102. package/scripts/audit-tools/bundle-tracker.js +290 -0
  103. package/scripts/audit-tools/canary-monitor.js +352 -0
  104. package/scripts/audit-tools/code-profiler.js +605 -0
  105. package/scripts/audit-tools/dep-doctor.js +320 -0
  106. package/scripts/audit-tools/env-validator.js +206 -0
  107. package/scripts/audit-tools/lib/fs-walk.js +48 -0
  108. package/scripts/audit-tools/lib/output.js +23 -0
  109. package/scripts/audit-tools/migration-checker.js +392 -0
  110. package/scripts/audit-tools/pentest-scanner.js +1436 -0
  111. package/scripts/benchmark-memoria.js +167 -167
  112. package/scripts/comandos/skills.js +251 -2
  113. package/scripts/configurar-branch-protection.js +418 -418
  114. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  115. package/scripts/field-report.js +199 -199
  116. package/scripts/generar-checklists-consolidados.js +273 -273
  117. package/scripts/generar-inventario.js +420 -420
  118. package/scripts/generar-matriz-lenguajes.js +271 -271
  119. package/scripts/lib/artefactos-python.js +43 -43
  120. package/scripts/lib/benchmark-metrics.js +160 -160
  121. package/scripts/lib/budget-enforcer.js +252 -252
  122. package/scripts/lib/configurar-ci.js +380 -380
  123. package/scripts/lib/contadores-inventario.js +217 -217
  124. package/scripts/lib/detectar-stack-detallado.js +307 -307
  125. package/scripts/lib/diary-entry.js +234 -234
  126. package/scripts/lib/eval-metrics-store.js +218 -218
  127. package/scripts/lib/eval-quality.js +171 -171
  128. package/scripts/lib/eval-schemas.js +144 -144
  129. package/scripts/lib/eval-self-correct.js +106 -106
  130. package/scripts/lib/eval-validator.js +185 -185
  131. package/scripts/lib/jaccard-similarity.js +98 -98
  132. package/scripts/lib/longmemeval-runner.js +125 -125
  133. package/scripts/lib/npm-version.js +261 -261
  134. package/scripts/lib/paquetes-conocidos.js +50 -50
  135. package/scripts/lib/prompt-builder.js +264 -264
  136. package/scripts/lib/rrf-fusion.js +175 -175
  137. package/scripts/lib/scoring-instintos.js +277 -277
  138. package/scripts/lib/semantic-search.js +252 -252
  139. package/scripts/limpiar-artefactos-python.js +131 -131
  140. package/scripts/mcp-server/README.md +128 -128
  141. package/scripts/mcp-server/handlers.js +206 -206
  142. package/scripts/migrar-csv-a-array.js +168 -168
  143. package/scripts/migrar-fase-dominio.js +201 -201
  144. package/scripts/publicar.js +511 -511
  145. package/scripts/run-eval.js +141 -141
  146. package/scripts/validar-manifest.js +195 -195
  147. package/scripts/validar-userland-vacio.js +110 -110
  148. package/scripts/verificar-release.js +110 -0
@@ -1,252 +1,252 @@
1
- 'use strict';
2
-
3
- /**
4
- * budget-enforcer.js
5
- *
6
- * Control de presupuesto con thresholds escalonados, backpressure y
7
- * idempotency keys. Patrón adaptado del BudgetManager de Shannon
8
- * (`temp/Shannon-main/go/orchestrator/internal/budget/manager.go`),
9
- * portado a Node.js zero-deps.
10
- *
11
- * Diferencias con `hooks/tracking-costos.js`:
12
- * - tracking-costos: registra consumo y emite warnings simples (80%, 100%).
13
- * - budget-enforcer: 4 niveles (warning/approval/hard/backpressure),
14
- * idempotency keys para reintentos, decisiones puras (no I/O).
15
- *
16
- * El módulo es PURO. No escribe a disco. El consumidor decide qué hacer
17
- * con el resultado (alertar, pausar, bloquear, retrasar).
18
- *
19
- * Modelo de niveles:
20
- * - PASS : por debajo de warning. Continúa sin restricción.
21
- * - WARNING : ≥ warning_pct. Emite alerta, continúa.
22
- * - APPROVAL : ≥ approval_pct. Requiere confirmación humana antes de seguir.
23
- * - BACKPRESSURE : ≥ backpressure_pct. Continúa pero con delay configurable.
24
- * - HARD_LIMIT : ≥ hard_pct. Detiene la operación; sin override.
25
- *
26
- * Defaults razonables (modificables vía opts):
27
- * warning_pct = 0.70 (70%)
28
- * approval_pct = 0.85 (85%)
29
- * backpressure_pct = 0.90 (90%)
30
- * hard_pct = 1.00 (100%)
31
- * backpressure_delay_ms = 2000
32
- *
33
- * Idempotency:
34
- * recordUsage acepta una idempotency_key. Si ya se contabilizó esa key,
35
- * devuelve el estado SIN re-incrementar. Permite reintentos seguros.
36
- *
37
- * @module scripts/lib/budget-enforcer
38
- */
39
-
40
- // ── constantes ────────────────────────────────────────────────────────────────
41
-
42
- const DEFAULTS = Object.freeze({
43
- warning_pct: 0.70,
44
- approval_pct: 0.85,
45
- backpressure_pct: 0.90,
46
- hard_pct: 1.00,
47
- backpressure_delay_ms: 2000,
48
- });
49
-
50
- const LEVEL = Object.freeze({
51
- PASS: 'pass',
52
- WARNING: 'warning',
53
- APPROVAL: 'approval',
54
- BACKPRESSURE: 'backpressure',
55
- HARD_LIMIT: 'hard_limit',
56
- });
57
-
58
- // ── helpers ───────────────────────────────────────────────────────────────────
59
-
60
- function clamp(n, min, max) {
61
- if (Number.isNaN(n)) return min;
62
- return Math.max(min, Math.min(max, n));
63
- }
64
-
65
- function mergeOpts(opts) {
66
- return { ...DEFAULTS, ...(opts || {}) };
67
- }
68
-
69
- // ── API pública ───────────────────────────────────────────────────────────────
70
-
71
- /**
72
- * Crea un nuevo estado de presupuesto.
73
- *
74
- * @param {object} params
75
- * @param {number} params.maxUsd - Presupuesto máximo en USD
76
- * @param {number} params.maxTokens - Presupuesto máximo en tokens
77
- * @returns {object} estado inicial
78
- */
79
- function createBudget({ maxUsd = 10.0, maxTokens = 500_000 } = {}) {
80
- return {
81
- maxUsd,
82
- maxTokens,
83
- usedUsd: 0,
84
- usedTokens: 0,
85
- requestCount: 0,
86
- idempotency: {}, // key → { usd, tokens, recordedAt }
87
- createdAt: new Date().toISOString(),
88
- };
89
- }
90
-
91
- /**
92
- * Registra consumo de presupuesto. Pure: devuelve nuevo estado.
93
- *
94
- * @param {object} budget - estado actual
95
- * @param {object} usage - consumo a registrar
96
- * @param {number} usage.usd
97
- * @param {number} usage.tokens
98
- * @param {string} [usage.idempotencyKey] - si presente, evita doble-conteo
99
- * @returns {{ budget: object, deduplicated: boolean }}
100
- */
101
- function recordUsage(budget, usage) {
102
- const next = { ...budget, idempotency: { ...budget.idempotency } };
103
- const usd = Number(usage.usd) || 0;
104
- const tokens = Number(usage.tokens) || 0;
105
- const key = usage.idempotencyKey;
106
-
107
- if (key && next.idempotency[key]) {
108
- // Ya contabilizado, retornar sin cambios
109
- return { budget: next, deduplicated: true };
110
- }
111
-
112
- next.usedUsd = next.usedUsd + usd;
113
- next.usedTokens = next.usedTokens + tokens;
114
- next.requestCount = next.requestCount + 1;
115
-
116
- if (key) {
117
- next.idempotency[key] = {
118
- usd,
119
- tokens,
120
- recordedAt: new Date().toISOString(),
121
- };
122
- }
123
-
124
- return { budget: next, deduplicated: false };
125
- }
126
-
127
- /**
128
- * Calcula la fracción del presupuesto consumida.
129
- * Devuelve max(usd_pct, tokens_pct) — el más cercano al límite manda.
130
- *
131
- * @param {object} budget
132
- * @returns {number} en [0, ∞) — sí puede pasar 1.0 si hay overshoot
133
- */
134
- function consumptionRatio(budget) {
135
- const usdPct = budget.maxUsd > 0 ? budget.usedUsd / budget.maxUsd : 0;
136
- const tokensPct = budget.maxTokens > 0 ? budget.usedTokens / budget.maxTokens : 0;
137
- return Math.max(usdPct, tokensPct);
138
- }
139
-
140
- /**
141
- * Determina el nivel de threshold actual.
142
- *
143
- * @param {object} budget
144
- * @param {object} [opts] - thresholds custom
145
- * @returns {string} LEVEL.*
146
- */
147
- function currentLevel(budget, opts) {
148
- const cfg = mergeOpts(opts);
149
- const ratio = consumptionRatio(budget);
150
-
151
- if (ratio >= cfg.hard_pct) return LEVEL.HARD_LIMIT;
152
- if (ratio >= cfg.backpressure_pct) return LEVEL.BACKPRESSURE;
153
- if (ratio >= cfg.approval_pct) return LEVEL.APPROVAL;
154
- if (ratio >= cfg.warning_pct) return LEVEL.WARNING;
155
- return LEVEL.PASS;
156
- }
157
-
158
- /**
159
- * Decisión combinada: nivel + acción recomendada + delay si aplica.
160
- *
161
- * @param {object} budget
162
- * @param {object} [opts]
163
- * @returns {object} {
164
- * level, ratio, allow, requiresApproval, delayMs, reason
165
- * }
166
- */
167
- function decide(budget, opts) {
168
- const cfg = mergeOpts(opts);
169
- const ratio = consumptionRatio(budget);
170
- const level = currentLevel(budget, cfg);
171
-
172
- const decision = {
173
- level,
174
- ratio: Math.round(ratio * 1000) / 1000,
175
- allow: true,
176
- requiresApproval: false,
177
- delayMs: 0,
178
- reason: null,
179
- };
180
-
181
- switch (level) {
182
- case LEVEL.HARD_LIMIT:
183
- decision.allow = false;
184
- decision.reason = `Presupuesto agotado (${(ratio * 100).toFixed(1)}% del máximo). Hard limit alcanzado.`;
185
- break;
186
- case LEVEL.BACKPRESSURE:
187
- decision.delayMs = cfg.backpressure_delay_ms;
188
- decision.reason = `Cerca del límite (${(ratio * 100).toFixed(1)}%). Aplicando backpressure de ${cfg.backpressure_delay_ms}ms.`;
189
- break;
190
- case LEVEL.APPROVAL:
191
- decision.requiresApproval = true;
192
- decision.reason = `Presupuesto al ${(ratio * 100).toFixed(1)}%. Requiere confirmación humana antes de continuar.`;
193
- break;
194
- case LEVEL.WARNING:
195
- decision.reason = `Aviso: presupuesto al ${(ratio * 100).toFixed(1)}% del máximo.`;
196
- break;
197
- case LEVEL.PASS:
198
- default:
199
- break;
200
- }
201
-
202
- return decision;
203
- }
204
-
205
- /**
206
- * Estima si una operación futura cabe en el presupuesto.
207
- * Útil para decidir antes de invocar un agente costoso.
208
- *
209
- * @param {object} budget
210
- * @param {object} estimate
211
- * @param {number} estimate.usd
212
- * @param {number} estimate.tokens
213
- * @param {object} [opts]
214
- * @returns {object} mismo formato que decide() — proyecta el estado post-operación
215
- */
216
- function projectDecision(budget, estimate, opts) {
217
- const projected = {
218
- ...budget,
219
- usedUsd: budget.usedUsd + (Number(estimate.usd) || 0),
220
- usedTokens: budget.usedTokens + (Number(estimate.tokens) || 0),
221
- };
222
- return decide(projected, opts);
223
- }
224
-
225
- /**
226
- * Resetea el contador de uso manteniendo límites e idempotency cache.
227
- * Útil para nuevos ciclos de facturación.
228
- */
229
- function resetUsage(budget) {
230
- return {
231
- ...budget,
232
- usedUsd: 0,
233
- usedTokens: 0,
234
- requestCount: 0,
235
- idempotency: {},
236
- resetAt: new Date().toISOString(),
237
- };
238
- }
239
-
240
- // ── exports ───────────────────────────────────────────────────────────────────
241
-
242
- module.exports = {
243
- createBudget,
244
- recordUsage,
245
- consumptionRatio,
246
- currentLevel,
247
- decide,
248
- projectDecision,
249
- resetUsage,
250
- LEVEL,
251
- DEFAULTS,
252
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * budget-enforcer.js
5
+ *
6
+ * Control de presupuesto con thresholds escalonados, backpressure y
7
+ * idempotency keys. Patrón adaptado del BudgetManager de Shannon
8
+ * (`temp/Shannon-main/go/orchestrator/internal/budget/manager.go`),
9
+ * portado a Node.js zero-deps.
10
+ *
11
+ * Diferencias con `hooks/tracking-costos.js`:
12
+ * - tracking-costos: registra consumo y emite warnings simples (80%, 100%).
13
+ * - budget-enforcer: 4 niveles (warning/approval/hard/backpressure),
14
+ * idempotency keys para reintentos, decisiones puras (no I/O).
15
+ *
16
+ * El módulo es PURO. No escribe a disco. El consumidor decide qué hacer
17
+ * con el resultado (alertar, pausar, bloquear, retrasar).
18
+ *
19
+ * Modelo de niveles:
20
+ * - PASS : por debajo de warning. Continúa sin restricción.
21
+ * - WARNING : ≥ warning_pct. Emite alerta, continúa.
22
+ * - APPROVAL : ≥ approval_pct. Requiere confirmación humana antes de seguir.
23
+ * - BACKPRESSURE : ≥ backpressure_pct. Continúa pero con delay configurable.
24
+ * - HARD_LIMIT : ≥ hard_pct. Detiene la operación; sin override.
25
+ *
26
+ * Defaults razonables (modificables vía opts):
27
+ * warning_pct = 0.70 (70%)
28
+ * approval_pct = 0.85 (85%)
29
+ * backpressure_pct = 0.90 (90%)
30
+ * hard_pct = 1.00 (100%)
31
+ * backpressure_delay_ms = 2000
32
+ *
33
+ * Idempotency:
34
+ * recordUsage acepta una idempotency_key. Si ya se contabilizó esa key,
35
+ * devuelve el estado SIN re-incrementar. Permite reintentos seguros.
36
+ *
37
+ * @module scripts/lib/budget-enforcer
38
+ */
39
+
40
+ // ── constantes ────────────────────────────────────────────────────────────────
41
+
42
+ const DEFAULTS = Object.freeze({
43
+ warning_pct: 0.70,
44
+ approval_pct: 0.85,
45
+ backpressure_pct: 0.90,
46
+ hard_pct: 1.00,
47
+ backpressure_delay_ms: 2000,
48
+ });
49
+
50
+ const LEVEL = Object.freeze({
51
+ PASS: 'pass',
52
+ WARNING: 'warning',
53
+ APPROVAL: 'approval',
54
+ BACKPRESSURE: 'backpressure',
55
+ HARD_LIMIT: 'hard_limit',
56
+ });
57
+
58
+ // ── helpers ───────────────────────────────────────────────────────────────────
59
+
60
+ function clamp(n, min, max) {
61
+ if (Number.isNaN(n)) return min;
62
+ return Math.max(min, Math.min(max, n));
63
+ }
64
+
65
+ function mergeOpts(opts) {
66
+ return { ...DEFAULTS, ...(opts || {}) };
67
+ }
68
+
69
+ // ── API pública ───────────────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * Crea un nuevo estado de presupuesto.
73
+ *
74
+ * @param {object} params
75
+ * @param {number} params.maxUsd - Presupuesto máximo en USD
76
+ * @param {number} params.maxTokens - Presupuesto máximo en tokens
77
+ * @returns {object} estado inicial
78
+ */
79
+ function createBudget({ maxUsd = 10.0, maxTokens = 500_000 } = {}) {
80
+ return {
81
+ maxUsd,
82
+ maxTokens,
83
+ usedUsd: 0,
84
+ usedTokens: 0,
85
+ requestCount: 0,
86
+ idempotency: {}, // key → { usd, tokens, recordedAt }
87
+ createdAt: new Date().toISOString(),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Registra consumo de presupuesto. Pure: devuelve nuevo estado.
93
+ *
94
+ * @param {object} budget - estado actual
95
+ * @param {object} usage - consumo a registrar
96
+ * @param {number} usage.usd
97
+ * @param {number} usage.tokens
98
+ * @param {string} [usage.idempotencyKey] - si presente, evita doble-conteo
99
+ * @returns {{ budget: object, deduplicated: boolean }}
100
+ */
101
+ function recordUsage(budget, usage) {
102
+ const next = { ...budget, idempotency: { ...budget.idempotency } };
103
+ const usd = Number(usage.usd) || 0;
104
+ const tokens = Number(usage.tokens) || 0;
105
+ const key = usage.idempotencyKey;
106
+
107
+ if (key && next.idempotency[key]) {
108
+ // Ya contabilizado, retornar sin cambios
109
+ return { budget: next, deduplicated: true };
110
+ }
111
+
112
+ next.usedUsd = next.usedUsd + usd;
113
+ next.usedTokens = next.usedTokens + tokens;
114
+ next.requestCount = next.requestCount + 1;
115
+
116
+ if (key) {
117
+ next.idempotency[key] = {
118
+ usd,
119
+ tokens,
120
+ recordedAt: new Date().toISOString(),
121
+ };
122
+ }
123
+
124
+ return { budget: next, deduplicated: false };
125
+ }
126
+
127
+ /**
128
+ * Calcula la fracción del presupuesto consumida.
129
+ * Devuelve max(usd_pct, tokens_pct) — el más cercano al límite manda.
130
+ *
131
+ * @param {object} budget
132
+ * @returns {number} en [0, ∞) — sí puede pasar 1.0 si hay overshoot
133
+ */
134
+ function consumptionRatio(budget) {
135
+ const usdPct = budget.maxUsd > 0 ? budget.usedUsd / budget.maxUsd : 0;
136
+ const tokensPct = budget.maxTokens > 0 ? budget.usedTokens / budget.maxTokens : 0;
137
+ return Math.max(usdPct, tokensPct);
138
+ }
139
+
140
+ /**
141
+ * Determina el nivel de threshold actual.
142
+ *
143
+ * @param {object} budget
144
+ * @param {object} [opts] - thresholds custom
145
+ * @returns {string} LEVEL.*
146
+ */
147
+ function currentLevel(budget, opts) {
148
+ const cfg = mergeOpts(opts);
149
+ const ratio = consumptionRatio(budget);
150
+
151
+ if (ratio >= cfg.hard_pct) return LEVEL.HARD_LIMIT;
152
+ if (ratio >= cfg.backpressure_pct) return LEVEL.BACKPRESSURE;
153
+ if (ratio >= cfg.approval_pct) return LEVEL.APPROVAL;
154
+ if (ratio >= cfg.warning_pct) return LEVEL.WARNING;
155
+ return LEVEL.PASS;
156
+ }
157
+
158
+ /**
159
+ * Decisión combinada: nivel + acción recomendada + delay si aplica.
160
+ *
161
+ * @param {object} budget
162
+ * @param {object} [opts]
163
+ * @returns {object} {
164
+ * level, ratio, allow, requiresApproval, delayMs, reason
165
+ * }
166
+ */
167
+ function decide(budget, opts) {
168
+ const cfg = mergeOpts(opts);
169
+ const ratio = consumptionRatio(budget);
170
+ const level = currentLevel(budget, cfg);
171
+
172
+ const decision = {
173
+ level,
174
+ ratio: Math.round(ratio * 1000) / 1000,
175
+ allow: true,
176
+ requiresApproval: false,
177
+ delayMs: 0,
178
+ reason: null,
179
+ };
180
+
181
+ switch (level) {
182
+ case LEVEL.HARD_LIMIT:
183
+ decision.allow = false;
184
+ decision.reason = `Presupuesto agotado (${(ratio * 100).toFixed(1)}% del máximo). Hard limit alcanzado.`;
185
+ break;
186
+ case LEVEL.BACKPRESSURE:
187
+ decision.delayMs = cfg.backpressure_delay_ms;
188
+ decision.reason = `Cerca del límite (${(ratio * 100).toFixed(1)}%). Aplicando backpressure de ${cfg.backpressure_delay_ms}ms.`;
189
+ break;
190
+ case LEVEL.APPROVAL:
191
+ decision.requiresApproval = true;
192
+ decision.reason = `Presupuesto al ${(ratio * 100).toFixed(1)}%. Requiere confirmación humana antes de continuar.`;
193
+ break;
194
+ case LEVEL.WARNING:
195
+ decision.reason = `Aviso: presupuesto al ${(ratio * 100).toFixed(1)}% del máximo.`;
196
+ break;
197
+ case LEVEL.PASS:
198
+ default:
199
+ break;
200
+ }
201
+
202
+ return decision;
203
+ }
204
+
205
+ /**
206
+ * Estima si una operación futura cabe en el presupuesto.
207
+ * Útil para decidir antes de invocar un agente costoso.
208
+ *
209
+ * @param {object} budget
210
+ * @param {object} estimate
211
+ * @param {number} estimate.usd
212
+ * @param {number} estimate.tokens
213
+ * @param {object} [opts]
214
+ * @returns {object} mismo formato que decide() — proyecta el estado post-operación
215
+ */
216
+ function projectDecision(budget, estimate, opts) {
217
+ const projected = {
218
+ ...budget,
219
+ usedUsd: budget.usedUsd + (Number(estimate.usd) || 0),
220
+ usedTokens: budget.usedTokens + (Number(estimate.tokens) || 0),
221
+ };
222
+ return decide(projected, opts);
223
+ }
224
+
225
+ /**
226
+ * Resetea el contador de uso manteniendo límites e idempotency cache.
227
+ * Útil para nuevos ciclos de facturación.
228
+ */
229
+ function resetUsage(budget) {
230
+ return {
231
+ ...budget,
232
+ usedUsd: 0,
233
+ usedTokens: 0,
234
+ requestCount: 0,
235
+ idempotency: {},
236
+ resetAt: new Date().toISOString(),
237
+ };
238
+ }
239
+
240
+ // ── exports ───────────────────────────────────────────────────────────────────
241
+
242
+ module.exports = {
243
+ createBudget,
244
+ recordUsage,
245
+ consumptionRatio,
246
+ currentLevel,
247
+ decide,
248
+ projectDecision,
249
+ resetUsage,
250
+ LEVEL,
251
+ DEFAULTS,
252
+ };