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.
- package/README.md +1 -1
- package/commands/oficina.md +7 -6
- package/dashboard/.dom-check.html +1257 -0
- package/dashboard/README-DASHBOARD.md +64 -0
- package/dashboard/assets/factory.css +1543 -0
- package/dashboard/assets/tokens.css +208 -0
- package/dashboard/build-kanban-data.mjs +279 -0
- package/dashboard/dashboard.css +753 -0
- package/dashboard/dashboard.html +121 -0
- package/dashboard/dashboard.js +610 -0
- package/dashboard/data/snapshot.json +883 -0
- package/dashboard/debate.html +134 -0
- package/dashboard/factory/build-snapshot.mjs +89 -0
- package/dashboard/factory/factory-app.js +1185 -0
- package/dashboard/factory/factory-core.js +541 -0
- package/dashboard/factory/snapshot.js +850 -0
- package/dashboard/factory.html +350 -0
- package/dashboard/kanban-data.json +671 -0
- package/dashboard/kanban-source.js +643 -0
- package/dashboard/kanban.check.mjs +454 -0
- package/dashboard/kanban.html +1257 -0
- package/dashboard/leadership.html +1008 -0
- package/dashboard/lib/debate-app.js +1149 -0
- package/dashboard/lib/debate-core.js +698 -0
- package/dashboard/lib/debate-data.js +163 -0
- package/dashboard/lib/debate-demo.js +110 -0
- package/dashboard/lib/debate.css +1155 -0
- package/dashboard/lib/leadership-model.js +946 -0
- package/dashboard/probe.mjs +35 -0
- package/dashboard/scripts/serve.mjs +122 -0
- package/dashboard/scripts/snapshot-data.mjs +93 -0
- package/dashboard/scripts/snapshot.mjs +28 -0
- package/dashboard/server.mjs +242 -0
- package/dashboard/shot.mjs +54 -0
- package/dashboard/shots/desktop-gates.png +0 -0
- package/dashboard/shots/desktop-light.png +0 -0
- package/dashboard/shots/desktop-real.png +0 -0
- package/dashboard/shots/mobile.png +0 -0
- package/dashboard/test/factory-core.test.mjs +244 -0
- package/dashboard/tests/debate-core.test.mjs +336 -0
- package/dashboard/tests/leadership-model.test.mjs +404 -0
- package/dist/cli.js +24 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/dashboard.d.ts +33 -0
- package/dist/core/dashboard.d.ts.map +1 -0
- package/dist/core/dashboard.js +115 -0
- package/dist/core/dashboard.js.map +1 -0
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +11 -0
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/environment.d.ts +28 -0
- package/dist/core/environment.d.ts.map +1 -0
- package/dist/core/environment.js +80 -0
- package/dist/core/environment.js.map +1 -0
- package/dist/core/systemPrompt.d.ts.map +1 -1
- package/dist/core/systemPrompt.js +3 -0
- package/dist/core/systemPrompt.js.map +1 -1
- package/dist/tools/index.d.ts +9 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +26 -22
- package/dist/tools/index.js.bak +972 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/v2.d.ts +5 -0
- package/dist/v2.d.ts.map +1 -1
- package/dist/v2.js +57 -5
- package/dist/v2.js.bak +517 -0
- package/dist/v2.js.map +1 -1
- package/docs/QUE-ES.md +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* leadership-model.js — modelo de decisiones para el panel del jefe.
|
|
3
|
+
*
|
|
4
|
+
* Se carga de dos formas:
|
|
5
|
+
* 1. <script src="./lib/leadership-model.js"> (browser, file:// incluido)
|
|
6
|
+
* 2. import "../lib/leadership-model.js" (node --test, side-effect)
|
|
7
|
+
*
|
|
8
|
+
* No toca el DOM y no hace I/O: recibe un snapshot y devuelve decisiones
|
|
9
|
+
* normalizadas. Todo lo que se dibuja en leadership.html sale de aca, asi que
|
|
10
|
+
* cada regla se puede testear sin navegador.
|
|
11
|
+
*/
|
|
12
|
+
(function (root) {
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// Coordinadores (las tres secciones del panel)
|
|
17
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/** @type {ReadonlyArray<{id:string,label:string,short:string,scope:string,tone:string}>} */
|
|
20
|
+
var COORDINATORS = [
|
|
21
|
+
{
|
|
22
|
+
id: "commander",
|
|
23
|
+
label: "Commander",
|
|
24
|
+
short: "CMD",
|
|
25
|
+
scope: "scope · prioridades · escalations",
|
|
26
|
+
tone: "commander",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "integrator",
|
|
30
|
+
label: "Integrator",
|
|
31
|
+
short: "INT",
|
|
32
|
+
scope: "routing · leases · gates · deadlocks",
|
|
33
|
+
tone: "integrator",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "architect",
|
|
37
|
+
label: "Architect",
|
|
38
|
+
short: "ARC",
|
|
39
|
+
scope: "tech decisions · patterns · contracts",
|
|
40
|
+
tone: "architect",
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
var COORDINATOR_IDS = COORDINATORS.map(function (c) { return c.id; });
|
|
45
|
+
|
|
46
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
47
|
+
// Tipos de decision (el vocabulario de cada seccion)
|
|
48
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* El orden importa: gana el primer patron que matchea.
|
|
52
|
+
* Mas especifico primero (deadlock antes que gate, gate antes que routing).
|
|
53
|
+
*/
|
|
54
|
+
var TYPE_RULES = [
|
|
55
|
+
{
|
|
56
|
+
type: "escalation",
|
|
57
|
+
coordinator: "commander",
|
|
58
|
+
re: /\bescalat|\bescalad|\bblocker\b|\bbloque[oa]|\bhalt\b|\bfreno\b|\bhuman[oa]?\b/i,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: "deadlock",
|
|
62
|
+
coordinator: "integrator",
|
|
63
|
+
re: /\bdeadlock\b|interbloqueo|\btrabad|\bstall(ed)?\b|\bcicl(o|a) de espera\b/i,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
type: "lease",
|
|
67
|
+
coordinator: "integrator",
|
|
68
|
+
re: /\blease\b|\bleasing\b|arrend|heartbeat|\bclaim\b|reclamo|\bttl\b/i,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: "gate",
|
|
72
|
+
coordinator: "integrator",
|
|
73
|
+
re: /\bgate\b|\bgates\b|quality gate|calidad|\brework\b|\bretry\b|\brechaz|rechaz|\breview\b|revisi[oó]n|\bfail(ed)?\b|\bnot_run\b/i,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: "routing",
|
|
77
|
+
coordinator: "integrator",
|
|
78
|
+
re: /\brouting\b|ruteo|asignaci[oó]n|asignad|\bowner\b|\breviewer\b|\bmatch\b|cobertura|capacit/,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: "spawn",
|
|
82
|
+
coordinator: "integrator",
|
|
83
|
+
re: /\bspawn\b|\bfactory\b|f[aá]brica|materializ|agente nuevo/i,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: "priority",
|
|
87
|
+
coordinator: "commander",
|
|
88
|
+
re: /prioridad|\bpriority\b|\borden\b|\bP[0-3]\b|\burgente\b|\bcritical\b/i,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: "scope",
|
|
92
|
+
coordinator: "commander",
|
|
93
|
+
re: /\bscope\b|alcance|objetiv|\bplan(ning)?\b|replan|descomposici|desglose/i,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: "contract",
|
|
97
|
+
coordinator: "architect",
|
|
98
|
+
re: /\bcontract\b|contrato|\binterface\b|\bschema\b|\bAPI\b|\bDTO\b|\btypes?\b|shape/i,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: "pattern",
|
|
102
|
+
coordinator: "architect",
|
|
103
|
+
re: /\bpattern\b|patr[oó]n|arquitect|architect|\bdesign\b|dise[nñ]o|\bADR\b|\bstack\b/i,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: "tech_decision",
|
|
107
|
+
coordinator: "architect",
|
|
108
|
+
re: /\bt[eé]cnic|\btech\b|implementaci|decisi[oó]n|\bdecid|\bcriterio\b/i,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
var TYPE_LABELS = {
|
|
113
|
+
escalation: "Escalation",
|
|
114
|
+
deadlock: "Deadlock",
|
|
115
|
+
lease: "Lease",
|
|
116
|
+
gate: "Gate / rework",
|
|
117
|
+
routing: "Routing",
|
|
118
|
+
spawn: "Spawn / factory",
|
|
119
|
+
priority: "Prioridad",
|
|
120
|
+
scope: "Scope",
|
|
121
|
+
contract: "Contract",
|
|
122
|
+
pattern: "Pattern",
|
|
123
|
+
tech_decision: "Tech decision",
|
|
124
|
+
unknown: "General",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
var TYPE_TO_COORDINATOR = (function () {
|
|
128
|
+
var map = { unknown: "integrator" };
|
|
129
|
+
TYPE_RULES.forEach(function (rule) { map[rule.type] = rule.coordinator; });
|
|
130
|
+
return map;
|
|
131
|
+
})();
|
|
132
|
+
|
|
133
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
134
|
+
// Actores
|
|
135
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
var ACTOR_RULES = [
|
|
138
|
+
{ re: /^(oficina-)?(director|commander|jefe|comandante)\b/i, coordinator: "commander" },
|
|
139
|
+
{ re: /^(oficina-)?(integrator|integrador|dispatcher|flow)\b/i, coordinator: "integrator" },
|
|
140
|
+
{ re: /^(oficina-)?(architect|arquitecto|arch|critic|critico)\b/i, coordinator: "architect" },
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
144
|
+
// Helpers
|
|
145
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
function isObj(v) {
|
|
148
|
+
return v !== null && typeof v === "object";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function str(v) {
|
|
152
|
+
return typeof v === "string" ? v : v === null || v === undefined ? "" : String(v);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function clip(text, max) {
|
|
156
|
+
var s = str(text).replace(/\s+/g, " ").trim();
|
|
157
|
+
if (!max || s.length <= max) return s;
|
|
158
|
+
return s.slice(0, max - 1).replace(/[\s,;:.]+$/, "") + "…";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function firstNonEmpty() {
|
|
162
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
163
|
+
var v = arguments[i];
|
|
164
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
165
|
+
}
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
var CARD_ID_RE = /\b([A-Z][A-Z0-9]{0,7}-\d{1,4})\b/;
|
|
170
|
+
|
|
171
|
+
/** Busca un id de card tipo T-06 en cualquier texto del mensaje. */
|
|
172
|
+
function inferCardId(msg) {
|
|
173
|
+
if (!isObj(msg)) return null;
|
|
174
|
+
var body = isObj(msg.body) ? msg.body : {};
|
|
175
|
+
var direct = firstNonEmpty(body.card, body.cardId, body.card_id, msg.cardId);
|
|
176
|
+
if (direct) return direct.toUpperCase();
|
|
177
|
+
var hay = [msg.subject, body.text, body.summary, body.reason, body.title]
|
|
178
|
+
.map(str)
|
|
179
|
+
.join(" ");
|
|
180
|
+
var m = CARD_ID_RE.exec(hay);
|
|
181
|
+
return m ? m[1] : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Deduce el coordinador que emitio una decision.
|
|
186
|
+
* Prioridad: campo explicito > actor > tipo inferido.
|
|
187
|
+
*/
|
|
188
|
+
function classifyActor(actor) {
|
|
189
|
+
var a = str(actor).trim();
|
|
190
|
+
if (!a) return null;
|
|
191
|
+
for (var i = 0; i < ACTOR_RULES.length; i++) {
|
|
192
|
+
if (ACTOR_RULES[i].re.test(a)) return ACTOR_RULES[i].coordinator;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function inferType(text) {
|
|
198
|
+
var hay = str(text);
|
|
199
|
+
if (!hay.trim()) return null;
|
|
200
|
+
for (var i = 0; i < TYPE_RULES.length; i++) {
|
|
201
|
+
if (TYPE_RULES[i].re.test(hay)) return TYPE_RULES[i].type;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* El campo `decision` de state.json es la transicion que ejecuto la maquina
|
|
208
|
+
* de estados: es autoritativo sobre el texto libre del `reason`.
|
|
209
|
+
* `approved` no dice nada del tema, asi que ahi manda el texto.
|
|
210
|
+
*/
|
|
211
|
+
var FIELD_CLASSIFICATION = {
|
|
212
|
+
escalated: { coordinator: "commander", type: "escalation" },
|
|
213
|
+
replan: { coordinator: "architect", type: "pattern" },
|
|
214
|
+
spawn_approved: { coordinator: "integrator", type: "spawn" },
|
|
215
|
+
spawn_rejected: { coordinator: "integrator", type: "spawn" },
|
|
216
|
+
rework: { coordinator: "integrator", type: "gate" },
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Clasifica una decision en {coordinator, type}.
|
|
221
|
+
*
|
|
222
|
+
* Prioridad: campo explicito > transicion de state.json > texto > actor.
|
|
223
|
+
*
|
|
224
|
+
* @param {{decision?:string, reason?:string, actor?:string, type?:string, coordinator?:string}} input
|
|
225
|
+
*/
|
|
226
|
+
function classifyDecision(input) {
|
|
227
|
+
var i = isObj(input) ? input : {};
|
|
228
|
+
var explicitType = str(i.type).trim();
|
|
229
|
+
var explicitCoord = str(i.coordinator).trim();
|
|
230
|
+
var decisionField = str(i.decision).trim().toLowerCase();
|
|
231
|
+
|
|
232
|
+
// Texto (reason + decision + subject) define el tema fino.
|
|
233
|
+
var hay = [i.reason, i.subject, i.text].map(str).join(" ");
|
|
234
|
+
var byText = inferType(hay);
|
|
235
|
+
var byField = FIELD_CLASSIFICATION[decisionField] || null;
|
|
236
|
+
|
|
237
|
+
var type;
|
|
238
|
+
var coordinator;
|
|
239
|
+
|
|
240
|
+
if (byField) {
|
|
241
|
+
type = byField.type;
|
|
242
|
+
coordinator = byField.coordinator;
|
|
243
|
+
// Refinamiento dentro de la misma seccion: un rework cuyo texto dice
|
|
244
|
+
// "deadlock" se muestra como deadlock, pero no cambia de seccion.
|
|
245
|
+
if (byText && TYPE_TO_COORDINATOR[byText] === coordinator) type = byText;
|
|
246
|
+
} else {
|
|
247
|
+
type = byText || "unknown";
|
|
248
|
+
coordinator =
|
|
249
|
+
TYPE_TO_COORDINATOR[type] ||
|
|
250
|
+
classifyActor(i.actor) ||
|
|
251
|
+
"integrator";
|
|
252
|
+
// approved sin tema reconocible: la firma del actor decide la seccion.
|
|
253
|
+
if (type === "unknown") coordinator = classifyActor(i.actor) || "integrator";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (explicitType) type = explicitType;
|
|
257
|
+
if (explicitCoord) coordinator = explicitCoord;
|
|
258
|
+
if (COORDINATOR_IDS.indexOf(coordinator) === -1) coordinator = "integrator";
|
|
259
|
+
if (!TYPE_LABELS[type]) type = "unknown";
|
|
260
|
+
|
|
261
|
+
return { coordinator: coordinator, type: type, typeLabel: TYPE_LABELS[type] || TYPE_LABELS.unknown };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
265
|
+
// Siguiente accion
|
|
266
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
var CARD_STATE_ES = {
|
|
269
|
+
todo: "todo",
|
|
270
|
+
in_progress: "in progress",
|
|
271
|
+
review: "review",
|
|
272
|
+
done: "done",
|
|
273
|
+
rework: "rework",
|
|
274
|
+
blocked: "blocked",
|
|
275
|
+
escalated: "escalated",
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Deriva la proxima accion concreta. No inventa estado: se apoya en la card.
|
|
280
|
+
*
|
|
281
|
+
* @param {{type:string, coordinator:string, raw:string, actor:string}} item
|
|
282
|
+
* @param {object|null} card
|
|
283
|
+
*/
|
|
284
|
+
function deriveNextAction(item, card) {
|
|
285
|
+
var c = isObj(card) ? card : null;
|
|
286
|
+
var owner = c && c.owner ? c.owner : null;
|
|
287
|
+
var ownerTxt = owner ? "`" + owner + "`" : "el owner";
|
|
288
|
+
var state = c ? str(c.state) : "";
|
|
289
|
+
var raw = str(item && item.raw).toLowerCase();
|
|
290
|
+
var type = item && item.type ? item.type : "unknown";
|
|
291
|
+
|
|
292
|
+
if (c && state === "done" && /approved/.test(raw)) return "Nada: la card ya cerro. Registrar como precedente.";
|
|
293
|
+
|
|
294
|
+
switch (type) {
|
|
295
|
+
case "escalation":
|
|
296
|
+
return "Esperando decision del usuario: elegir una opcion de la escalada y desbloquear la card.";
|
|
297
|
+
case "deadlock":
|
|
298
|
+
return "Romper el ciclo: liberar el lease vencido y reasignar owner antes de reintentar.";
|
|
299
|
+
case "lease":
|
|
300
|
+
return "Renovar el lease antes del TTL; si el agente no da heartbeat, liberarlo.";
|
|
301
|
+
case "gate":
|
|
302
|
+
if (state === "rework") return ownerTxt + " rehace contra review.md y vuelve a correr los gates.";
|
|
303
|
+
if (state === "review") return "Revisor aprueba y despues se corren los gates de la card.";
|
|
304
|
+
return "Correr los gates de la card y registrar la evidencia.";
|
|
305
|
+
case "routing":
|
|
306
|
+
if (state === "todo" || state === "in_progress") {
|
|
307
|
+
return owner ? "Esperar claim y heartbeat de " + ownerTxt + "." : "Asignar owner: no hay agente disponible en el catalogo.";
|
|
308
|
+
}
|
|
309
|
+
if (state === "done") return "Ruta cerrada: no requiere accion.";
|
|
310
|
+
return "Revisar que owner/reviewer sigan vivos antes de avanzar.";
|
|
311
|
+
case "spawn":
|
|
312
|
+
if (/reject/.test(raw)) return "Buscar cobertura alternativa o reenviar SPAWN_REQUEST con mejor justificacion.";
|
|
313
|
+
return "La factory materializa el agente y lo suma al catalogo para el proximo match.";
|
|
314
|
+
case "priority":
|
|
315
|
+
return "Reordenar la cola de cards segun la nueva prioridad.";
|
|
316
|
+
case "scope":
|
|
317
|
+
return "Actualizar el plan (plan.json) y re-scopear la card afectada.";
|
|
318
|
+
case "contract":
|
|
319
|
+
return "Fijar el contrato en el handoff y consumirlo desde el lado que lo implementa.";
|
|
320
|
+
case "pattern":
|
|
321
|
+
return "Aplicar el patron en la implementacion y anotarlo en context.decisions.";
|
|
322
|
+
case "tech_decision":
|
|
323
|
+
return "Implementar segun el criterio y registrarlo para las cards que vengan.";
|
|
324
|
+
default:
|
|
325
|
+
return "Revisar la card y sus artifacts para decidir el proximo paso.";
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
330
|
+
// Links
|
|
331
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
var ARTIFACTS_ROOT = "../.oficina/artifacts";
|
|
334
|
+
|
|
335
|
+
/** Devuelve todos los enlaces de una card. */
|
|
336
|
+
function cardLinks(cardId) {
|
|
337
|
+
if (!cardId) {
|
|
338
|
+
return { card: null, kanban: null, artifacts: null, handoff: null, evidence: null, gates: null };
|
|
339
|
+
}
|
|
340
|
+
var dir = ARTIFACTS_ROOT + "/" + encodeURIComponent(cardId) + "/";
|
|
341
|
+
return {
|
|
342
|
+
card: "#card-" + cardId,
|
|
343
|
+
kanban: "kanban.html#" + encodeURIComponent(cardId),
|
|
344
|
+
artifacts: dir,
|
|
345
|
+
handoff: dir + "handoff.md",
|
|
346
|
+
evidence: dir + "evidence.md",
|
|
347
|
+
gates: dir + "gates.md",
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
352
|
+
// Normalizacion
|
|
353
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
var DECISION_ES = {
|
|
356
|
+
approved: "Aprobado",
|
|
357
|
+
rework: "Rework",
|
|
358
|
+
replan: "Replan",
|
|
359
|
+
escalated: "Escalado",
|
|
360
|
+
spawn_approved: "Spawn aprobado",
|
|
361
|
+
spawn_rejected: "Spawn rechazado",
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
function actorLabel(actorId, roster) {
|
|
365
|
+
var id = str(actorId);
|
|
366
|
+
if (roster && isObj(roster.agents) && roster.agents[id] && roster.agents[id].displayName) {
|
|
367
|
+
return roster.agents[id].displayName;
|
|
368
|
+
}
|
|
369
|
+
if (!id) return "oficina";
|
|
370
|
+
return id;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Convierte un DecisionRecord de state.json en una decision de UI.
|
|
375
|
+
*
|
|
376
|
+
* @param {object} rec
|
|
377
|
+
* @param {{cards:object, roster:object, escalations:object[], artifacts:object, now:number}} ctx
|
|
378
|
+
*/
|
|
379
|
+
function normalizeDecisionRecord(rec, ctx) {
|
|
380
|
+
var c = ctx || {};
|
|
381
|
+
var cards = isObj(c.cards) ? c.cards : {};
|
|
382
|
+
var cardId = firstNonEmpty(rec && rec.cardId) || null;
|
|
383
|
+
var card = cardId && cards[cardId] ? cards[cardId] : null;
|
|
384
|
+
var rawDecision = str(rec && rec.decision) || "approved";
|
|
385
|
+
var reason = str(rec && rec.reason);
|
|
386
|
+
var cls = classifyDecision({
|
|
387
|
+
decision: rawDecision,
|
|
388
|
+
reason: reason,
|
|
389
|
+
actor: rec && rec.actor,
|
|
390
|
+
type: rec && rec.type,
|
|
391
|
+
coordinator: rec && rec.coordinator,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
var item = {
|
|
395
|
+
id: "state:" + (rec && rec.id ? rec.id : cardId + ":" + (rec && rec.at) + ":" + rawDecision),
|
|
396
|
+
source: "state",
|
|
397
|
+
channel: null,
|
|
398
|
+
at: str(rec && rec.at) || new Date(c.now || Date.now()).toISOString(),
|
|
399
|
+
actorId: str(rec && rec.actor) || "oficina",
|
|
400
|
+
cardId: cardId,
|
|
401
|
+
cardTitle: card ? str(card.title) : "",
|
|
402
|
+
cardState: card ? str(card.state) : "",
|
|
403
|
+
raw: rawDecision,
|
|
404
|
+
decision: (DECISION_ES[rawDecision] || rawDecision) + (cls.type !== "unknown" ? " · " + cls.typeLabel : ""),
|
|
405
|
+
reason: reason || "(sin razon registrada)",
|
|
406
|
+
coordinator: cls.coordinator,
|
|
407
|
+
type: cls.type,
|
|
408
|
+
typeLabel: cls.typeLabel,
|
|
409
|
+
priority: rawDecision === "escalated" ? "critical" : "normal",
|
|
410
|
+
decisionLike: true,
|
|
411
|
+
links: cardLinks(cardId),
|
|
412
|
+
};
|
|
413
|
+
item.nextAction = deriveNextAction(item, card);
|
|
414
|
+
item.actorLabel = actorLabel(item.actorId, c.roster);
|
|
415
|
+
item.actorRole = classifyActor(item.actorId) || "worker";
|
|
416
|
+
return item;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Escalations abiertas de state.json -> decisiones de Commander. */
|
|
420
|
+
function normalizeEscalation(esc, ctx) {
|
|
421
|
+
var c = ctx || {};
|
|
422
|
+
var cards = isObj(c.cards) ? c.cards : {};
|
|
423
|
+
var cardId = firstNonEmpty(esc && esc.cardId) || null;
|
|
424
|
+
var card = cardId && cards[cardId] ? cards[cardId] : null;
|
|
425
|
+
var options = Array.isArray(esc && esc.options) ? esc.options : [];
|
|
426
|
+
var label = options.length ? options[0].label : "sin opciones registradas";
|
|
427
|
+
var reason = firstNonEmpty(esc && esc.cause, "escalation sin causa") +
|
|
428
|
+
(esc && esc.recommendation ? " — recomendacion: " + esc.recommendation : "");
|
|
429
|
+
|
|
430
|
+
var item = {
|
|
431
|
+
id: "esc:" + (esc && esc.id ? esc.id : cardId),
|
|
432
|
+
source: "escalation",
|
|
433
|
+
channel: null,
|
|
434
|
+
at: str(esc && esc.at) || new Date(c.now || Date.now()).toISOString(),
|
|
435
|
+
actorId: "commander",
|
|
436
|
+
actorLabel: "Commander",
|
|
437
|
+
actorRole: "commander",
|
|
438
|
+
cardId: cardId,
|
|
439
|
+
cardTitle: card ? str(card.title) : "",
|
|
440
|
+
cardState: card ? str(card.state) : "",
|
|
441
|
+
raw: "escalated",
|
|
442
|
+
decision: "Escalado · " + clip(label, 60),
|
|
443
|
+
reason: reason,
|
|
444
|
+
coordinator: "commander",
|
|
445
|
+
type: "escalation",
|
|
446
|
+
typeLabel: TYPE_LABELS.escalation,
|
|
447
|
+
priority: "critical",
|
|
448
|
+
decisionLike: true,
|
|
449
|
+
links: cardLinks(cardId),
|
|
450
|
+
options: options.map(function (o) { return { label: str(o && o.label), tradeoff: str(o && o.tradeoff) }; }),
|
|
451
|
+
status: str(esc && esc.status) || "open",
|
|
452
|
+
};
|
|
453
|
+
item.nextAction = "El usuario elige una de las " + (options.length || 0) + " opciones y la card sale de blocked.";
|
|
454
|
+
return item;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
var QUESTION_KINDS = { QUESTION: 1, HELP_QUESTION: 1, HELP_REQUEST: 1 };
|
|
458
|
+
|
|
459
|
+
var CHANNEL_COORDINATOR = {
|
|
460
|
+
"coord-command": "commander",
|
|
461
|
+
"coord-flow": "integrator",
|
|
462
|
+
"coord-arch": "architect",
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Convierte un mensaje del bus en una decision de UI.
|
|
467
|
+
*
|
|
468
|
+
* @param {object} msg
|
|
469
|
+
* @param {{channel?:string, cards:object, roster:object, now:number}} ctx
|
|
470
|
+
*/
|
|
471
|
+
function normalizeBusMessage(msg, ctx) {
|
|
472
|
+
var c = ctx || {};
|
|
473
|
+
var cards = isObj(c.cards) ? c.cards : {};
|
|
474
|
+
var channel = str(c.channel);
|
|
475
|
+
var body = isObj(msg && msg.body) ? msg.body : {};
|
|
476
|
+
var cardId = inferCardId(msg);
|
|
477
|
+
var card = cardId && cards[cardId] ? cards[cardId] : null;
|
|
478
|
+
var kind = str(msg && msg.kind) || "NOTICE";
|
|
479
|
+
var subject = firstNonEmpty(msg && msg.subject, body.title, "(sin asunto)");
|
|
480
|
+
var reason = firstNonEmpty(
|
|
481
|
+
body.reason,
|
|
482
|
+
body.rationale,
|
|
483
|
+
body.summary,
|
|
484
|
+
body.text,
|
|
485
|
+
body.note,
|
|
486
|
+
body.recommendation,
|
|
487
|
+
clip(JSON.stringify(body), 400)
|
|
488
|
+
);
|
|
489
|
+
var cls = classifyDecision({
|
|
490
|
+
decision: body.decision,
|
|
491
|
+
reason: firstNonEmpty(body.reason, body.summary, body.text, subject),
|
|
492
|
+
subject: subject,
|
|
493
|
+
actor: msg && msg.from,
|
|
494
|
+
type: body.type,
|
|
495
|
+
coordinator: body.coordinator || CHANNEL_COORDINATOR[channel] || null,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
var item = {
|
|
499
|
+
id: "msg:" + (msg && msg.id ? msg.id : channel + ":" + (msg && msg.seq)),
|
|
500
|
+
source: "bus",
|
|
501
|
+
channel: channel || null,
|
|
502
|
+
at: str(msg && msg.createdAt) || new Date(c.now || Date.now()).toISOString(),
|
|
503
|
+
actorId: str(msg && msg.from) || "oficina",
|
|
504
|
+
cardId: cardId,
|
|
505
|
+
cardTitle: card ? str(card.title) : "",
|
|
506
|
+
cardState: card ? str(card.state) : "",
|
|
507
|
+
raw: kind.toLowerCase(),
|
|
508
|
+
decision: kind + " · " + clip(subject, 90),
|
|
509
|
+
reason: reason || "(mensaje sin cuerpo)",
|
|
510
|
+
coordinator: cls.coordinator,
|
|
511
|
+
type: cls.type,
|
|
512
|
+
typeLabel: cls.typeLabel,
|
|
513
|
+
priority: str(msg && msg.priority) || "normal",
|
|
514
|
+
decisionLike: !QUESTION_KINDS[kind],
|
|
515
|
+
links: cardLinks(cardId),
|
|
516
|
+
messageId: msg && msg.id ? msg.id : null,
|
|
517
|
+
seq: isObj(msg) && typeof msg.seq === "number" ? msg.seq : null,
|
|
518
|
+
to: Array.isArray(msg && msg.to) ? msg.to.slice() : [],
|
|
519
|
+
};
|
|
520
|
+
item.nextAction = deriveNextAction(item, card);
|
|
521
|
+
item.actorLabel = actorLabel(item.actorId, c.roster);
|
|
522
|
+
item.actorRole = classifyActor(item.actorId) || "worker";
|
|
523
|
+
return item;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Entradas del ledger de contexto (state.context.decisions) -> Architect. */
|
|
527
|
+
function normalizeContextDecision(text, index, ctx) {
|
|
528
|
+
var c = ctx || {};
|
|
529
|
+
var cardId = null;
|
|
530
|
+
var m = CARD_ID_RE.exec(str(text));
|
|
531
|
+
if (m) cardId = m[1];
|
|
532
|
+
var cards = isObj(c.cards) ? c.cards : {};
|
|
533
|
+
var card = cardId && cards[cardId] ? cards[cardId] : null;
|
|
534
|
+
var cls = classifyDecision({ decision: "approved", reason: text, actor: "architect", type: "tech_decision" });
|
|
535
|
+
var item = {
|
|
536
|
+
id: "ctx:" + index,
|
|
537
|
+
source: "context",
|
|
538
|
+
channel: null,
|
|
539
|
+
at: str(c.nowIso) || new Date(c.now || Date.now()).toISOString(),
|
|
540
|
+
actorId: "architect",
|
|
541
|
+
actorLabel: "Architect",
|
|
542
|
+
actorRole: "architect",
|
|
543
|
+
cardId: cardId,
|
|
544
|
+
cardTitle: card ? str(card.title) : "",
|
|
545
|
+
cardState: card ? str(card.state) : "",
|
|
546
|
+
raw: "context.decisions",
|
|
547
|
+
decision: "Tech decision registrada",
|
|
548
|
+
reason: str(text),
|
|
549
|
+
coordinator: "architect",
|
|
550
|
+
type: cls.type,
|
|
551
|
+
typeLabel: cls.typeLabel,
|
|
552
|
+
priority: "normal",
|
|
553
|
+
decisionLike: true,
|
|
554
|
+
links: cardLinks(cardId),
|
|
555
|
+
checkpoint: true,
|
|
556
|
+
};
|
|
557
|
+
item.nextAction = deriveNextAction(item, card);
|
|
558
|
+
return item;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
562
|
+
// Señales: no son decisiones registradas, son condiciones que hoy piden
|
|
563
|
+
// intervencion del jefe. Se marcan como `signal` para no confundirlas con
|
|
564
|
+
// decisiones tomadas.
|
|
565
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
function signalBase(fields) {
|
|
568
|
+
var item = {
|
|
569
|
+
source: "signal",
|
|
570
|
+
channel: null,
|
|
571
|
+
priority: "high",
|
|
572
|
+
decisionLike: false,
|
|
573
|
+
links: cardLinks(fields.cardId || null),
|
|
574
|
+
cardId: fields.cardId || null,
|
|
575
|
+
cardTitle: fields.cardTitle || "",
|
|
576
|
+
cardState: fields.cardState || "",
|
|
577
|
+
actorId: fields.actorId,
|
|
578
|
+
actorLabel: fields.actorLabel,
|
|
579
|
+
actorRole: fields.actorRole,
|
|
580
|
+
coordinator: fields.coordinator,
|
|
581
|
+
type: fields.type,
|
|
582
|
+
typeLabel: TYPE_LABELS[fields.type] || TYPE_LABELS.unknown,
|
|
583
|
+
at: fields.at,
|
|
584
|
+
raw: fields.raw,
|
|
585
|
+
decision: fields.decision,
|
|
586
|
+
reason: fields.reason,
|
|
587
|
+
};
|
|
588
|
+
item.nextAction = deriveNextAction(item, fields.card || null);
|
|
589
|
+
return item;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** state.halt != null -> Commander. */
|
|
593
|
+
function normalizeHalt(halt, ctx) {
|
|
594
|
+
if (!isObj(halt) || !halt.reason) return null;
|
|
595
|
+
return signalBase({
|
|
596
|
+
id: "halt:" + (halt.at || "now"),
|
|
597
|
+
at: str(halt.at) || str(ctx.nowIso),
|
|
598
|
+
actorId: str(halt.by) || "commander",
|
|
599
|
+
actorLabel: (str(halt.by) || "commander") + " · halt",
|
|
600
|
+
actorRole: "commander",
|
|
601
|
+
coordinator: "commander",
|
|
602
|
+
type: "escalation",
|
|
603
|
+
raw: "halt",
|
|
604
|
+
decision: "Freno de emergencia activo",
|
|
605
|
+
reason: str(halt.reason),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Leases vencidos o sin heartbeat -> Integrator. */
|
|
610
|
+
function normalizeLease(lease, ctx) {
|
|
611
|
+
if (!isObj(lease)) return null;
|
|
612
|
+
var expires = Date.parse(str(lease.expiresAt));
|
|
613
|
+
if (!isFinite(expires)) return null;
|
|
614
|
+
var now = typeof ctx.now === "number" ? ctx.now : Date.now();
|
|
615
|
+
if (expires > now) return null; // vigente: no pide intervencion
|
|
616
|
+
var hb = Date.parse(str(lease.heartbeatAt));
|
|
617
|
+
var hbAge = isFinite(hb) ? Math.round((now - hb) / 1000) : null;
|
|
618
|
+
var cardId = str(lease.taskId) || null;
|
|
619
|
+
var cards = isObj(ctx.cards) ? ctx.cards : {};
|
|
620
|
+
var card = cardId && cards[cardId] ? cards[cardId] : null;
|
|
621
|
+
var overdue = Math.round((now - expires) / 1000);
|
|
622
|
+
|
|
623
|
+
return signalBase({
|
|
624
|
+
id: "lease:" + (cardId || "?") + ":" + str(lease.expiresAt),
|
|
625
|
+
at: str(lease.expiresAt),
|
|
626
|
+
actorId: str(lease.agentId) || "integrator",
|
|
627
|
+
actorLabel: str(lease.agentId) || "lease",
|
|
628
|
+
actorRole: "integrator",
|
|
629
|
+
coordinator: "integrator",
|
|
630
|
+
type: "lease",
|
|
631
|
+
raw: "lease_expired",
|
|
632
|
+
decision: "Lease vencido" + (cardId ? " · " + cardId : ""),
|
|
633
|
+
reason:
|
|
634
|
+
"expiró hace " + overdue + "s" +
|
|
635
|
+
(hbAge !== null ? " · último heartbeat hace " + hbAge + "s" : "") +
|
|
636
|
+
" · renovaciones: " + (lease.renewals || 0),
|
|
637
|
+
cardId: cardId,
|
|
638
|
+
card: card,
|
|
639
|
+
cardTitle: card ? str(card.title) : "",
|
|
640
|
+
cardState: card ? str(card.state) : "",
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** pendingGates -> Integrator/gate. */
|
|
645
|
+
function normalizePendingGate(cardId, ctx) {
|
|
646
|
+
var cards = isObj(ctx.cards) ? ctx.cards : {};
|
|
647
|
+
var card = cards[cardId] || null;
|
|
648
|
+
return signalBase({
|
|
649
|
+
id: "gate:" + cardId,
|
|
650
|
+
at: str(ctx.nowIso),
|
|
651
|
+
actorId: "integrator",
|
|
652
|
+
actorLabel: "integrator · gate pendiente",
|
|
653
|
+
actorRole: "integrator",
|
|
654
|
+
coordinator: "integrator",
|
|
655
|
+
type: "gate",
|
|
656
|
+
raw: "pending_gate",
|
|
657
|
+
decision: "Gate pendiente · " + cardId,
|
|
658
|
+
reason: "la card espera la corrida de gates para poder cerrarse",
|
|
659
|
+
cardId: cardId,
|
|
660
|
+
card: card,
|
|
661
|
+
cardTitle: card ? str(card.title) : "",
|
|
662
|
+
cardState: card ? str(card.state) : "",
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
667
|
+
// Extraccion
|
|
668
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Extrae y agrupa decisiones desde un snapshot de la oficina.
|
|
672
|
+
*
|
|
673
|
+
* @param {object} snapshot {state, roster, messages:[{channel,message}], artifacts}
|
|
674
|
+
* @param {object} [opts] {now, includePeer}
|
|
675
|
+
* @returns {{items:object[], sections:object, counts:object, runId:string}}
|
|
676
|
+
*/
|
|
677
|
+
function extractDecisions(snapshot, opts) {
|
|
678
|
+
var o = isObj(opts) ? opts : {};
|
|
679
|
+
var s = isObj(snapshot) ? snapshot : {};
|
|
680
|
+
var state = isObj(s.state) ? s.state : {};
|
|
681
|
+
var roster = isObj(s.roster) ? s.roster : {};
|
|
682
|
+
var cards = isObj(state.cards) ? state.cards : {};
|
|
683
|
+
var now = typeof o.now === "number" ? o.now : Date.now();
|
|
684
|
+
var nowIso = new Date(now).toISOString();
|
|
685
|
+
var ctx = { cards: cards, roster: roster, artifacts: s.artifacts, now: now, nowIso: nowIso };
|
|
686
|
+
|
|
687
|
+
var items = [];
|
|
688
|
+
|
|
689
|
+
(Array.isArray(state.decisions) ? state.decisions : []).forEach(function (rec) {
|
|
690
|
+
if (!isObj(rec)) return;
|
|
691
|
+
items.push(normalizeDecisionRecord(rec, ctx));
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
(Array.isArray(state.escalations) ? state.escalations : []).forEach(function (esc) {
|
|
695
|
+
if (!isObj(esc)) return;
|
|
696
|
+
if (str(esc.status) === "resolved") return;
|
|
697
|
+
items.push(normalizeEscalation(esc, ctx));
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
var ledger = isObj(state.context) && Array.isArray(state.context.decisions) ? state.context.decisions : [];
|
|
701
|
+
ledger.forEach(function (text, i) { items.push(normalizeContextDecision(text, i, ctx)); });
|
|
702
|
+
|
|
703
|
+
var messages = Array.isArray(s.messages) ? s.messages : [];
|
|
704
|
+
messages.forEach(function (entry) {
|
|
705
|
+
if (!isObj(entry) || !isObj(entry.message)) return;
|
|
706
|
+
var channel = str(entry.channel);
|
|
707
|
+
if (!o.includePeer && channel === "peer") return;
|
|
708
|
+
if (channel && !CHANNEL_COORDINATOR[channel] && channel !== "peer") return;
|
|
709
|
+
items.push(normalizeBusMessage(entry.message, {
|
|
710
|
+
channel: channel, cards: cards, roster: roster, now: now,
|
|
711
|
+
}));
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
// Dedup: mismo origen + mismo payload no se muestra dos veces.
|
|
715
|
+
var seen = {};
|
|
716
|
+
items = items.filter(function (it) {
|
|
717
|
+
var key = it.source + "|" + it.id;
|
|
718
|
+
if (seen[key]) return false;
|
|
719
|
+
seen[key] = true;
|
|
720
|
+
return true;
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
items.sort(function (a, b) {
|
|
724
|
+
var ta = Date.parse(a.at) || 0;
|
|
725
|
+
var tb = Date.parse(b.at) || 0;
|
|
726
|
+
if (tb !== ta) return tb - ta;
|
|
727
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
var sections = { commander: [], integrator: [], architect: [] };
|
|
731
|
+
items.forEach(function (it) {
|
|
732
|
+
(sections[it.coordinator] = sections[it.coordinator] || []).push(it);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
items: items,
|
|
737
|
+
sections: sections,
|
|
738
|
+
counts: {
|
|
739
|
+
total: items.length,
|
|
740
|
+
commander: sections.commander.length,
|
|
741
|
+
integrator: sections.integrator.length,
|
|
742
|
+
architect: sections.architect.length,
|
|
743
|
+
},
|
|
744
|
+
runId: str(state.runId),
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
749
|
+
// Filtros
|
|
750
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
751
|
+
|
|
752
|
+
var EMPTY_FILTERS = { coordinator: "all", card: "all", type: "all", query: "", onlyDecisions: false };
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* @param {object[]} items
|
|
756
|
+
* @param {{coordinator?:string, card?:string, type?:string, query?:string, onlyDecisions?:boolean}} filters
|
|
757
|
+
*/
|
|
758
|
+
function filterDecisions(items, filters) {
|
|
759
|
+
var f = Object.assign({}, EMPTY_FILTERS, isObj(filters) ? filters : {});
|
|
760
|
+
var q = str(f.query).trim().toLowerCase();
|
|
761
|
+
return (Array.isArray(items) ? items : []).filter(function (it) {
|
|
762
|
+
if (f.coordinator !== "all" && it.coordinator !== f.coordinator) return false;
|
|
763
|
+
if (f.card !== "all" && it.cardId !== f.card) return false;
|
|
764
|
+
if (f.type !== "all" && it.type !== f.type) return false;
|
|
765
|
+
if (f.onlyDecisions && !it.decisionLike) return false;
|
|
766
|
+
if (q) {
|
|
767
|
+
var hay = [it.decision, it.reason, it.nextAction, it.actorId, it.actorLabel, it.cardId, it.cardTitle, it.typeLabel, it.channel]
|
|
768
|
+
.map(str)
|
|
769
|
+
.join(" ")
|
|
770
|
+
.toLowerCase();
|
|
771
|
+
if (hay.indexOf(q) === -1) return false;
|
|
772
|
+
}
|
|
773
|
+
return true;
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** Agrupa por coordinador respetando el filtro; siempre devuelve las 3 claves. */
|
|
778
|
+
function groupByCoordinator(items) {
|
|
779
|
+
var out = { commander: [], integrator: [], architect: [] };
|
|
780
|
+
(Array.isArray(items) ? items : []).forEach(function (it) {
|
|
781
|
+
(out[it.coordinator] = out[it.coordinator] || []).push(it);
|
|
782
|
+
});
|
|
783
|
+
return out;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** Opciones de filtro derivadas de los datos presentes. */
|
|
787
|
+
function filterOptions(items) {
|
|
788
|
+
var cards = {};
|
|
789
|
+
var types = {};
|
|
790
|
+
(Array.isArray(items) ? items : []).forEach(function (it) {
|
|
791
|
+
if (it.cardId) cards[it.cardId] = it.cardTitle || it.cardState || "";
|
|
792
|
+
types[it.type] = { id: it.type, label: it.typeLabel, coordinator: it.coordinator, count: (types[it.type] ? types[it.type].count : 0) + 1 };
|
|
793
|
+
});
|
|
794
|
+
return {
|
|
795
|
+
cards: Object.keys(cards).sort().map(function (id) { return { id: id, title: cards[id] }; }),
|
|
796
|
+
types: Object.keys(types)
|
|
797
|
+
.map(function (k) { return types[k]; })
|
|
798
|
+
.sort(function (a, b) {
|
|
799
|
+
var ci = COORDINATOR_IDS.indexOf(a.coordinator) - COORDINATOR_IDS.indexOf(b.coordinator);
|
|
800
|
+
return ci !== 0 ? ci : a.label.localeCompare(b.label);
|
|
801
|
+
}),
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
806
|
+
// Tiempo
|
|
807
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
808
|
+
|
|
809
|
+
/** "hace 12s", "hace 3m", "hace 2h", "hace 4d". */
|
|
810
|
+
function formatRelative(iso, now) {
|
|
811
|
+
var t = Date.parse(str(iso));
|
|
812
|
+
if (!isFinite(t)) return "—";
|
|
813
|
+
var diff = Math.max(0, (typeof now === "number" ? now : Date.now()) - t);
|
|
814
|
+
var s = Math.round(diff / 1000);
|
|
815
|
+
if (s < 5) return "ahora";
|
|
816
|
+
if (s < 60) return "hace " + s + "s";
|
|
817
|
+
var m = Math.round(s / 60);
|
|
818
|
+
if (m < 60) return "hace " + m + "m";
|
|
819
|
+
var h = Math.round(m / 60);
|
|
820
|
+
if (h < 24) return "hace " + h + "h";
|
|
821
|
+
var d = Math.round(h / 24);
|
|
822
|
+
return "hace " + d + "d";
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function formatClock(iso) {
|
|
826
|
+
var t = Date.parse(str(iso));
|
|
827
|
+
if (!isFinite(t)) return "—";
|
|
828
|
+
var d = new Date(t);
|
|
829
|
+
function p(n) { return n < 10 ? "0" + n : String(n); }
|
|
830
|
+
return p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
834
|
+
// Demo (para file:// sin datos y para tests manuales)
|
|
835
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
836
|
+
|
|
837
|
+
function demoSnapshot(now) {
|
|
838
|
+
var t = typeof now === "number" ? now : Date.now();
|
|
839
|
+
function at(secAgo) { return new Date(t - secAgo * 1000).toISOString(); }
|
|
840
|
+
return {
|
|
841
|
+
generatedAt: new Date(t).toISOString(),
|
|
842
|
+
source: "demo",
|
|
843
|
+
state: {
|
|
844
|
+
version: 2,
|
|
845
|
+
runId: "run-demo",
|
|
846
|
+
status: "running",
|
|
847
|
+
cards: {
|
|
848
|
+
"T-06": {
|
|
849
|
+
id: "T-06",
|
|
850
|
+
title: "Panel de intervencion del jefe",
|
|
851
|
+
state: "in_progress",
|
|
852
|
+
owner: "e-xr-immersive-developer",
|
|
853
|
+
reviewer: "e-testing-api-tester",
|
|
854
|
+
attempts: 1,
|
|
855
|
+
rejections: 0,
|
|
856
|
+
},
|
|
857
|
+
"T-00": { id: "T-00", title: "Guard anti-gate-verde-vacio", state: "done", owner: "e-testing-api-tester", reviewer: null },
|
|
858
|
+
},
|
|
859
|
+
decisions: [
|
|
860
|
+
{ at: at(5400), cardId: "T-06", decision: "approved", reason: "routing: mejor match de capabilities (3)", actor: "integrator" },
|
|
861
|
+
{ at: at(3600), cardId: "T-00", decision: "rework", reason: "1 de 3 rechazos: gate secrecy FAIL", actor: "oficina-critico" },
|
|
862
|
+
{ at: at(1200), cardId: "T-00", decision: "escalated", reason: "la card no cierra sin decision humana sobre el scan de tests", actor: "e-testing-api-tester" },
|
|
863
|
+
],
|
|
864
|
+
escalations: [
|
|
865
|
+
{
|
|
866
|
+
id: "esc-1",
|
|
867
|
+
cardId: "T-00",
|
|
868
|
+
at: at(1200),
|
|
869
|
+
cause: "el gate de seguridad escanea los propios fixtures del repo",
|
|
870
|
+
options: [
|
|
871
|
+
{ id: "o1", label: "Excluir tests/** del scan", tradeoff: "oculta secretos reales en tests" },
|
|
872
|
+
{ id: "o2", label: "Neutralizar fixtures por concatenacion", tradeoff: "requiere rework de tests" },
|
|
873
|
+
],
|
|
874
|
+
recommendation: "o2",
|
|
875
|
+
status: "open",
|
|
876
|
+
},
|
|
877
|
+
],
|
|
878
|
+
context: { decisions: ["HTML + CSS + JS vanilla sin build step (T-02)", "Lectura read-only de .oficina/** via snapshot"] },
|
|
879
|
+
},
|
|
880
|
+
roster: {
|
|
881
|
+
agents: {
|
|
882
|
+
"e-xr-immersive-developer": { id: "e-xr-immersive-developer", role: "xr-immersive-developer", displayName: "XR Immersive Developer" },
|
|
883
|
+
"e-testing-api-tester": { id: "e-testing-api-tester", role: "testing-api-tester", displayName: "API Tester" },
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
messages: [
|
|
887
|
+
{
|
|
888
|
+
channel: "coord-flow",
|
|
889
|
+
message: {
|
|
890
|
+
id: "m-demo-1", seq: 1, from: "integrator", to: ["e-xr-immersive-developer"], kind: "NOTICE",
|
|
891
|
+
subject: "T-06 despachada: dashboard/** es tu territorio",
|
|
892
|
+
body: { card: "T-06", decision: "route", reason: "owner asignado por cobertura de capability frontend", },
|
|
893
|
+
createdAt: at(5400), priority: "normal",
|
|
894
|
+
},
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
channel: "coord-arch",
|
|
898
|
+
message: {
|
|
899
|
+
id: "m-demo-2", seq: 2, from: "architect", to: ["*"], kind: "NOTICE",
|
|
900
|
+
subject: "Contrato de snapshot unico para todos los paneles",
|
|
901
|
+
body: { type: "contract", reason: "un endpoint /api/snapshot evita lecturas rasgadas entre state.json y bus", text: "snapshot { state, roster, messages[], artifacts }", },
|
|
902
|
+
createdAt: at(7200), priority: "high",
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
channel: "coord-command",
|
|
907
|
+
message: {
|
|
908
|
+
id: "m-demo-3", seq: 3, from: "oficina-director", to: ["integrator"], kind: "STATUS",
|
|
909
|
+
subject: "Prioridad: T-06 antes que T-08",
|
|
910
|
+
body: { text: "el panel del jefe es requisito de T-10; factory puede esperar", card: "T-06" },
|
|
911
|
+
createdAt: at(2400), priority: "high",
|
|
912
|
+
},
|
|
913
|
+
},
|
|
914
|
+
],
|
|
915
|
+
artifacts: { "T-06": { dir: "../.oficina/artifacts/T-06/", files: ["handoff.md", "evidence.md"] } },
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
var API = {
|
|
920
|
+
COORDINATORS: COORDINATORS,
|
|
921
|
+
COORDINATOR_IDS: COORDINATOR_IDS,
|
|
922
|
+
TYPE_LABELS: TYPE_LABELS,
|
|
923
|
+
TYPE_TO_COORDINATOR: TYPE_TO_COORDINATOR,
|
|
924
|
+
CHANNEL_COORDINATOR: CHANNEL_COORDINATOR,
|
|
925
|
+
ARTIFACTS_ROOT: ARTIFACTS_ROOT,
|
|
926
|
+
classifyActor: classifyActor,
|
|
927
|
+
classifyDecision: classifyDecision,
|
|
928
|
+
deriveNextAction: deriveNextAction,
|
|
929
|
+
inferCardId: inferCardId,
|
|
930
|
+
cardLinks: cardLinks,
|
|
931
|
+
normalizeDecisionRecord: normalizeDecisionRecord,
|
|
932
|
+
normalizeEscalation: normalizeEscalation,
|
|
933
|
+
normalizeBusMessage: normalizeBusMessage,
|
|
934
|
+
extractDecisions: extractDecisions,
|
|
935
|
+
filterDecisions: filterDecisions,
|
|
936
|
+
groupByCoordinator: groupByCoordinator,
|
|
937
|
+
filterOptions: filterOptions,
|
|
938
|
+
formatRelative: formatRelative,
|
|
939
|
+
formatClock: formatClock,
|
|
940
|
+
demoSnapshot: demoSnapshot,
|
|
941
|
+
clip: clip,
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
root.OficinaLeadership = API;
|
|
945
|
+
if (typeof module !== "undefined" && module.exports) module.exports = API;
|
|
946
|
+
})(typeof globalThis !== "undefined" ? globalThis : this);
|