oficina-opencode 3.1.0 → 3.1.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 (69) hide show
  1. package/README.md +1 -1
  2. package/commands/oficina.md +7 -6
  3. package/dashboard/.dom-check.html +1257 -0
  4. package/dashboard/README-DASHBOARD.md +64 -0
  5. package/dashboard/assets/factory.css +1543 -0
  6. package/dashboard/assets/tokens.css +208 -0
  7. package/dashboard/build-kanban-data.mjs +279 -0
  8. package/dashboard/dashboard.css +753 -0
  9. package/dashboard/dashboard.html +121 -0
  10. package/dashboard/dashboard.js +610 -0
  11. package/dashboard/data/snapshot.json +883 -0
  12. package/dashboard/debate.html +134 -0
  13. package/dashboard/factory/build-snapshot.mjs +89 -0
  14. package/dashboard/factory/factory-app.js +1185 -0
  15. package/dashboard/factory/factory-core.js +541 -0
  16. package/dashboard/factory/snapshot.js +850 -0
  17. package/dashboard/factory.html +350 -0
  18. package/dashboard/kanban-data.json +671 -0
  19. package/dashboard/kanban-source.js +643 -0
  20. package/dashboard/kanban.check.mjs +454 -0
  21. package/dashboard/kanban.html +1257 -0
  22. package/dashboard/leadership.html +1008 -0
  23. package/dashboard/lib/debate-app.js +1149 -0
  24. package/dashboard/lib/debate-core.js +698 -0
  25. package/dashboard/lib/debate-data.js +163 -0
  26. package/dashboard/lib/debate-demo.js +110 -0
  27. package/dashboard/lib/debate.css +1155 -0
  28. package/dashboard/lib/leadership-model.js +946 -0
  29. package/dashboard/probe.mjs +35 -0
  30. package/dashboard/scripts/serve.mjs +122 -0
  31. package/dashboard/scripts/snapshot-data.mjs +93 -0
  32. package/dashboard/scripts/snapshot.mjs +28 -0
  33. package/dashboard/server.mjs +242 -0
  34. package/dashboard/shot.mjs +54 -0
  35. package/dashboard/shots/desktop-gates.png +0 -0
  36. package/dashboard/shots/desktop-light.png +0 -0
  37. package/dashboard/shots/desktop-real.png +0 -0
  38. package/dashboard/shots/mobile.png +0 -0
  39. package/dashboard/test/factory-core.test.mjs +244 -0
  40. package/dashboard/tests/debate-core.test.mjs +336 -0
  41. package/dashboard/tests/leadership-model.test.mjs +404 -0
  42. package/dist/cli.js +24 -0
  43. package/dist/cli.js.map +1 -1
  44. package/dist/core/dashboard.d.ts +33 -0
  45. package/dist/core/dashboard.d.ts.map +1 -0
  46. package/dist/core/dashboard.js +115 -0
  47. package/dist/core/dashboard.js.map +1 -0
  48. package/dist/core/dispatch.d.ts.map +1 -1
  49. package/dist/core/dispatch.js +11 -0
  50. package/dist/core/dispatch.js.map +1 -1
  51. package/dist/core/environment.d.ts +28 -0
  52. package/dist/core/environment.d.ts.map +1 -0
  53. package/dist/core/environment.js +80 -0
  54. package/dist/core/environment.js.map +1 -0
  55. package/dist/core/systemPrompt.d.ts.map +1 -1
  56. package/dist/core/systemPrompt.js +3 -0
  57. package/dist/core/systemPrompt.js.map +1 -1
  58. package/dist/tools/index.d.ts +9 -1
  59. package/dist/tools/index.d.ts.map +1 -1
  60. package/dist/tools/index.js +26 -22
  61. package/dist/tools/index.js.bak +972 -0
  62. package/dist/tools/index.js.map +1 -1
  63. package/dist/v2.d.ts +5 -0
  64. package/dist/v2.d.ts.map +1 -1
  65. package/dist/v2.js +57 -5
  66. package/dist/v2.js.bak +517 -0
  67. package/dist/v2.js.map +1 -1
  68. package/docs/QUE-ES.md +1 -1
  69. package/package.json +4 -1
@@ -0,0 +1,698 @@
1
+ /*
2
+ * debate-core.js — lógica pura del panel de debate (T-05).
3
+ *
4
+ * Se carga como script clásico (no módulo) para que funcione tanto con
5
+ * `file://` como servido por `dashboard/server.mjs`, y se publica como
6
+ * `globalThis.DebateCore`. Sin DOM, sin red: todo lo que decide qué es un
7
+ * debate, cómo se anidan las respuestas y en qué estado está un hilo vive acá,
8
+ * y por eso es lo que cubren los tests (`dashboard/tests/*.test.mjs`).
9
+ *
10
+ * Contrato de entrada: `BusMessage` de `src/core/bus.ts`
11
+ * { id, seq, from, to[], kind, subject, body{}, inReplyTo, ttlSec,
12
+ * createdAt, priority, idempotencyKey, hops }
13
+ */
14
+ (function (root) {
15
+ "use strict";
16
+
17
+ /** Respuestas que anidan bajo un HELP_REQUEST. */
18
+ var RESPONSE_KINDS = ["HELP_ACCEPT", "HELP_DECLINE", "HELP_PARTIAL", "HELP_QUESTION"];
19
+ /** Tipos que cierran un hilo (el que pidió ayuda reporta el desenlace). */
20
+ var CLOSURE_KINDS = ["STATUS", "NOTICE", "ANSWER"];
21
+
22
+ var RESPONSE_SET = toSet(RESPONSE_KINDS);
23
+ var CLOSURE_SET = toSet(CLOSURE_KINDS);
24
+ var KIND_SET = toSet([
25
+ "HELP_REQUEST", "HELP_ACCEPT", "HELP_DECLINE", "HELP_PARTIAL", "HELP_QUESTION",
26
+ "QUESTION", "ANSWER", "SPAWN_REQUEST", "SPAWN_APPROVED", "SPAWN_REJECTED",
27
+ "CLAIM", "RELEASE", "STATUS", "NOTICE",
28
+ ]);
29
+ var PRIORITIES = toSet(["low", "normal", "high", "critical"]);
30
+ var DEFAULT_TTL_SEC = 900;
31
+
32
+ var STATUS_LABELS = {
33
+ pending: "Esperando respuesta",
34
+ accepted: "Aceptado",
35
+ declined: "Rechazado",
36
+ partial: "Parcial",
37
+ completed: "Completado",
38
+ };
39
+ var KIND_LABELS = {
40
+ HELP_REQUEST: "Pide ayuda",
41
+ HELP_ACCEPT: "Acepta",
42
+ HELP_DECLINE: "Rechaza",
43
+ HELP_PARTIAL: "Ayuda parcial",
44
+ HELP_QUESTION: "Pregunta",
45
+ QUESTION: "Pregunta",
46
+ ANSWER: "Respuesta",
47
+ STATUS: "Estado",
48
+ NOTICE: "Aviso",
49
+ };
50
+ var BODY_LABELS = {
51
+ need: "Necesita",
52
+ why_me: "Por qué no lo resuelve solo",
53
+ whyMe: "Por qué no lo resuelve solo",
54
+ answer: "Respuesta",
55
+ summary: "Resumen",
56
+ reason: "Motivo",
57
+ text: "Mensaje",
58
+ message: "Mensaje",
59
+ detail: "Detalle",
60
+ card: "Card",
61
+ state: "Estado",
62
+ available_for: "Disponible para",
63
+ quality_gate: "Control de calidad",
64
+ };
65
+ var BODY_ORDER = Object.keys(BODY_LABELS);
66
+
67
+ // ---------------------------------------------------------------- helpers
68
+
69
+ function toSet(list) {
70
+ var set = Object.create(null);
71
+ for (var i = 0; i < list.length; i += 1) set[list[i]] = true;
72
+ return set;
73
+ }
74
+
75
+ function isObject(value) {
76
+ return value !== null && typeof value === "object" && !Array.isArray(value);
77
+ }
78
+
79
+ function str(value) {
80
+ return typeof value === "string" ? value.trim() : "";
81
+ }
82
+
83
+ function parseTime(iso) {
84
+ if (typeof iso !== "string") return NaN;
85
+ var t = Date.parse(iso);
86
+ return Number.isFinite(t) ? t : NaN;
87
+ }
88
+
89
+ function escapeHtml(value) {
90
+ return String(value)
91
+ .replace(/&/g, "&amp;")
92
+ .replace(/</g, "&lt;")
93
+ .replace(/>/g, "&gt;")
94
+ .replace(/"/g, "&quot;")
95
+ .replace(/'/g, "&#39;");
96
+ }
97
+
98
+ function fold(value) {
99
+ var text = String(value == null ? "" : value).toLowerCase();
100
+ if (typeof text.normalize === "function") {
101
+ text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
102
+ }
103
+ return text;
104
+ }
105
+
106
+ // ------------------------------------------------------- normalización
107
+
108
+ /**
109
+ * Valida y normaliza un mensaje del bus. Devuelve null si no sirve
110
+ * (sin id o sin createdAt parseable) — un mensaje ilegible no debe
111
+ * romper el panel ni inventar un hilo.
112
+ */
113
+ function normalizeMessage(raw, channel) {
114
+ if (!isObject(raw)) return null;
115
+ var id = str(raw.id);
116
+ if (!id) return null;
117
+ var createdAt = parseTime(raw.createdAt);
118
+ if (!Number.isFinite(createdAt)) return null;
119
+
120
+ var kind = str(raw.kind).toUpperCase();
121
+ if (!KIND_SET[kind]) kind = "NOTICE";
122
+
123
+ var to = [];
124
+ if (Array.isArray(raw.to)) {
125
+ for (var i = 0; i < raw.to.length; i += 1) {
126
+ var addr = str(raw.to[i]);
127
+ if (addr && to.indexOf(addr) === -1) to.push(addr);
128
+ }
129
+ } else if (typeof raw.to === "string" && str(raw.to)) {
130
+ to.push(str(raw.to));
131
+ }
132
+
133
+ var ttl = Number(raw.ttlSec);
134
+ if (!Number.isFinite(ttl) || ttl <= 0) ttl = DEFAULT_TTL_SEC;
135
+
136
+ var priority = str(raw.priority).toLowerCase();
137
+ if (!PRIORITIES[priority]) priority = "normal";
138
+
139
+ var seq = Number(raw.seq);
140
+
141
+ return {
142
+ id: id,
143
+ seq: Number.isFinite(seq) ? seq : 0,
144
+ from: str(raw.from) || "desconocido",
145
+ to: to,
146
+ kind: kind,
147
+ subject: str(raw.subject),
148
+ body: isObject(raw.body) ? raw.body : {},
149
+ inReplyTo: str(raw.inReplyTo) || null,
150
+ ttlSec: ttl,
151
+ createdAt: new Date(createdAt).toISOString(),
152
+ createdAtMs: createdAt,
153
+ expiresAtMs: createdAt + ttl * 1000,
154
+ priority: priority,
155
+ idempotencyKey: str(raw.idempotencyKey) || null,
156
+ hops: Number.isFinite(Number(raw.hops)) ? Number(raw.hops) : 0,
157
+ channel: str(channel || raw.channel) || null,
158
+ };
159
+ }
160
+
161
+ /** ¿Es un pedido de ayuda dirigido a todos? (criterio 1) */
162
+ function isBroadcastHelpRequest(message) {
163
+ return !!message && message.kind === "HELP_REQUEST" && message.to.indexOf("*") !== -1;
164
+ }
165
+
166
+ /**
167
+ * ¿Abre un hilo de debate? Broadcast (to:['*']) o pedido a varios peers.
168
+ * El broadcast es el caso canónico; un pedido a 2+ agentes también genera
169
+ * respuestas anidadas y no queremos perderlo.
170
+ */
171
+ function isDebateRoot(message) {
172
+ return !!message && message.kind === "HELP_REQUEST" && (message.to.indexOf("*") !== -1 || message.to.length > 1);
173
+ }
174
+
175
+ function isExpired(message, now) {
176
+ var t = Number.isFinite(now) ? now : Date.now();
177
+ return t > message.expiresAtMs;
178
+ }
179
+
180
+ /** Deduplica por id conservando el primer registro y el canal original. */
181
+ function dedupeMessages(messages) {
182
+ var seen = Object.create(null);
183
+ var out = [];
184
+ for (var i = 0; i < messages.length; i += 1) {
185
+ var m = messages[i];
186
+ if (!m || seen[m.id]) continue;
187
+ seen[m.id] = true;
188
+ out.push(m);
189
+ }
190
+ return out;
191
+ }
192
+
193
+ // ---------------------------------------------------------------- hilos
194
+
195
+ function resolveRootOf(message, byId, rootIds) {
196
+ var current = message;
197
+ var guard = 0;
198
+ while (current && current.inReplyTo && guard < 32) {
199
+ guard += 1;
200
+ if (rootIds[current.inReplyTo]) return rootIds[current.inReplyTo];
201
+ current = byId[current.inReplyTo] || null;
202
+ }
203
+ return null;
204
+ }
205
+
206
+ /**
207
+ * Agrupa mensajes en hilos de debate.
208
+ *
209
+ * - raíz: HELP_REQUEST broadcast (o multi-destinatario)
210
+ * - respuestas: cualquier mensaje que apunte a la raíz, directo o a través
211
+ * de otro mensaje del hilo (inReplyTo transitivo)
212
+ * - huérfanas: respuestas cuyo inReplyTo no existe en el snapshot pero cuyo
213
+ * `subject` identifica un único hilo → se reenganchan (el bus rota por TTL
214
+ * y el padre puede haber vencido). Si es ambiguo, van a `unlinked` y se
215
+ * reportan, no se adivinan.
216
+ */
217
+ function buildThreads(messages, options) {
218
+ var opts = options || {};
219
+ var now = Number.isFinite(opts.now) ? opts.now : Date.now();
220
+
221
+ var normalized = [];
222
+ for (var i = 0; i < messages.length; i += 1) {
223
+ var m = normalizeMessage(messages[i], messages[i] && messages[i].channel);
224
+ if (m) normalized.push(m);
225
+ }
226
+ var unique = dedupeMessages(normalized);
227
+
228
+ var byId = Object.create(null);
229
+ for (var j = 0; j < unique.length; j += 1) byId[unique[j].id] = unique[j];
230
+
231
+ var roots = [];
232
+ var rootIds = Object.create(null);
233
+ for (var k = 0; k < unique.length; k += 1) {
234
+ if (isDebateRoot(unique[k])) {
235
+ roots.push(unique[k]);
236
+ rootIds[unique[k].id] = unique[k].id;
237
+ }
238
+ }
239
+
240
+ var repliesByRoot = Object.create(null);
241
+ for (var r = 0; r < roots.length; r += 1) repliesByRoot[roots[r].id] = [];
242
+
243
+ var rootsBySubject = Object.create(null);
244
+ for (var s = 0; s < roots.length; s += 1) {
245
+ var subject = fold(roots[s].subject);
246
+ if (!subject) continue;
247
+ if (!rootsBySubject[subject]) rootsBySubject[subject] = [];
248
+ rootsBySubject[subject].push(roots[s]);
249
+ }
250
+
251
+ var unlinked = [];
252
+ for (var n = 0; n < unique.length; n += 1) {
253
+ var msg = unique[n];
254
+ if (rootIds[msg.id]) continue;
255
+
256
+ var target = msg.inReplyTo ? resolveRootOf(msg, byId, rootIds) : null;
257
+ if (target) {
258
+ repliesByRoot[target].push(msg);
259
+ continue;
260
+ }
261
+
262
+ // Reenganche por subject: solo respuestas de ayuda, solo si hay un
263
+ // único hilo candidato y no se está contestando a sí mismo.
264
+ var candidates = RESPONSE_SET[msg.kind] && msg.subject ? rootsBySubject[fold(msg.subject)] : null;
265
+ if (candidates && candidates.length === 1 && candidates[0].from !== msg.from) {
266
+ repliesByRoot[candidates[0].id].push(msg);
267
+ msg.reattached = true;
268
+ continue;
269
+ }
270
+
271
+ if (msg.inReplyTo || RESPONSE_SET[msg.kind]) unlinked.push(msg);
272
+ }
273
+
274
+ var threads = [];
275
+ for (var t = 0; t < roots.length; t += 1) {
276
+ var rootMsg = roots[t];
277
+ var replies = repliesByRoot[rootMsg.id].slice().sort(compareByTime);
278
+ threads.push(buildThread(rootMsg, replies, now));
279
+ }
280
+ threads.sort(compareThreads);
281
+
282
+ return { threads: threads, unlinked: unlinked, byId: byId, messages: unique };
283
+ }
284
+
285
+ function compareByTime(a, b) {
286
+ if (a.createdAtMs !== b.createdAtMs) return a.createdAtMs - b.createdAtMs;
287
+ return a.seq - b.seq;
288
+ }
289
+
290
+ function compareThreads(a, b) {
291
+ if (b.updatedAtMs !== a.updatedAtMs) return b.updatedAtMs - a.updatedAtMs;
292
+ return a.root.seq - b.root.seq;
293
+ }
294
+
295
+ function buildThread(rootMessage, replies, now) {
296
+ var counts = { accept: 0, decline: 0, partial: 0, question: 0, closure: 0, other: 0 };
297
+ var participants = [rootMessage.from];
298
+ var updatedAtMs = rootMessage.createdAtMs;
299
+
300
+ for (var i = 0; i < replies.length; i += 1) {
301
+ var m = replies[i];
302
+ if (m.kind === "HELP_ACCEPT") counts.accept += 1;
303
+ else if (m.kind === "HELP_DECLINE") counts.decline += 1;
304
+ else if (m.kind === "HELP_PARTIAL") counts.partial += 1;
305
+ else if (m.kind === "HELP_QUESTION" || m.kind === "QUESTION") counts.question += 1;
306
+ else if (CLOSURE_SET[m.kind]) counts.closure += 1;
307
+ else counts.other += 1;
308
+
309
+ if (participants.indexOf(m.from) === -1) participants.push(m.from);
310
+ if (m.createdAtMs > updatedAtMs) updatedAtMs = m.createdAtMs;
311
+ }
312
+
313
+ var thread = {
314
+ id: rootMessage.id,
315
+ root: rootMessage,
316
+ replies: replies,
317
+ replyCount: replies.length,
318
+ counts: counts,
319
+ participants: participants,
320
+ broadcast: rootMessage.to.indexOf("*") !== -1,
321
+ openedAtMs: rootMessage.createdAtMs,
322
+ updatedAtMs: updatedAtMs,
323
+ expired: isExpired(rootMessage, now),
324
+ };
325
+ thread.status = threadStatus(thread, now);
326
+ return thread;
327
+ }
328
+
329
+ /**
330
+ * Máquina de estados del hilo (criterio 4):
331
+ * pending → accepted | declined | partial → completed
332
+ *
333
+ * `completed` exige una señal de cierre (el que pidió responde STATUS /
334
+ * ANSWER / NOTICE en el hilo) o que el pedido haya vencido (TTL del bus):
335
+ * no se marca completado solo porque alguien dijo "acepto".
336
+ */
337
+ function threadStatus(thread, now) {
338
+ var replies = thread.replies || [];
339
+ var has = function (kind) {
340
+ for (var i = 0; i < replies.length; i += 1) if (replies[i].kind === kind) return true;
341
+ return false;
342
+ };
343
+ var anyResponse = has("HELP_ACCEPT") || has("HELP_PARTIAL") || has("HELP_DECLINE");
344
+ var closed = false;
345
+ for (var i = 0; i < replies.length; i += 1) {
346
+ if (CLOSURE_SET[replies[i].kind]) {
347
+ closed = true;
348
+ break;
349
+ }
350
+ }
351
+ var expired = Number.isFinite(now) ? now > thread.root.expiresAtMs : isExpired(thread.root);
352
+
353
+ if (closed || (anyResponse && expired)) return "completed";
354
+ if (has("HELP_ACCEPT")) return "accepted";
355
+ if (has("HELP_PARTIAL")) return "partial";
356
+ if (has("HELP_DECLINE")) return "declined";
357
+ return "pending";
358
+ }
359
+
360
+ function statusLabel(status) {
361
+ return STATUS_LABELS[status] || status;
362
+ }
363
+
364
+ function kindLabel(kind) {
365
+ return KIND_LABELS[kind] || String(kind || "");
366
+ }
367
+
368
+ // ------------------------------------------------------------- búsqueda
369
+
370
+ function threadSearchText(thread) {
371
+ var parts = [thread.root.subject, thread.root.from, bodyText(thread.root.body)];
372
+ for (var i = 0; i < thread.replies.length; i += 1) {
373
+ parts.push(thread.replies[i].from, thread.replies[i].subject, bodyText(thread.replies[i].body));
374
+ }
375
+ return fold(parts.join(" \u00b7 "));
376
+ }
377
+
378
+ function filterThreads(threads, filters) {
379
+ var f = filters || {};
380
+ var status = str(f.status) || "all";
381
+ var agent = str(f.agent);
382
+ var query = fold(str(f.query));
383
+ var onlyPending = f.onlyPending === true;
384
+
385
+ return threads.filter(function (thread) {
386
+ if (status !== "all" && thread.status !== status) return false;
387
+ if (agent && thread.participants.indexOf(agent) === -1) return false;
388
+ if (onlyPending && thread.status !== "pending") return false;
389
+ if (query && threadSearchText(thread).indexOf(query) === -1) return false;
390
+ return true;
391
+ });
392
+ }
393
+
394
+ function statusCounts(threads) {
395
+ var counts = { all: threads.length, pending: 0, accepted: 0, declined: 0, partial: 0, completed: 0 };
396
+ for (var i = 0; i < threads.length; i += 1) {
397
+ var status = threads[i].status;
398
+ if (counts[status] === undefined) counts[status] = 0;
399
+ counts[status] += 1;
400
+ }
401
+ return counts;
402
+ }
403
+
404
+ /**
405
+ * Mensajes nuevos respecto de un snapshot previo (criterio 6).
406
+ * Devuelve solo lo que no se había visto, con el hilo al que pertenece y si
407
+ * ese hilo está abierto, para que la ARIA live region anuncie únicamente
408
+ * novedades de hilos visibles.
409
+ */
410
+ function newMessages(previousIds, threads, options) {
411
+ var opts = options || {};
412
+ var seen = previousIds || Object.create(null);
413
+ var openThreads = opts.openThreads || null;
414
+ var out = [];
415
+
416
+ for (var i = 0; i < threads.length; i += 1) {
417
+ var thread = threads[i];
418
+ var threadOpen = openThreads ? openThreads[thread.id] === true : true;
419
+ var items = [thread.root].concat(thread.replies);
420
+ for (var j = 0; j < items.length; j += 1) {
421
+ var m = items[j];
422
+ if (seen[m.id]) continue;
423
+ if (opts.onlyOpen && !threadOpen) continue;
424
+ out.push({
425
+ threadId: thread.id,
426
+ message: m,
427
+ isRoot: m.id === thread.id,
428
+ threadOpen: threadOpen,
429
+ status: thread.status,
430
+ });
431
+ }
432
+ }
433
+ return out.sort(function (a, b) {
434
+ return a.message.createdAtMs - b.message.createdAtMs;
435
+ });
436
+ }
437
+
438
+ function announcementFor(entry) {
439
+ var m = entry.message;
440
+ var who = m.from;
441
+ if (entry.isRoot) {
442
+ return "Nuevo pedido de ayuda de " + who + ": " + shortText(bodyText(m.body), 90);
443
+ }
444
+ return kindLabel(m.kind) + " de " + who + " en hilo de " + (entry.subject || m.subject || "ayuda") +
445
+ ": " + shortText(bodyText(m.body), 90);
446
+ }
447
+
448
+ function idsMap(messages) {
449
+ var map = Object.create(null);
450
+ for (var i = 0; i < messages.length; i += 1) map[messages[i].id] = true;
451
+ return map;
452
+ }
453
+
454
+ // -------------------------------------------------------------- formato
455
+
456
+ function relativeTime(iso, now) {
457
+ var t = parseTime(iso);
458
+ if (!Number.isFinite(t)) return "\u2014";
459
+ var ref = Number.isFinite(now) ? now : Date.now();
460
+ var diff = ref - t;
461
+ if (diff < 0) {
462
+ var ahead = Math.round(-diff / 1000);
463
+ return "en " + ahead + "s";
464
+ }
465
+ var sec = Math.floor(diff / 1000);
466
+ if (sec < 5) return "ahora";
467
+ if (sec < 60) return "hace " + sec + "s";
468
+ var min = Math.floor(sec / 60);
469
+ if (min < 60) return "hace " + min + "m";
470
+ var hour = Math.floor(min / 60);
471
+ if (hour < 24) return "hace " + hour + "h";
472
+ var day = Math.floor(hour / 24);
473
+ if (day < 30) return "hace " + day + "d";
474
+ var month = Math.floor(day / 30);
475
+ if (month < 12) return "hace " + month + "mes";
476
+ return "hace " + Math.floor(month / 12) + "a";
477
+ }
478
+
479
+ function pad2(n) {
480
+ return n < 10 ? "0" + n : String(n);
481
+ }
482
+
483
+ /** Hora local HH:MM:SS. */
484
+ function formatClock(iso) {
485
+ var t = parseTime(iso);
486
+ if (!Number.isFinite(t)) return "--:--:--";
487
+ var d = new Date(t);
488
+ return pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
489
+ }
490
+
491
+ function formatDateTime(iso) {
492
+ var t = parseTime(iso);
493
+ if (!Number.isFinite(t)) return "\u2014";
494
+ var d = new Date(t);
495
+ return pad2(d.getDate()) + "/" + pad2(d.getMonth() + 1) + "/" + d.getFullYear() +
496
+ " " + pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
497
+ }
498
+
499
+ function shortText(text, max) {
500
+ var limit = Number.isFinite(max) ? max : 120;
501
+ var flat = String(text == null ? "" : text).replace(/\s+/g, " ").trim();
502
+ if (flat.length <= limit) return flat;
503
+ return flat.slice(0, Math.max(0, limit - 1)).trimEnd() + "\u2026";
504
+ }
505
+
506
+ // -------------------------------------------------------------- agentes
507
+
508
+ /** Iniciales para el avatar: "e-testing-api-tester" → "TA", "API Tester" → "AT". */
509
+ function initials(name) {
510
+ var raw = String(name == null ? "" : name).replace(/^e-/, "").trim();
511
+ if (!raw) return "?";
512
+ var words = raw.split(/[-_\s.@/]+/).filter(function (w) {
513
+ return w.length > 0;
514
+ });
515
+ if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
516
+ return (words[0].charAt(0) + words[words.length - 1].charAt(0)).toUpperCase();
517
+ }
518
+
519
+ /** Hue estable por agente (0-359) para colorear avatares sin lista fija. */
520
+ function agentHue(agentId) {
521
+ var text = String(agentId == null ? "" : agentId);
522
+ var hash = 0;
523
+ for (var i = 0; i < text.length; i += 1) {
524
+ hash = (hash * 31 + text.charCodeAt(i)) % 360;
525
+ }
526
+ return hash;
527
+ }
528
+
529
+ // ---------------------------------------------------------------- cuerpo
530
+
531
+ function valueToText(value) {
532
+ if (value == null) return "";
533
+ if (typeof value === "string") return value;
534
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
535
+ if (Array.isArray(value)) {
536
+ return value.map(function (item) {
537
+ return typeof item === "string" ? item : JSON.stringify(item);
538
+ }).join("\n");
539
+ }
540
+ try {
541
+ return JSON.stringify(value, null, 2);
542
+ } catch (err) {
543
+ return String(value);
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Convierte el body del mensaje en pares legibles y ordenados.
549
+ * Las claves conocidas van primero (need, why_me, ...) y el resto alfabético,
550
+ * para que un cuerpo con campos nuevos no se pierda ni desordene la lectura.
551
+ */
552
+ function describeBody(body) {
553
+ if (!isObject(body)) return [];
554
+ var keys = Object.keys(body);
555
+ var known = BODY_ORDER.filter(function (key) {
556
+ return Object.prototype.hasOwnProperty.call(body, key);
557
+ });
558
+ var rest = keys.filter(function (key) {
559
+ return BODY_ORDER.indexOf(key) === -1;
560
+ }).sort();
561
+ var ordered = known.concat(rest);
562
+ var out = [];
563
+ for (var i = 0; i < ordered.length; i += 1) {
564
+ var key = ordered[i];
565
+ var text = valueToText(body[key]);
566
+ if (!text) continue;
567
+ out.push({ key: key, label: BODY_LABELS[key] || key, text: text });
568
+ }
569
+ return out;
570
+ }
571
+
572
+ function bodyText(body) {
573
+ var parts = describeBody(body).map(function (part) {
574
+ return part.label + ": " + part.text;
575
+ });
576
+ return parts.join(" \u00b7 ");
577
+ }
578
+
579
+ /** Clave que identifica el contenido (para no re-renderizar de más). */
580
+ function threadSignature(thread) {
581
+ return thread.id + "|" + thread.status + "|" + thread.replyCount + "|" +
582
+ thread.replies.map(function (m) {
583
+ return m.id;
584
+ }).join(",");
585
+ }
586
+
587
+ function snapshotSignature(threads) {
588
+ return threads.map(threadSignature).join(";");
589
+ }
590
+
591
+ // ------------------------------------------------------ snapshot de datos
592
+
593
+ /**
594
+ * Normaliza cualquier snapshot tolerando formatos: `{messages:[...]}`,
595
+ * `[...]` suelto, o un mensaje único. Devuelve avisos en vez de explotar:
596
+ * un archivo roto no puede tumbar el panel.
597
+ */
598
+ function readSnapshot(payload) {
599
+ var result = { messages: [], agents: {}, cards: {}, acks: {}, meta: {}, warnings: [] };
600
+ if (payload == null) {
601
+ result.warnings.push("snapshot vacío");
602
+ return result;
603
+ }
604
+
605
+ var container = payload;
606
+ if (Array.isArray(payload)) {
607
+ result.messages = payload;
608
+ } else if (isObject(payload)) {
609
+ if (Array.isArray(payload.messages)) {
610
+ result.messages = payload.messages;
611
+ } else if (isObject(payload.messages)) {
612
+ result.messages = Object.keys(payload.messages).map(function (key) {
613
+ return payload.messages[key];
614
+ });
615
+ } else if (payload.id && payload.kind) {
616
+ result.messages = [payload];
617
+ } else if (Array.isArray(payload.files)) {
618
+ for (var i = 0; i < payload.files.length; i += 1) {
619
+ var file = payload.files[i];
620
+ if (isObject(file) && file.message) result.messages.push(file.message);
621
+ else if (isObject(file)) result.messages.push(file);
622
+ }
623
+ } else {
624
+ result.warnings.push("el snapshot no tiene mensajes");
625
+ }
626
+
627
+ if (isObject(payload.agents)) result.agents = payload.agents;
628
+ if (isObject(payload.cards)) result.cards = payload.cards;
629
+ if (isObject(payload.acks)) result.acks = payload.acks;
630
+
631
+ var meta = {};
632
+ var metaKeys = ["generatedAt", "channels", "run", "objective", "status", "root", "ttlSec"];
633
+ for (var k = 0; k < metaKeys.length; k += 1) {
634
+ if (payload[metaKeys[k]] !== undefined) meta[metaKeys[k]] = payload[metaKeys[k]];
635
+ }
636
+ result.meta = meta;
637
+ } else {
638
+ result.warnings.push("snapshot con formato desconocido");
639
+ return result;
640
+ }
641
+
642
+ var valid = [];
643
+ for (var m = 0; m < result.messages.length; m += 1) {
644
+ var normalized = normalizeMessage(result.messages[m], result.messages[m] && result.messages[m].channel);
645
+ if (normalized) valid.push(normalized);
646
+ else result.warnings.push("mensaje descartado (sin id o createdAt válido)");
647
+ }
648
+ result.messages = dedupeMessages(valid);
649
+ return result;
650
+ }
651
+
652
+ function ackedBy(acks, messageId) {
653
+ var list = acks && acks[messageId];
654
+ if (!Array.isArray(list)) return [];
655
+ return list.map(function (entry) {
656
+ return typeof entry === "string" ? entry : entry && entry.agentId;
657
+ }).filter(Boolean);
658
+ }
659
+
660
+ var api = {
661
+ RESPONSE_KINDS: RESPONSE_KINDS,
662
+ CLOSURE_KINDS: CLOSURE_KINDS,
663
+ STATUS_LABELS: STATUS_LABELS,
664
+ KIND_LABELS: KIND_LABELS,
665
+ BODY_LABELS: BODY_LABELS,
666
+ DEFAULT_TTL_SEC: DEFAULT_TTL_SEC,
667
+ normalizeMessage: normalizeMessage,
668
+ isBroadcastHelpRequest: isBroadcastHelpRequest,
669
+ isDebateRoot: isDebateRoot,
670
+ isExpired: isExpired,
671
+ dedupeMessages: dedupeMessages,
672
+ buildThreads: buildThreads,
673
+ threadStatus: threadStatus,
674
+ statusLabel: statusLabel,
675
+ kindLabel: kindLabel,
676
+ filterThreads: filterThreads,
677
+ statusCounts: statusCounts,
678
+ newMessages: newMessages,
679
+ announcementFor: announcementFor,
680
+ idsMap: idsMap,
681
+ relativeTime: relativeTime,
682
+ formatClock: formatClock,
683
+ formatDateTime: formatDateTime,
684
+ shortText: shortText,
685
+ initials: initials,
686
+ agentHue: agentHue,
687
+ describeBody: describeBody,
688
+ bodyText: bodyText,
689
+ threadSignature: threadSignature,
690
+ snapshotSignature: snapshotSignature,
691
+ readSnapshot: readSnapshot,
692
+ ackedBy: ackedBy,
693
+ escapeHtml: escapeHtml,
694
+ threadSearchText: threadSearchText,
695
+ };
696
+
697
+ root.DebateCore = api;
698
+ })(typeof globalThis !== "undefined" ? globalThis : this);