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,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kanban-source.js — lógica pura del Kanban de la Oficina.
|
|
3
|
+
*
|
|
4
|
+
* Este archivo no toca el DOM: recibe datos de la corrida y devuelve strings
|
|
5
|
+
* HTML o valores primitivos. Eso permite ejecutarlo igual en el browser
|
|
6
|
+
* (como script clásico, sin build step, y sin CORS aunque se abra con
|
|
7
|
+
* `file://`) y en Node (el checker `kanban.check.mjs` lo evalúa en `node:vm`
|
|
8
|
+
* y le corre assertions reales).
|
|
9
|
+
*
|
|
10
|
+
* Contrato de datos: ver `build-kanban-data.mjs`. El dataset tiene la forma
|
|
11
|
+
* { meta: {...}, agents: { [id]: Agent }, cards: [ViewCard] }
|
|
12
|
+
* y `ViewCard` es la `Card` de `src/core/types.ts` normalizada para lectura.
|
|
13
|
+
*/
|
|
14
|
+
(function (global) {
|
|
15
|
+
"use strict"
|
|
16
|
+
|
|
17
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
18
|
+
// Constantes
|
|
19
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
var COLUMNS = [
|
|
22
|
+
{ id: "todo", label: "TODO", hint: "sin empezar" },
|
|
23
|
+
{ id: "in_progress", label: "IN_PROGRESS", hint: "en ejecución" },
|
|
24
|
+
{ id: "review", label: "REVIEW", hint: "esperando revisor" },
|
|
25
|
+
{ id: "done", label: "DONE", hint: "cerrada con gates" },
|
|
26
|
+
{ id: "rework", label: "REWORK", hint: "rechazada, vuelve" },
|
|
27
|
+
{ id: "blocked", label: "BLOCKED", hint: "dependencia externa" },
|
|
28
|
+
{ id: "escalated", label: "ESCALATED", hint: "esperando decisión" }
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
var COLUMN_IDS = COLUMNS.map(function (c) { return c.id })
|
|
32
|
+
|
|
33
|
+
var GATE_LABELS = { pass: "PASS", fail: "FAIL", not_run: "NOT_RUN", na: "N/A" }
|
|
34
|
+
|
|
35
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
36
|
+
// Helpers de formato
|
|
37
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function escapeHtml(value) {
|
|
40
|
+
return String(value === null || value === undefined ? "" : value).replace(/[&<>"']/g, function (ch) {
|
|
41
|
+
if (ch === "&") return "&"
|
|
42
|
+
if (ch === "<") return "<"
|
|
43
|
+
if (ch === ">") return ">"
|
|
44
|
+
if (ch === '"') return """
|
|
45
|
+
return "'"
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function initials(name) {
|
|
50
|
+
var raw = String(name || "").trim()
|
|
51
|
+
if (!raw) return "·"
|
|
52
|
+
var parts = raw.replace(/^e-/, "").split(/[\s._-]+/).filter(Boolean)
|
|
53
|
+
if (parts.length === 0) return raw.slice(0, 2).toUpperCase()
|
|
54
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
|
|
55
|
+
return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Hash estable para colorear avatares por rol (sin depender del orden). */
|
|
59
|
+
function hueFor(seed) {
|
|
60
|
+
var text = String(seed || "")
|
|
61
|
+
var h = 0
|
|
62
|
+
for (var i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) % 360
|
|
63
|
+
return h
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function clamp(n, min, max) {
|
|
67
|
+
return Math.max(min, Math.min(max, n))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function percent(part, total) {
|
|
71
|
+
if (!total) return 0
|
|
72
|
+
return clamp(Math.round((part / total) * 100), 0, 100)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatRelative(iso, now) {
|
|
76
|
+
if (!iso) return "—"
|
|
77
|
+
var then = new Date(iso).getTime()
|
|
78
|
+
if (!Number.isFinite(then)) return "—"
|
|
79
|
+
var base = typeof now === "number" ? now : Date.now()
|
|
80
|
+
var diff = Math.max(0, base - then)
|
|
81
|
+
var sec = Math.floor(diff / 1000)
|
|
82
|
+
if (sec < 60) return "hace " + sec + "s"
|
|
83
|
+
var min = Math.floor(sec / 60)
|
|
84
|
+
if (min < 60) return "hace " + min + "m"
|
|
85
|
+
var hours = Math.floor(min / 60)
|
|
86
|
+
if (hours < 24) return "hace " + hours + "h"
|
|
87
|
+
return "hace " + Math.floor(hours / 24) + "d"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
91
|
+
// Normalización
|
|
92
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/** Un agente tal como lo necesita la UI: id, nombre, rol, iniciales. */
|
|
95
|
+
function normalizeAgent(raw) {
|
|
96
|
+
if (!raw) return null
|
|
97
|
+
var id = raw.id || raw.agentId || ""
|
|
98
|
+
if (!id) return null
|
|
99
|
+
var name = raw.displayName || raw.name || raw.role || id
|
|
100
|
+
return {
|
|
101
|
+
id: id,
|
|
102
|
+
name: name,
|
|
103
|
+
role: raw.role || "",
|
|
104
|
+
initials: initials(name),
|
|
105
|
+
hue: hueFor(raw.role || id),
|
|
106
|
+
state: raw.state || null,
|
|
107
|
+
currentCard: raw.currentCard || null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeGateOutcome(outcome) {
|
|
112
|
+
var value = String(outcome || "").toLowerCase()
|
|
113
|
+
if (value === "pass") return "pass"
|
|
114
|
+
if (value === "fail") return "fail"
|
|
115
|
+
if (value === "na") return "na"
|
|
116
|
+
return "not_run"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Badge de gate: etiqueta + tono. Contrato: PASS / FAIL / NOT_RUN. */
|
|
120
|
+
function gateBadge(outcome) {
|
|
121
|
+
var normalized = normalizeGateOutcome(outcome)
|
|
122
|
+
return {
|
|
123
|
+
outcome: normalized,
|
|
124
|
+
label: GATE_LABELS[normalized],
|
|
125
|
+
className: "gate-badge gate-badge--" + normalized.replace(/_/g, "-"),
|
|
126
|
+
tone: normalized === "pass" ? "pass" : normalized === "fail" ? "fail" : normalized === "na" ? "na" : "not-run"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Gate de requisitos derivado del grafo de evidencia.
|
|
132
|
+
*
|
|
133
|
+
* - `fail` → alguna evidencia documenta un fallo (`passed: false`).
|
|
134
|
+
* - `pass` → todos los criterios tienen al menos una evidencia viva.
|
|
135
|
+
* - `not_run` → no hay evidencia: el gate no se corrió, no que falló.
|
|
136
|
+
* - `na` → la card no declara criterios.
|
|
137
|
+
*/
|
|
138
|
+
function requirementsOutcome(requirements) {
|
|
139
|
+
var list = requirements || []
|
|
140
|
+
if (list.length === 0) return "na"
|
|
141
|
+
var failed = false
|
|
142
|
+
var covered = 0
|
|
143
|
+
for (var i = 0; i < list.length; i++) {
|
|
144
|
+
var nodes = list[i].evidence || []
|
|
145
|
+
var alive = nodes.filter(function (n) { return n && n.passed !== false })
|
|
146
|
+
if (nodes.some(function (n) { return n && n.passed === false })) failed = true
|
|
147
|
+
if (alive.length > 0) covered++
|
|
148
|
+
}
|
|
149
|
+
if (failed) return "fail"
|
|
150
|
+
if (covered === list.length) return "pass"
|
|
151
|
+
return "not_run"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Outcome agregado de una card.
|
|
156
|
+
* Prioridad: FAIL > NOT_RUN > PASS. Un gate sin correr no es un gate verde.
|
|
157
|
+
*/
|
|
158
|
+
function gateOutcome(gate) {
|
|
159
|
+
if (!gate) return "not_run"
|
|
160
|
+
var outcomes = [
|
|
161
|
+
gate.technical ? gate.technical.outcome : "not_run",
|
|
162
|
+
gate.security ? gate.security.outcome : "not_run",
|
|
163
|
+
gate.requirements ? gate.requirements.outcome : "not_run"
|
|
164
|
+
].map(normalizeGateOutcome)
|
|
165
|
+
if (outcomes.indexOf("fail") !== -1) return "fail"
|
|
166
|
+
if (outcomes.indexOf("not_run") !== -1) return "not_run"
|
|
167
|
+
if (outcomes.every(function (o) { return o === "na" })) return "na"
|
|
168
|
+
return "pass"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizePhases(phases) {
|
|
172
|
+
return (phases || []).map(function (p) {
|
|
173
|
+
return {
|
|
174
|
+
phase: String(p.phase || "—"),
|
|
175
|
+
command: p.command || null,
|
|
176
|
+
outcome: normalizeGateOutcome(p.outcome),
|
|
177
|
+
detail: p.detail || "",
|
|
178
|
+
reason: p.reason || ""
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeGate(raw, cardId) {
|
|
184
|
+
var gate = raw || {}
|
|
185
|
+
var technical = normalizePhases(gate.technical)
|
|
186
|
+
var security = normalizePhases(gate.security)
|
|
187
|
+
var requirements = normalizePhases(gate.requirements)
|
|
188
|
+
return {
|
|
189
|
+
cardId: cardId,
|
|
190
|
+
ranAt: gate.ranAt || null,
|
|
191
|
+
text: gate.text || "",
|
|
192
|
+
technical: { outcome: normalizeGateOutcome(gate.technicalOutcome), phases: technical },
|
|
193
|
+
security: { outcome: normalizeGateOutcome(gate.securityOutcome), phases: security },
|
|
194
|
+
requirements: { outcome: normalizeGateOutcome(gate.requirementsOutcome), phases: requirements },
|
|
195
|
+
verdict: normalizeGateOutcome(gate.verdict)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function normalizeAcceptance(list, nodes) {
|
|
200
|
+
var evidence = nodes || []
|
|
201
|
+
return (list || []).map(function (text, index) {
|
|
202
|
+
var linked = evidence.filter(function (n) {
|
|
203
|
+
if (!n) return false
|
|
204
|
+
if (typeof n.requirementIndex === "number" && n.requirementIndex >= 0) return n.requirementIndex === index
|
|
205
|
+
return n.requirement === text
|
|
206
|
+
})
|
|
207
|
+
var passed = linked.filter(function (n) { return n.passed !== false })
|
|
208
|
+
return {
|
|
209
|
+
index: index,
|
|
210
|
+
text: text,
|
|
211
|
+
evidence: linked,
|
|
212
|
+
satisfied: passed.length > 0,
|
|
213
|
+
failed: linked.some(function (n) { return n.passed === false })
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Progreso del checklist: criterios con evidencia viva sobre total. */
|
|
219
|
+
function acceptanceProgress(card) {
|
|
220
|
+
var list = (card && card.acceptance) || []
|
|
221
|
+
var covered = list.filter(function (a) { return a.satisfied }).length
|
|
222
|
+
return {
|
|
223
|
+
total: list.length,
|
|
224
|
+
covered: covered,
|
|
225
|
+
missing: list.length - covered,
|
|
226
|
+
pct: percent(covered, list.length)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function artifactBase(rawPath, cardId) {
|
|
231
|
+
var text = String(rawPath || "").replace(/\\/g, "/")
|
|
232
|
+
if (!text) return "../.oficina/artifacts/" + cardId
|
|
233
|
+
if (/^\.oficina\//.test(text)) return "../" + text
|
|
234
|
+
return text
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeCard(raw, ctx) {
|
|
238
|
+
var context = ctx || {}
|
|
239
|
+
var agents = context.agents || {}
|
|
240
|
+
var gates = context.gates || {}
|
|
241
|
+
var evidence = context.evidence || {}
|
|
242
|
+
var id = String(raw.id || "")
|
|
243
|
+
var owner = normalizeAgent(agents[raw.owner]) || (raw.owner ? {
|
|
244
|
+
id: raw.owner, name: raw.owner, role: "", initials: initials(raw.owner), hue: hueFor(raw.owner), state: null, currentCard: null
|
|
245
|
+
} : null)
|
|
246
|
+
var reviewer = normalizeAgent(agents[raw.reviewer]) || (raw.reviewer ? {
|
|
247
|
+
id: raw.reviewer, name: raw.reviewer, role: "", initials: initials(raw.reviewer), hue: hueFor(raw.reviewer), state: null, currentCard: null
|
|
248
|
+
} : null)
|
|
249
|
+
var gate = normalizeGate(gates[id] || {}, id)
|
|
250
|
+
gate.text = raw.gate || ""
|
|
251
|
+
var state = COLUMN_IDS.indexOf(raw.state) === -1 ? "todo" : raw.state
|
|
252
|
+
var acceptance = normalizeAcceptance(raw.acceptance, evidence[id])
|
|
253
|
+
// El gate de requisitos no vive en gates.md: se deriva del grafo de
|
|
254
|
+
// evidencia. Un criterio sin evidencia deja el gate en NOT_RUN, no en PASS.
|
|
255
|
+
gate.requirements.outcome = requirementsOutcome(acceptance)
|
|
256
|
+
gate.requirements.phases = acceptance.map(function (item) {
|
|
257
|
+
return {
|
|
258
|
+
phase: "criterio " + (item.index + 1),
|
|
259
|
+
command: null,
|
|
260
|
+
outcome: item.failed ? "fail" : item.satisfied ? "pass" : "not_run",
|
|
261
|
+
detail: item.text.length > 120 ? item.text.slice(0, 117) + "…" : item.text,
|
|
262
|
+
reason: ""
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
return {
|
|
266
|
+
id: id,
|
|
267
|
+
title: raw.title || "(sin título)",
|
|
268
|
+
type: raw.type || "—",
|
|
269
|
+
state: state,
|
|
270
|
+
rawState: raw.state || state,
|
|
271
|
+
owner: owner,
|
|
272
|
+
reviewer: reviewer,
|
|
273
|
+
rejections: Number(raw.rejections || 0),
|
|
274
|
+
attempts: Number(raw.attempts || 0),
|
|
275
|
+
acceptance: acceptance,
|
|
276
|
+
gate: gate,
|
|
277
|
+
gateText: raw.gate || "",
|
|
278
|
+
gateOutcome: gateOutcome(gate),
|
|
279
|
+
scope: {
|
|
280
|
+
mayTouch: (raw.scope && raw.scope.mayTouch) || [],
|
|
281
|
+
forbidden: (raw.scope && raw.scope.forbidden) || []
|
|
282
|
+
},
|
|
283
|
+
artifactsUrl: artifactBase(raw.artifacts, id),
|
|
284
|
+
artifactsRaw: raw.artifacts || "",
|
|
285
|
+
result: raw.result || null,
|
|
286
|
+
dependsOn: raw.dependsOn || [],
|
|
287
|
+
helpHops: Number(raw.helpHops || 0),
|
|
288
|
+
helpRequests: raw.helpRequests || []
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function normalizeDataset(input) {
|
|
293
|
+
var data = input || {}
|
|
294
|
+
var agents = {}
|
|
295
|
+
var rawAgents = data.agents || {}
|
|
296
|
+
Object.keys(rawAgents).forEach(function (key) {
|
|
297
|
+
var agent = normalizeAgent(rawAgents[key])
|
|
298
|
+
if (agent) agents[agent.id] = agent
|
|
299
|
+
})
|
|
300
|
+
var ctx = { agents: agents, gates: data.gates || {}, evidence: data.evidence || {} }
|
|
301
|
+
var cards = (data.cards || []).map(function (raw) { return normalizeCard(raw, ctx) })
|
|
302
|
+
return {
|
|
303
|
+
meta: data.meta || {},
|
|
304
|
+
agents: agents,
|
|
305
|
+
cards: cards,
|
|
306
|
+
columns: COLUMNS.map(function (column) {
|
|
307
|
+
return { id: column.id, label: column.label, hint: column.hint }
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
313
|
+
// Filtros y agrupación
|
|
314
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
function matchesFilters(card, filters) {
|
|
317
|
+
var f = filters || {}
|
|
318
|
+
if (f.owner && f.owner !== "all") {
|
|
319
|
+
if (f.owner === "unassigned") {
|
|
320
|
+
if (card.owner) return false
|
|
321
|
+
} else if (!card.owner || card.owner.id !== f.owner) {
|
|
322
|
+
return false
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (f.type && f.type !== "all" && card.type !== f.type) return false
|
|
326
|
+
if (f.state && f.state !== "all" && card.state !== f.state) return false
|
|
327
|
+
if (f.q) {
|
|
328
|
+
var needle = String(f.q).toLowerCase()
|
|
329
|
+
var haystack = (card.id + " " + card.title + " " + card.type + " " + (card.owner ? card.owner.id : "")).toLowerCase()
|
|
330
|
+
if (haystack.indexOf(needle) === -1) return false
|
|
331
|
+
}
|
|
332
|
+
return true
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function filterCards(cards, filters) {
|
|
336
|
+
return (cards || []).filter(function (card) { return matchesFilters(card, filters) })
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Agrupa por columna. Siempre devuelve las 7 claves, aun vacías. */
|
|
340
|
+
function groupByColumn(cards) {
|
|
341
|
+
var groups = {}
|
|
342
|
+
COLUMN_IDS.forEach(function (id) { groups[id] = [] })
|
|
343
|
+
;(cards || []).forEach(function (card) {
|
|
344
|
+
var key = COLUMN_IDS.indexOf(card.state) === -1 ? "todo" : card.state
|
|
345
|
+
groups[key].push(card)
|
|
346
|
+
})
|
|
347
|
+
COLUMN_IDS.forEach(function (id) {
|
|
348
|
+
groups[id].sort(function (a, b) {
|
|
349
|
+
var na = parseInt(String(a.id).replace(/\D+/g, ""), 10)
|
|
350
|
+
var nb = parseInt(String(b.id).replace(/\D+/g, ""), 10)
|
|
351
|
+
if (Number.isFinite(na) && Number.isFinite(nb) && na !== nb) return na - nb
|
|
352
|
+
return String(a.id).localeCompare(String(b.id))
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
return groups
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function columnCounts(cards) {
|
|
359
|
+
var groups = groupByColumn(cards)
|
|
360
|
+
var counts = {}
|
|
361
|
+
COLUMN_IDS.forEach(function (id) { counts[id] = groups[id].length })
|
|
362
|
+
return counts
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
366
|
+
// Render
|
|
367
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
function avatarHtml(agent, extraClass) {
|
|
370
|
+
var classes = "avatar" + (extraClass ? " " + extraClass : "")
|
|
371
|
+
if (!agent) {
|
|
372
|
+
return '<span class="' + classes + ' avatar--empty" aria-hidden="true">?</span>'
|
|
373
|
+
}
|
|
374
|
+
return (
|
|
375
|
+
'<span class="' + classes + '" style="--avatar-hue:' + agent.hue + '" title="' +
|
|
376
|
+
escapeHtml(agent.name + (agent.role ? " · " + agent.role : "")) + '" data-agent-id="' + escapeHtml(agent.id) + '">' +
|
|
377
|
+
escapeHtml(agent.initials) + "</span>"
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function gateBadgeHtml(label, outcome) {
|
|
382
|
+
var badge = gateBadge(outcome)
|
|
383
|
+
return (
|
|
384
|
+
'<span class="' + badge.className + '" data-outcome="' + badge.outcome + '" title="' +
|
|
385
|
+
escapeHtml(label + ": " + badge.label) + '"><span class="gate-key">' + escapeHtml(label) + '</span> ' +
|
|
386
|
+
escapeHtml(badge.label) + "</span>"
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function cardHtml(card) {
|
|
391
|
+
var progress = acceptanceProgress(card)
|
|
392
|
+
var visible = card.acceptance.slice(0, 3)
|
|
393
|
+
var hidden = Math.max(0, card.acceptance.length - visible.length)
|
|
394
|
+
var ownerHtml = card.owner
|
|
395
|
+
? '<span class="person person--owner" data-owner="' + escapeHtml(card.owner.id) + '">' +
|
|
396
|
+
avatarHtml(card.owner) + '<span class="person-name">' + escapeHtml(card.owner.name) + "</span></span>"
|
|
397
|
+
: '<span class="person person--owner person--unassigned" data-owner="">' +
|
|
398
|
+
avatarHtml(null) + '<span class="person-name">sin asignar</span></span>'
|
|
399
|
+
var reviewerHtml = card.reviewer
|
|
400
|
+
? '<span class="person person--reviewer" data-reviewer="' + escapeHtml(card.reviewer.id) + '">' +
|
|
401
|
+
avatarHtml(card.reviewer) + '<span class="person-name">' + escapeHtml(card.reviewer.name) + "</span></span>"
|
|
402
|
+
: '<span class="person person--reviewer person--unassigned" data-reviewer="">' +
|
|
403
|
+
avatarHtml(null) + '<span class="person-name">sin revisor</span></span>'
|
|
404
|
+
|
|
405
|
+
var checklist = visible.map(function (item) {
|
|
406
|
+
return (
|
|
407
|
+
'<li class="check-item ' + (item.satisfied ? "is-checked" : item.failed ? "is-failed" : "is-empty") + '">' +
|
|
408
|
+
'<span class="check-box" aria-hidden="true">' + (item.satisfied ? "✓" : item.failed ? "!" : "·") + "</span>" +
|
|
409
|
+
'<span class="check-text">' + escapeHtml(item.text) + "</span></li>"
|
|
410
|
+
)
|
|
411
|
+
}).join("")
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
'<article class="card" data-card-id="' + escapeHtml(card.id) + '" data-state="' + escapeHtml(card.state) +
|
|
415
|
+
'" data-type="' + escapeHtml(card.type) + '" draggable="true" tabindex="0" role="button" aria-haspopup="dialog"' +
|
|
416
|
+
' aria-label="' + escapeHtml(card.id + " · " + card.title) + '">' +
|
|
417
|
+
'<header class="card-top"><span class="card-id">' + escapeHtml(card.id) + "</span>" +
|
|
418
|
+
'<span class="badge badge--type">' + escapeHtml(card.type) + "</span></header>" +
|
|
419
|
+
'<h3 class="card-title">' + escapeHtml(card.title) + "</h3>" +
|
|
420
|
+
'<div class="card-people">' + ownerHtml + reviewerHtml +
|
|
421
|
+
'<span class="rejections" data-rejections="' + card.rejections + '" title="Rechazos del revisor">' +
|
|
422
|
+
'<span class="rejections-mark" aria-hidden="true">↺</span> ' + card.rejections + "</span></div>" +
|
|
423
|
+
(card.acceptance.length
|
|
424
|
+
? '<ul class="checklist" aria-label="Criterios de aceptación">' + checklist +
|
|
425
|
+
(hidden > 0 ? '<li class="check-more">+' + hidden + " criterio" + (hidden === 1 ? "" : "s") + " más</li>" : "") +
|
|
426
|
+
'<li class="check-progress"><span class="check-progress-label">' + progress.covered + "/" + progress.total +
|
|
427
|
+
' con evidencia</span><span class="check-progress-track" aria-hidden="true"><span class="check-progress-fill" style="width:' +
|
|
428
|
+
progress.pct + '%"></span></span></li></ul>'
|
|
429
|
+
: '<p class="checklist-empty">sin criterios declarados</p>') +
|
|
430
|
+
'<footer class="card-gates">' +
|
|
431
|
+
gateBadgeHtml("TEC", card.gate.technical.outcome) +
|
|
432
|
+
gateBadgeHtml("SEC", card.gate.security.outcome) +
|
|
433
|
+
gateBadgeHtml("REQ", card.gate.requirements.outcome) +
|
|
434
|
+
"</footer></article>"
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function emptyColumnHtml(column) {
|
|
439
|
+
return (
|
|
440
|
+
'<p class="column-empty">nada en <strong>' + escapeHtml(column.label) + "</strong>" +
|
|
441
|
+
(column.hint ? ' <span class="column-hint">(' + escapeHtml(column.hint) + ")</span>" : "") + "</p>"
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function columnBodyHtml(column, cards) {
|
|
446
|
+
if (!cards.length) return emptyColumnHtml(column)
|
|
447
|
+
return cards.map(cardHtml).join("")
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* HTML interno de cada columna, indexado por id (no incluye el shell
|
|
452
|
+
* estático: el `<section>` de cada columna vive en el HTML para que la
|
|
453
|
+
* grilla exista aun sin JS).
|
|
454
|
+
*/
|
|
455
|
+
function boardHtml(view, filters) {
|
|
456
|
+
var visible = filterCards(view.cards, filters)
|
|
457
|
+
var groups = groupByColumn(visible)
|
|
458
|
+
var result = {}
|
|
459
|
+
COLUMN_IDS.forEach(function (id) {
|
|
460
|
+
var column = COLUMNS.filter(function (c) { return c.id === id })[0]
|
|
461
|
+
result[id] = columnBodyHtml(column, groups[id])
|
|
462
|
+
})
|
|
463
|
+
return result
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function evidenceLinksHtml(card) {
|
|
467
|
+
var nodes = []
|
|
468
|
+
card.acceptance.forEach(function (item) {
|
|
469
|
+
item.evidence.forEach(function (node) { nodes.push({ item: item, node: node }) })
|
|
470
|
+
})
|
|
471
|
+
if (nodes.length === 0) return '<p class="detail-empty">Sin evidencia registrada en .oficina/evidence/.</p>'
|
|
472
|
+
return (
|
|
473
|
+
'<ul class="detail-list">' +
|
|
474
|
+
nodes.map(function (entry) {
|
|
475
|
+
var node = entry.node
|
|
476
|
+
var href = ""
|
|
477
|
+
if (node.kind === "artifact" && node.ref) {
|
|
478
|
+
href = card.artifactsUrl + "/" + String(node.ref).replace(/^.*[\\/]/, "")
|
|
479
|
+
href = '<a class="artifact-link" href="' + escapeHtml(href) + '">' + escapeHtml(node.ref) + "</a>"
|
|
480
|
+
} else {
|
|
481
|
+
href = '<code class="evidence-ref">' + escapeHtml(node.ref || "—") + "</code>"
|
|
482
|
+
}
|
|
483
|
+
var mark = node.passed === false ? "fail" : "pass"
|
|
484
|
+
return (
|
|
485
|
+
'<li class="evidence-item" data-evidence-kind="' + escapeHtml(node.kind || "—") +
|
|
486
|
+
'" data-evidence-passed="' + String(node.passed !== false) + '">' +
|
|
487
|
+
'<span class="gate-badge gate-badge--' + mark + '">' + (mark === "pass" ? "PASS" : "FAIL") + "</span> " +
|
|
488
|
+
href +
|
|
489
|
+
'<span class="evidence-meta">' + escapeHtml(node.kind || "—") + " · nivel " + escapeHtml(node.level || "—") +
|
|
490
|
+
" · " + escapeHtml(node.recordedBy || "—") + "</span>" +
|
|
491
|
+
(node.note ? '<p class="evidence-note">' + escapeHtml(node.note) + "</p>" : "") +
|
|
492
|
+
'<p class="evidence-req">' + escapeHtml(entry.item.text) + "</p></li>"
|
|
493
|
+
)
|
|
494
|
+
}).join("") +
|
|
495
|
+
"</ul>"
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function acceptanceDetailHtml(card) {
|
|
500
|
+
if (!card.acceptance.length) return '<p class="detail-empty">La card no declara criterios de aceptación.</p>'
|
|
501
|
+
return (
|
|
502
|
+
'<ol class="detail-acceptance">' +
|
|
503
|
+
card.acceptance.map(function (item) {
|
|
504
|
+
var state = item.satisfied ? "pass" : item.failed ? "fail" : "not_run"
|
|
505
|
+
var evidence = item.evidence.length
|
|
506
|
+
? item.evidence.map(function (node) {
|
|
507
|
+
return '<li class="acceptance-evidence"><code>' + escapeHtml(node.ref || "—") + "</code>" +
|
|
508
|
+
'<span class="evidence-meta">' + escapeHtml(node.kind || "—") + " · nivel " + escapeHtml(node.level || "—") + "</span></li>"
|
|
509
|
+
}).join("")
|
|
510
|
+
: '<li class="acceptance-evidence detail-empty">sin evidencia</li>'
|
|
511
|
+
return (
|
|
512
|
+
'<li class="acceptance-item" data-acceptance-index="' + item.index + '" data-acceptance-state="' + state + '">' +
|
|
513
|
+
'<span class="gate-badge gate-badge--' + state.replace(/_/g, "-") + '">' + escapeHtml(GATE_LABELS[state] || "—") + "</span>" +
|
|
514
|
+
'<p class="acceptance-text">' + escapeHtml(item.text) + "</p>" +
|
|
515
|
+
'<ul class="acceptance-evidence-list">' + evidence + "</ul></li>"
|
|
516
|
+
)
|
|
517
|
+
}).join("") +
|
|
518
|
+
"</ol>"
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function gateSectionHtml(gate) {
|
|
523
|
+
var sections = [
|
|
524
|
+
{ key: "technical", label: "Técnico" },
|
|
525
|
+
{ key: "security", label: "Seguridad" },
|
|
526
|
+
{ key: "requirements", label: "Requisitos" }
|
|
527
|
+
]
|
|
528
|
+
return sections.map(function (section) {
|
|
529
|
+
var data = gate[section.key]
|
|
530
|
+
var phases = data.phases || []
|
|
531
|
+
var rows = phases.length
|
|
532
|
+
? phases.map(function (phase) {
|
|
533
|
+
return (
|
|
534
|
+
"<tr><td>" + escapeHtml(phase.phase) + "</td><td><code>" + escapeHtml(phase.command || "—") + "</code></td>" +
|
|
535
|
+
"<td>" + gateBadgeHtml("", phase.outcome) + "</td><td>" + escapeHtml(phase.detail || "—") +
|
|
536
|
+
(phase.reason ? '<p class="phase-reason">' + escapeHtml(phase.reason) + "</p>" : "") + "</td></tr>"
|
|
537
|
+
)
|
|
538
|
+
}).join("")
|
|
539
|
+
: '<tr><td colspan="4" class="detail-empty">Sin registro de fases para este bloque (gates.md ausente o vacío).</td></tr>'
|
|
540
|
+
return (
|
|
541
|
+
'<div class="gate-block" data-gate-section="' + section.key + '">' +
|
|
542
|
+
'<h4 class="gate-block-title">' + escapeHtml(section.label) + " " + gateBadgeHtml("", data.outcome) + "</h4>" +
|
|
543
|
+
'<table class="gate-table"><thead><tr><th>Fase</th><th>Comando</th><th>Estado</th><th>Detalle</th></tr></thead>' +
|
|
544
|
+
"<tbody>" + rows + "</tbody></table></div>"
|
|
545
|
+
)
|
|
546
|
+
}).join("")
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function scopeListHtml(label, items, tone) {
|
|
550
|
+
return (
|
|
551
|
+
'<div class="scope-block scope-block--' + tone + '"><h4>' + escapeHtml(label) + "</h4>" +
|
|
552
|
+
(items.length
|
|
553
|
+
? '<ul class="scope-list">' + items.map(function (item) {
|
|
554
|
+
return '<li><code>' + escapeHtml(item) + "</code></li>"
|
|
555
|
+
}).join("") + "</ul>"
|
|
556
|
+
: '<p class="detail-empty">—</p>') +
|
|
557
|
+
"</div>"
|
|
558
|
+
)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function modalHtml(card) {
|
|
562
|
+
var progress = acceptanceProgress(card)
|
|
563
|
+
var artifacts = [
|
|
564
|
+
{ label: "handoff.md", href: card.artifactsUrl + "/handoff.md" },
|
|
565
|
+
{ label: "evidence.md", href: card.artifactsUrl + "/evidence.md" },
|
|
566
|
+
{ label: "gates.md", href: card.artifactsUrl + "/gates.md" }
|
|
567
|
+
]
|
|
568
|
+
return (
|
|
569
|
+
'<header class="modal-head">' +
|
|
570
|
+
'<div class="modal-ident"><span class="card-id">' + escapeHtml(card.id) + "</span>" +
|
|
571
|
+
'<span class="badge badge--state" data-state="' + escapeHtml(card.state) + '">' + escapeHtml(card.state.toUpperCase()) + "</span>" +
|
|
572
|
+
'<span class="badge badge--type">' + escapeHtml(card.type) + "</span></div>" +
|
|
573
|
+
"<h2 id=\"card-modal-title\" class=\"modal-title\">" + escapeHtml(card.title) + "</h2>" +
|
|
574
|
+
'<p class="modal-meta">intentos ' + card.attempts + " · rechazos " + card.rejections +
|
|
575
|
+
" · evidencia " + progress.covered + "/" + progress.total +
|
|
576
|
+
(card.gate.ranAt ? " · gates corridos " + escapeHtml(formatRelative(card.gate.ranAt)) : " · gates sin correr") +
|
|
577
|
+
"</p></header>" +
|
|
578
|
+
'<div class="modal-body">' +
|
|
579
|
+
'<section class="detail-section" data-detail="scope"><h3>Scope</h3>' +
|
|
580
|
+
scopeListHtml("Puede tocar", card.scope.mayTouch, "may") +
|
|
581
|
+
scopeListHtml("Prohibido", card.scope.forbidden, "forbidden") +
|
|
582
|
+
(card.dependsOn.length
|
|
583
|
+
? '<p class="detail-meta">Depende de: ' + card.dependsOn.map(function (d) {
|
|
584
|
+
return '<span class="chip">' + escapeHtml(d) + "</span>"
|
|
585
|
+
}).join(" ") + "</p>"
|
|
586
|
+
: "") +
|
|
587
|
+
"</section>" +
|
|
588
|
+
'<section class="detail-section" data-detail="acceptance"><h3>Criterios de aceptación <span class="detail-count">' +
|
|
589
|
+
progress.covered + "/" + progress.total + "</span></h3>" + acceptanceDetailHtml(card) + "</section>" +
|
|
590
|
+
'<section class="detail-section" data-detail="gate"><h3>Gate</h3>' +
|
|
591
|
+
'<p class="detail-gate-text"><code>' + escapeHtml(card.gateText || "sin condición declarada") + "</code></p>" +
|
|
592
|
+
gateSectionHtml(card.gate) + "</section>" +
|
|
593
|
+
'<section class="detail-section" data-detail="artifacts"><h3>Artifacts</h3>' +
|
|
594
|
+
'<ul class="artifact-list">' + artifacts.map(function (a) {
|
|
595
|
+
return '<li><a class="artifact-link" href="' + escapeHtml(a.href) + '">' + escapeHtml(a.label) + "</a></li>"
|
|
596
|
+
}).join("") +
|
|
597
|
+
'<li><code class="artifact-path">' + escapeHtml(card.artifactsRaw || card.artifactsUrl) + "</code></li></ul>" +
|
|
598
|
+
(card.result ? '<p class="detail-result">' + escapeHtml(card.result) + "</p>" : "") +
|
|
599
|
+
"</section>" +
|
|
600
|
+
'<section class="detail-section" data-detail="evidence"><h3>Evidence</h3>' + evidenceLinksHtml(card) + "</section>" +
|
|
601
|
+
"</div>"
|
|
602
|
+
)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
606
|
+
// Export
|
|
607
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
608
|
+
|
|
609
|
+
var api = {
|
|
610
|
+
COLUMNS: COLUMNS,
|
|
611
|
+
COLUMN_IDS: COLUMN_IDS,
|
|
612
|
+
GATE_LABELS: GATE_LABELS,
|
|
613
|
+
escapeHtml: escapeHtml,
|
|
614
|
+
initials: initials,
|
|
615
|
+
hueFor: hueFor,
|
|
616
|
+
percent: percent,
|
|
617
|
+
formatRelative: formatRelative,
|
|
618
|
+
normalizeAgent: normalizeAgent,
|
|
619
|
+
normalizeCard: normalizeCard,
|
|
620
|
+
normalizeDataset: normalizeDataset,
|
|
621
|
+
normalizeGateOutcome: normalizeGateOutcome,
|
|
622
|
+
requirementsOutcome: requirementsOutcome,
|
|
623
|
+
gateOutcome: gateOutcome,
|
|
624
|
+
gateBadge: gateBadge,
|
|
625
|
+
acceptanceProgress: acceptanceProgress,
|
|
626
|
+
matchesFilters: matchesFilters,
|
|
627
|
+
filterCards: filterCards,
|
|
628
|
+
groupByColumn: groupByColumn,
|
|
629
|
+
columnCounts: columnCounts,
|
|
630
|
+
cardHtml: cardHtml,
|
|
631
|
+
columnBodyHtml: columnBodyHtml,
|
|
632
|
+
boardHtml: boardHtml,
|
|
633
|
+
avatarHtml: avatarHtml,
|
|
634
|
+
gateBadgeHtml: gateBadgeHtml,
|
|
635
|
+
acceptanceDetailHtml: acceptanceDetailHtml,
|
|
636
|
+
evidenceLinksHtml: evidenceLinksHtml,
|
|
637
|
+
gateSectionHtml: gateSectionHtml,
|
|
638
|
+
modalHtml: modalHtml
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
global.KanbanSource = api
|
|
642
|
+
if (typeof module !== "undefined" && module.exports) module.exports = api
|
|
643
|
+
})(typeof globalThis !== "undefined" ? globalThis : this)
|