@saulwade/swl-ses 1.4.0 β†’ 1.4.2

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 (116) hide show
  1. package/CLAUDE.md +4 -3
  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/comandos/swl/.evolved.json +22 -22
  6. package/comandos/swl/contribuir.md +233 -233
  7. package/comandos/swl/nemesis.md +122 -0
  8. package/comandos/swl/salud.md +34 -0
  9. package/comandos/swl/verificar.md +45 -0
  10. package/gateway/lib/event-channel.js +191 -191
  11. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  12. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  13. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  14. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  15. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  16. package/habilidades/eval-framework/SKILL.md +212 -212
  17. package/habilidades/feynman-auditor-swl/SKILL.md +123 -0
  18. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -0
  19. package/habilidades/harness-claude-code/SKILL.md +299 -299
  20. package/habilidades/infra-github-actions/SKILL.md +166 -166
  21. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  22. package/habilidades/manejo-errores/.evolved.json +8 -8
  23. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  24. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  25. package/habilidades/patrones-python/SKILL.md +229 -229
  26. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  27. package/habilidades/planear-fase/SKILL.md +319 -319
  28. package/habilidades/release-semver/.evolved.json +8 -8
  29. package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -0
  30. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -0
  31. package/habilidades/testing-python/SKILL.md +340 -340
  32. package/habilidades/web-fetcher-routing/SKILL.md +75 -0
  33. package/hooks/claudemd-bloat-detector.js +161 -161
  34. package/hooks/lib/agent-routing.js +107 -107
  35. package/hooks/lib/auto-consolidator.js +335 -335
  36. package/hooks/lib/error-classifier.js +308 -308
  37. package/hooks/lib/merkle-audit.js +96 -96
  38. package/hooks/lib/provenance-tracker.js +191 -191
  39. package/hooks/lib/rate-limit-tracker.js +253 -253
  40. package/hooks/lib/resource-quota.js +122 -122
  41. package/hooks/lib/retry-jitter.js +165 -165
  42. package/hooks/lib/security-net.js +201 -0
  43. package/hooks/lib/skill-auditor.js +588 -588
  44. package/hooks/lib/sync-status.js +228 -228
  45. package/hooks/lib/taint-tracker.js +107 -107
  46. package/hooks/lib/text-similarity.js +241 -241
  47. package/hooks/lib/toon-compressor.js +245 -245
  48. package/hooks/registro-turnos.js +209 -209
  49. package/hooks/sugerir-regenerar-inventario.js +170 -170
  50. package/hooks/validar-formato-post-subagente.js +140 -140
  51. package/hooks/validar-memoria-hook.js +218 -218
  52. package/instintos/prompt-appendices.yaml +57 -57
  53. package/manifiestos/agent-output-schemas.json +57 -57
  54. package/manifiestos/modulos.json +41 -6
  55. package/manifiestos/perfiles.json +2 -1
  56. package/manifiestos/skills-lock.json +30 -9
  57. package/package.json +2 -2
  58. package/plantillas/auditor-veto-template.md +105 -105
  59. package/plantillas/github-workflows/README.md +47 -47
  60. package/plantillas/github-workflows/release-please.yml +44 -44
  61. package/plantillas/github-workflows/swl-ci.yml +107 -107
  62. package/plantillas/github-workflows/swl-security.yml +51 -51
  63. package/plugin.json +10 -2
  64. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  65. package/reglas/arreglar-al-detectar.md +147 -147
  66. package/reglas/fragmentos-compartidos.md +152 -152
  67. package/reglas/harness-claude-code.md +213 -213
  68. package/reglas/usar-context7.md +226 -226
  69. package/schemas/diary-entry.schema.json +80 -80
  70. package/scripts/audit-tools/audit-history.js +330 -0
  71. package/scripts/audit-tools/bundle-tracker.js +290 -0
  72. package/scripts/audit-tools/canary-monitor.js +352 -0
  73. package/scripts/audit-tools/code-profiler.js +605 -0
  74. package/scripts/audit-tools/dep-doctor.js +320 -0
  75. package/scripts/audit-tools/env-validator.js +206 -0
  76. package/scripts/audit-tools/lib/fs-walk.js +48 -0
  77. package/scripts/audit-tools/lib/output.js +23 -0
  78. package/scripts/audit-tools/migration-checker.js +392 -0
  79. package/scripts/audit-tools/pentest-scanner.js +1436 -0
  80. package/scripts/benchmark-memoria.js +167 -167
  81. package/scripts/configurar-branch-protection.js +418 -418
  82. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  83. package/scripts/field-report.js +199 -199
  84. package/scripts/generar-checklists-consolidados.js +273 -273
  85. package/scripts/generar-inventario.js +420 -420
  86. package/scripts/generar-matriz-lenguajes.js +271 -271
  87. package/scripts/lib/artefactos-python.js +43 -43
  88. package/scripts/lib/benchmark-metrics.js +160 -160
  89. package/scripts/lib/budget-enforcer.js +252 -252
  90. package/scripts/lib/configurar-ci.js +380 -380
  91. package/scripts/lib/contadores-inventario.js +217 -217
  92. package/scripts/lib/detectar-stack-detallado.js +307 -307
  93. package/scripts/lib/diary-entry.js +234 -234
  94. package/scripts/lib/eval-metrics-store.js +218 -218
  95. package/scripts/lib/eval-quality.js +171 -171
  96. package/scripts/lib/eval-schemas.js +144 -144
  97. package/scripts/lib/eval-self-correct.js +106 -106
  98. package/scripts/lib/eval-validator.js +185 -185
  99. package/scripts/lib/jaccard-similarity.js +98 -98
  100. package/scripts/lib/longmemeval-runner.js +125 -125
  101. package/scripts/lib/manifiestos.js +42 -1
  102. package/scripts/lib/npm-version.js +261 -261
  103. package/scripts/lib/paquetes-conocidos.js +50 -50
  104. package/scripts/lib/prompt-builder.js +264 -264
  105. package/scripts/lib/rrf-fusion.js +175 -175
  106. package/scripts/lib/scoring-instintos.js +277 -277
  107. package/scripts/lib/semantic-search.js +252 -252
  108. package/scripts/limpiar-artefactos-python.js +131 -131
  109. package/scripts/mcp-server/README.md +128 -128
  110. package/scripts/mcp-server/handlers.js +206 -206
  111. package/scripts/migrar-csv-a-array.js +168 -168
  112. package/scripts/migrar-fase-dominio.js +201 -201
  113. package/scripts/publicar.js +511 -511
  114. package/scripts/run-eval.js +141 -141
  115. package/scripts/validar-manifest.js +231 -195
  116. package/scripts/validar-userland-vacio.js +110 -110
@@ -13,6 +13,7 @@ Eres el coordinador de verificaciΓ³n SWL. Tu trabajo es asegurar la calidad del
13
13
  | Flag | Efecto |
14
14
  |------|--------|
15
15
  | (sin flag) | VerificaciΓ³n estΓ‘ndar: tests + linter + revisor-codigo-swl + revisor-seguridad-swl |
16
+ | `--full` | AΓ±ade 4 audits ejecutables en paralelo al flujo estΓ‘ndar: `code-profiler.js`, `pentest-scanner.js`, `dep-doctor.js` y secret-scanner. Produce un scorecard agregado 0-100 con thresholds de aprobaciΓ³n. Ver secciΓ³n "Modo `--full`". |
16
17
  | `--ultra` | AΓ±ade tercera pasada de **revisiΓ³n profunda** con Opus 4.7 sobre los hallazgos de los dos revisores. Consolida, prioriza, detecta contradicciones entre reviewers y genera plan de acciΓ³n. Ideal para fases crΓ­ticas (auth, pagos, migraciones de schema, endpoints pΓΊblicos). |
17
18
  | `--solo-codigo` | Ejecuta solo revisor-codigo-swl (salta seguridad). Usar solo para refactors internos sin impacto externo. |
18
19
  | `--solo-seguridad` | Ejecuta solo revisor-seguridad-swl (salta cΓ³digo). Usar para auditorΓ­as de seguridad focalizadas. |
@@ -576,6 +577,48 @@ El comando:
576
577
  repetΓ­a 2+ veces por sesiΓ³n con alta variaciΓ³n sobre el alcance exacto. Formalizar
577
578
  con flag elimina los 2 mensajes de fricciΓ³n y deja el scope auditable en el comando.
578
579
 
580
+ ## Modo `--full` β€” Parallel Scorecard
581
+
582
+ Al pasar el flag `--full`, /swl:verificar lanza en paralelo 4 audits ejecutables ademΓ‘s del flujo secuencial estΓ‘ndar (revisor-codigo-swl + revisor-seguridad-swl). Cada audit produce score 0-100 y findings JSON. El scorecard final agrega resultados con thresholds estΓ‘ndar.
583
+
584
+ ### Audits paralelos invocados
585
+
586
+ | Tool | Detecta | Comando |
587
+ |---|---|---|
588
+ | `code-profiler.js` | N+1 queries, sync I/O en async, memory leaks, ReDoS, queries unbounded | `node scripts/audit-tools/code-profiler.js .` |
589
+ | `pentest-scanner.js` | XSS, SQLi, SSTI, CORS misconfig, JWT vulnerabilities, prototype pollution | `node scripts/audit-tools/pentest-scanner.js <target>` |
590
+ | `dep-doctor.js` | Deps no usadas, outdated, heavy deps con alternativa | `node scripts/audit-tools/dep-doctor.js .` |
591
+ | `secret-scanner` (hook existente) | Credenciales hardcodeadas | (vΓ­a hook) |
592
+
593
+ ### Thresholds del scorecard
594
+
595
+ | Score | Status | AcciΓ³n |
596
+ |---|---|---|
597
+ | β‰₯80 | READY | Listo para mergear |
598
+ | 60-79 | NEEDS WORK | Hallazgos menores; mergear bajo decisiΓ³n informada |
599
+ | <60 | NOT READY | Hallazgos crΓ­ticos; bloquear merge |
600
+
601
+ ### Salida agregada
602
+
603
+ El comando produce un reporte tipo:
604
+
605
+ ```
606
+ SCORECARD β€” Audits paralelos
607
+ ─────────────────────────────────
608
+ code-profiler: 95 / 100 [READY]
609
+ pentest-scanner: 82 / 100 [READY]
610
+ dep-doctor: 70 / 100 [NEEDS WORK]
611
+ secret-scanner: 100 / 100 [READY]
612
+ ─────────────────────────────────
613
+ Score promedio: 86.75 [READY]
614
+ Total findings: 3 (0 crΓ­ticos, 2 mayores, 1 menor)
615
+ ```
616
+
617
+ ### CuΓ‘ndo usar `--full` vs flujo estΓ‘ndar
618
+
619
+ - **Flujo estΓ‘ndar** (`/swl:verificar`): revisiΓ³n cualitativa por agentes con criterio editorial. Adecuado para PRs pequeΓ±os/medianos donde la calidad arquitectΓ³nica importa mΓ‘s que cobertura mecΓ‘nica.
620
+ - **Flag `--full`**: agrega anΓ‘lisis estΓ‘tico y dinΓ‘mico ejecutable. Adecuado pre-release, audit de seguridad mensual, o features que tocan endpoints, dependencias o lΓ³gica compleja.
621
+
579
622
  ## Reglas de comportamiento
580
623
 
581
624
  - NUNCA marques una fase como APROBADO si hay hallazgos CRÍTICOS de seguridad.
@@ -583,3 +626,5 @@ con flag elimina los 2 mensajes de fricciΓ³n y deja el scope auditable en el com
583
626
  - Si los tests fallan, es siempre CRÍTICO, sin excepciΓ³n.
584
627
  - Las SUGERENCIAS son de largo plazo β€” no bloquean el avance pero deben documentarse.
585
628
  - Si el RESUMEN.md no existe, usa git diff para inferir el alcance. Nunca te bloquees.
629
+
630
+ <!-- El patrΓ³n parallel scorecard adaptado de Houseofmvps/ultraship /ship bajo MIT License -->
@@ -1,191 +1,191 @@
1
- 'use strict';
2
-
3
- /**
4
- * event-channel.js
5
- *
6
- * Pub/sub event channel para CommandRelay y otros componentes del gateway.
7
- *
8
- * PatrΓ³n adaptado de `temp/obsidian-agent-client-master/src/acp/acp-handler.ts`
9
- * (Set<listeners> con callback unsubscribe). Diferencias:
10
- * - Zero-deps Node.js (sin RxJS, sin EventEmitter de node β€” Set nativo).
11
- * - Backward compat: si no hay listeners, comportamiento idΓ©ntico al actual.
12
- * - Tipos de evento explΓ­citos para evitar typos.
13
- *
14
- * Casos de uso en SWL:
15
- * - CommandRelay emite eventos al recibir/aceptar/rechazar/procesar comandos.
16
- * - MΓΊltiples adaptadores (Telegram, Discord) pueden suscribirse simultΓ‘neamente.
17
- * - El consumidor /swl:inbox puede mostrar progreso sin polling.
18
- *
19
- * Uso:
20
- * const channel = new EventChannel();
21
- * const off = channel.on('cmd:queued', (event) => console.log(event));
22
- * channel.emit({ type: 'cmd:queued', commandId: 'cmd-abc', userId: '123' });
23
- * off(); // unsubscribe
24
- *
25
- * @module gateway/lib/event-channel
26
- */
27
-
28
- // ── tipos de evento ──────────────────────────────────────────────────────────
29
-
30
- /**
31
- * Tipos vΓ‘lidos de evento. Se usan strings para serializar fΓ‘cilmente
32
- * a JSONL si se requiere persistencia (no obligatorio).
33
- */
34
- const EVENTS = Object.freeze({
35
- // Lifecycle de comandos en CommandRelay
36
- CMD_RECEIVED: 'cmd:received', // mensaje llegΓ³ al relay
37
- CMD_REJECTED: 'cmd:rejected', // rechazado por validaciΓ³n (auth/rate/dedup)
38
- CMD_QUEUED: 'cmd:queued', // encolado en .planning/inbox/
39
- CMD_PROCESSED: 'cmd:processed', // marcado como procesado por consumidor
40
-
41
- // Lifecycle de notificaciones
42
- NOTIFICATION_SENT: 'notification:sent',
43
- NOTIFICATION_FAILED: 'notification:failed',
44
- });
45
-
46
- // ── implementaciΓ³n ────────────────────────────────────────────────────────────
47
-
48
- /**
49
- * Canal de eventos pub/sub con escucha por tipo o wildcard.
50
- *
51
- * Mantiene dos estructuras:
52
- * - listenersByType: Map<eventType, Set<callback>>
53
- * - wildcardListeners: Set<callback> (escuchan TODOS los eventos)
54
- *
55
- * Una excepciΓ³n en un listener NO interrumpe la propagaciΓ³n a los demΓ‘s β€”
56
- * patrΓ³n de aislamiento del mismo origen (acp-handler.ts:47-50).
57
- */
58
- class EventChannel {
59
- constructor() {
60
- this.listenersByType = new Map();
61
- this.wildcardListeners = new Set();
62
- this.errorHandler = null; // opcional: callback para errores en listeners
63
- }
64
-
65
- /**
66
- * Suscribe un listener a un tipo de evento (o '*' para todos).
67
- *
68
- * @param {string} eventType - tipo de evento o '*'
69
- * @param {function} callback - recibe el objeto event
70
- * @returns {function} funciΓ³n de unsubscribe (idempotente)
71
- */
72
- on(eventType, callback) {
73
- if (typeof callback !== 'function') {
74
- throw new TypeError('callback debe ser funciΓ³n');
75
- }
76
- if (typeof eventType !== 'string' || !eventType) {
77
- throw new TypeError('eventType debe ser string no vacΓ­o');
78
- }
79
-
80
- if (eventType === '*') {
81
- this.wildcardListeners.add(callback);
82
- return () => this.wildcardListeners.delete(callback);
83
- }
84
-
85
- if (!this.listenersByType.has(eventType)) {
86
- this.listenersByType.set(eventType, new Set());
87
- }
88
- const set = this.listenersByType.get(eventType);
89
- set.add(callback);
90
- return () => set.delete(callback);
91
- }
92
-
93
- /**
94
- * Suscribe un listener que se invoca UNA sola vez y luego se desuscribe.
95
- */
96
- once(eventType, callback) {
97
- const off = this.on(eventType, (event) => {
98
- off();
99
- callback(event);
100
- });
101
- return off;
102
- }
103
-
104
- /**
105
- * Emite un evento a todos los listeners suscriptos al tipo + wildcards.
106
- * El evento debe tener al menos `type`. Se enriquece con `ts` (ISO).
107
- *
108
- * @param {object} event - objeto con `type` y datos arbitrarios
109
- * @returns {number} cantidad de listeners notificados
110
- */
111
- emit(event) {
112
- if (!event || typeof event !== 'object') {
113
- throw new TypeError('event debe ser objeto');
114
- }
115
- if (typeof event.type !== 'string' || !event.type) {
116
- throw new TypeError('event.type es obligatorio');
117
- }
118
-
119
- const enriched = { ts: new Date().toISOString(), ...event };
120
- let notified = 0;
121
-
122
- // Listeners especΓ­ficos del tipo
123
- const typedSet = this.listenersByType.get(enriched.type);
124
- if (typedSet) {
125
- for (const cb of typedSet) {
126
- notified++;
127
- this._safeInvoke(cb, enriched);
128
- }
129
- }
130
-
131
- // Listeners wildcard
132
- for (const cb of this.wildcardListeners) {
133
- notified++;
134
- this._safeInvoke(cb, enriched);
135
- }
136
-
137
- return notified;
138
- }
139
-
140
- /**
141
- * Cuenta de listeners para un tipo (o total si no se pasa tipo).
142
- */
143
- listenerCount(eventType) {
144
- if (eventType == null) {
145
- let total = this.wildcardListeners.size;
146
- for (const set of this.listenersByType.values()) total += set.size;
147
- return total;
148
- }
149
- if (eventType === '*') return this.wildcardListeners.size;
150
- const set = this.listenersByType.get(eventType);
151
- return (set ? set.size : 0) + this.wildcardListeners.size;
152
- }
153
-
154
- /**
155
- * Limpia todos los listeners. Útil para tests.
156
- */
157
- clear() {
158
- this.listenersByType.clear();
159
- this.wildcardListeners.clear();
160
- }
161
-
162
- /**
163
- * Asigna un handler para errores en listeners.
164
- * Si no se asigna, los errores se silencian (no rompen la propagaciΓ³n).
165
- */
166
- onError(handler) {
167
- if (typeof handler !== 'function') {
168
- throw new TypeError('handler debe ser funciΓ³n');
169
- }
170
- this.errorHandler = handler;
171
- }
172
-
173
- // ── internos ────────────────────────────────────────────────────────────────
174
-
175
- _safeInvoke(cb, event) {
176
- try {
177
- cb(event);
178
- } catch (err) {
179
- if (this.errorHandler) {
180
- try { this.errorHandler(err, event); } catch (_) { /* silencioso */ }
181
- }
182
- }
183
- }
184
- }
185
-
186
- // ── exports ───────────────────────────────────────────────────────────────────
187
-
188
- module.exports = {
189
- EventChannel,
190
- EVENTS,
191
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * event-channel.js
5
+ *
6
+ * Pub/sub event channel para CommandRelay y otros componentes del gateway.
7
+ *
8
+ * PatrΓ³n adaptado de `temp/obsidian-agent-client-master/src/acp/acp-handler.ts`
9
+ * (Set<listeners> con callback unsubscribe). Diferencias:
10
+ * - Zero-deps Node.js (sin RxJS, sin EventEmitter de node β€” Set nativo).
11
+ * - Backward compat: si no hay listeners, comportamiento idΓ©ntico al actual.
12
+ * - Tipos de evento explΓ­citos para evitar typos.
13
+ *
14
+ * Casos de uso en SWL:
15
+ * - CommandRelay emite eventos al recibir/aceptar/rechazar/procesar comandos.
16
+ * - MΓΊltiples adaptadores (Telegram, Discord) pueden suscribirse simultΓ‘neamente.
17
+ * - El consumidor /swl:inbox puede mostrar progreso sin polling.
18
+ *
19
+ * Uso:
20
+ * const channel = new EventChannel();
21
+ * const off = channel.on('cmd:queued', (event) => console.log(event));
22
+ * channel.emit({ type: 'cmd:queued', commandId: 'cmd-abc', userId: '123' });
23
+ * off(); // unsubscribe
24
+ *
25
+ * @module gateway/lib/event-channel
26
+ */
27
+
28
+ // ── tipos de evento ──────────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * Tipos vΓ‘lidos de evento. Se usan strings para serializar fΓ‘cilmente
32
+ * a JSONL si se requiere persistencia (no obligatorio).
33
+ */
34
+ const EVENTS = Object.freeze({
35
+ // Lifecycle de comandos en CommandRelay
36
+ CMD_RECEIVED: 'cmd:received', // mensaje llegΓ³ al relay
37
+ CMD_REJECTED: 'cmd:rejected', // rechazado por validaciΓ³n (auth/rate/dedup)
38
+ CMD_QUEUED: 'cmd:queued', // encolado en .planning/inbox/
39
+ CMD_PROCESSED: 'cmd:processed', // marcado como procesado por consumidor
40
+
41
+ // Lifecycle de notificaciones
42
+ NOTIFICATION_SENT: 'notification:sent',
43
+ NOTIFICATION_FAILED: 'notification:failed',
44
+ });
45
+
46
+ // ── implementaciΓ³n ────────────────────────────────────────────────────────────
47
+
48
+ /**
49
+ * Canal de eventos pub/sub con escucha por tipo o wildcard.
50
+ *
51
+ * Mantiene dos estructuras:
52
+ * - listenersByType: Map<eventType, Set<callback>>
53
+ * - wildcardListeners: Set<callback> (escuchan TODOS los eventos)
54
+ *
55
+ * Una excepciΓ³n en un listener NO interrumpe la propagaciΓ³n a los demΓ‘s β€”
56
+ * patrΓ³n de aislamiento del mismo origen (acp-handler.ts:47-50).
57
+ */
58
+ class EventChannel {
59
+ constructor() {
60
+ this.listenersByType = new Map();
61
+ this.wildcardListeners = new Set();
62
+ this.errorHandler = null; // opcional: callback para errores en listeners
63
+ }
64
+
65
+ /**
66
+ * Suscribe un listener a un tipo de evento (o '*' para todos).
67
+ *
68
+ * @param {string} eventType - tipo de evento o '*'
69
+ * @param {function} callback - recibe el objeto event
70
+ * @returns {function} funciΓ³n de unsubscribe (idempotente)
71
+ */
72
+ on(eventType, callback) {
73
+ if (typeof callback !== 'function') {
74
+ throw new TypeError('callback debe ser funciΓ³n');
75
+ }
76
+ if (typeof eventType !== 'string' || !eventType) {
77
+ throw new TypeError('eventType debe ser string no vacΓ­o');
78
+ }
79
+
80
+ if (eventType === '*') {
81
+ this.wildcardListeners.add(callback);
82
+ return () => this.wildcardListeners.delete(callback);
83
+ }
84
+
85
+ if (!this.listenersByType.has(eventType)) {
86
+ this.listenersByType.set(eventType, new Set());
87
+ }
88
+ const set = this.listenersByType.get(eventType);
89
+ set.add(callback);
90
+ return () => set.delete(callback);
91
+ }
92
+
93
+ /**
94
+ * Suscribe un listener que se invoca UNA sola vez y luego se desuscribe.
95
+ */
96
+ once(eventType, callback) {
97
+ const off = this.on(eventType, (event) => {
98
+ off();
99
+ callback(event);
100
+ });
101
+ return off;
102
+ }
103
+
104
+ /**
105
+ * Emite un evento a todos los listeners suscriptos al tipo + wildcards.
106
+ * El evento debe tener al menos `type`. Se enriquece con `ts` (ISO).
107
+ *
108
+ * @param {object} event - objeto con `type` y datos arbitrarios
109
+ * @returns {number} cantidad de listeners notificados
110
+ */
111
+ emit(event) {
112
+ if (!event || typeof event !== 'object') {
113
+ throw new TypeError('event debe ser objeto');
114
+ }
115
+ if (typeof event.type !== 'string' || !event.type) {
116
+ throw new TypeError('event.type es obligatorio');
117
+ }
118
+
119
+ const enriched = { ts: new Date().toISOString(), ...event };
120
+ let notified = 0;
121
+
122
+ // Listeners especΓ­ficos del tipo
123
+ const typedSet = this.listenersByType.get(enriched.type);
124
+ if (typedSet) {
125
+ for (const cb of typedSet) {
126
+ notified++;
127
+ this._safeInvoke(cb, enriched);
128
+ }
129
+ }
130
+
131
+ // Listeners wildcard
132
+ for (const cb of this.wildcardListeners) {
133
+ notified++;
134
+ this._safeInvoke(cb, enriched);
135
+ }
136
+
137
+ return notified;
138
+ }
139
+
140
+ /**
141
+ * Cuenta de listeners para un tipo (o total si no se pasa tipo).
142
+ */
143
+ listenerCount(eventType) {
144
+ if (eventType == null) {
145
+ let total = this.wildcardListeners.size;
146
+ for (const set of this.listenersByType.values()) total += set.size;
147
+ return total;
148
+ }
149
+ if (eventType === '*') return this.wildcardListeners.size;
150
+ const set = this.listenersByType.get(eventType);
151
+ return (set ? set.size : 0) + this.wildcardListeners.size;
152
+ }
153
+
154
+ /**
155
+ * Limpia todos los listeners. Útil para tests.
156
+ */
157
+ clear() {
158
+ this.listenersByType.clear();
159
+ this.wildcardListeners.clear();
160
+ }
161
+
162
+ /**
163
+ * Asigna un handler para errores en listeners.
164
+ * Si no se asigna, los errores se silencian (no rompen la propagaciΓ³n).
165
+ */
166
+ onError(handler) {
167
+ if (typeof handler !== 'function') {
168
+ throw new TypeError('handler debe ser funciΓ³n');
169
+ }
170
+ this.errorHandler = handler;
171
+ }
172
+
173
+ // ── internos ────────────────────────────────────────────────────────────────
174
+
175
+ _safeInvoke(cb, event) {
176
+ try {
177
+ cb(event);
178
+ } catch (err) {
179
+ if (this.errorHandler) {
180
+ try { this.errorHandler(err, event); } catch (_) { /* silencioso */ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ // ── exports ───────────────────────────────────────────────────────────────────
187
+
188
+ module.exports = {
189
+ EventChannel,
190
+ EVENTS,
191
+ };