oficina-opencode 3.0.5 → 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 (145) hide show
  1. package/README.md +240 -208
  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 +148 -26
  43. package/dist/cli.js.map +1 -1
  44. package/dist/core/bus.js +3 -3
  45. package/dist/core/bus.js.map +1 -1
  46. package/dist/core/catalog.d.ts +9 -1
  47. package/dist/core/catalog.d.ts.map +1 -1
  48. package/dist/core/catalog.js +77 -2
  49. package/dist/core/catalog.js.map +1 -1
  50. package/dist/core/dashboard.d.ts +33 -0
  51. package/dist/core/dashboard.d.ts.map +1 -0
  52. package/dist/core/dashboard.js +115 -0
  53. package/dist/core/dashboard.js.map +1 -0
  54. package/dist/core/dispatch.d.ts +1 -1
  55. package/dist/core/dispatch.d.ts.map +1 -1
  56. package/dist/core/dispatch.js +84 -12
  57. package/dist/core/dispatch.js.map +1 -1
  58. package/dist/core/docsync.d.ts +0 -9
  59. package/dist/core/docsync.d.ts.map +1 -1
  60. package/dist/core/docsync.js +12 -15
  61. package/dist/core/docsync.js.map +1 -1
  62. package/dist/core/environment.d.ts +28 -0
  63. package/dist/core/environment.d.ts.map +1 -0
  64. package/dist/core/environment.js +80 -0
  65. package/dist/core/environment.js.map +1 -0
  66. package/dist/core/factory.d.ts +16 -0
  67. package/dist/core/factory.d.ts.map +1 -1
  68. package/dist/core/factory.js +143 -0
  69. package/dist/core/factory.js.map +1 -1
  70. package/dist/core/fsx.d.ts +20 -2
  71. package/dist/core/fsx.d.ts.map +1 -1
  72. package/dist/core/fsx.js +10 -6
  73. package/dist/core/fsx.js.map +1 -1
  74. package/dist/core/gates.d.ts +23 -1
  75. package/dist/core/gates.d.ts.map +1 -1
  76. package/dist/core/gates.js +83 -8
  77. package/dist/core/gates.js.map +1 -1
  78. package/dist/core/leases.d.ts.map +1 -1
  79. package/dist/core/leases.js +4 -1
  80. package/dist/core/leases.js.map +1 -1
  81. package/dist/core/mesa.d.ts +1 -6
  82. package/dist/core/mesa.d.ts.map +1 -1
  83. package/dist/core/mesa.js +1 -6
  84. package/dist/core/mesa.js.map +1 -1
  85. package/dist/core/office.d.ts +3 -29
  86. package/dist/core/office.d.ts.map +1 -1
  87. package/dist/core/office.js +77 -90
  88. package/dist/core/office.js.map +1 -1
  89. package/dist/core/paths.d.ts.map +1 -1
  90. package/dist/core/paths.js +6 -1
  91. package/dist/core/paths.js.map +1 -1
  92. package/dist/core/retry.d.ts +2 -8
  93. package/dist/core/retry.d.ts.map +1 -1
  94. package/dist/core/retry.js +3 -34
  95. package/dist/core/retry.js.map +1 -1
  96. package/dist/core/roster.js +3 -3
  97. package/dist/core/roster.js.map +1 -1
  98. package/dist/core/sandbox.d.ts +38 -1
  99. package/dist/core/sandbox.d.ts.map +1 -1
  100. package/dist/core/sandbox.js +119 -15
  101. package/dist/core/sandbox.js.map +1 -1
  102. package/dist/core/state.d.ts +2 -2
  103. package/dist/core/state.d.ts.map +1 -1
  104. package/dist/core/state.js +5 -33
  105. package/dist/core/state.js.map +1 -1
  106. package/dist/core/stores.d.ts +3 -13
  107. package/dist/core/stores.d.ts.map +1 -1
  108. package/dist/core/stores.js +8 -32
  109. package/dist/core/stores.js.map +1 -1
  110. package/dist/core/systemPrompt.d.ts.map +1 -1
  111. package/dist/core/systemPrompt.js +4 -1
  112. package/dist/core/systemPrompt.js.map +1 -1
  113. package/dist/core/tripwire.d.ts +22 -8
  114. package/dist/core/tripwire.d.ts.map +1 -1
  115. package/dist/core/tripwire.js +71 -26
  116. package/dist/core/tripwire.js.map +1 -1
  117. package/dist/core/types.d.ts +6 -14
  118. package/dist/core/types.d.ts.map +1 -1
  119. package/dist/core/types.js +11 -3
  120. package/dist/core/types.js.map +1 -1
  121. package/dist/hooks/scopeGuard.d.ts +15 -1
  122. package/dist/hooks/scopeGuard.d.ts.map +1 -1
  123. package/dist/hooks/scopeGuard.js +88 -73
  124. package/dist/hooks/scopeGuard.js.map +1 -1
  125. package/dist/tools/index.d.ts +9 -1
  126. package/dist/tools/index.d.ts.map +1 -1
  127. package/dist/tools/index.js +29 -81
  128. package/dist/tools/index.js.bak +972 -0
  129. package/dist/tools/index.js.map +1 -1
  130. package/dist/v1.d.ts.map +1 -1
  131. package/dist/v1.js +30 -36
  132. package/dist/v1.js.map +1 -1
  133. package/dist/v2.d.ts +5 -0
  134. package/dist/v2.d.ts.map +1 -1
  135. package/dist/v2.js +79 -46
  136. package/dist/v2.js.bak +517 -0
  137. package/dist/v2.js.map +1 -1
  138. package/docs/GARANTIAS.md +2 -2
  139. package/docs/PRUEBA-E2E.md +4 -4
  140. package/docs/QUE-ES.md +4 -5
  141. package/docs/SEGURIDAD-REPO.md +83 -0
  142. package/docs/assets/banner.svg +42 -0
  143. package/docs/catalog-facts.json +3 -3
  144. package/docs/superpowers/specs/2026-10-29-factory-skill-creator-and-remove-budget.md +182 -0
  145. package/package.json +14 -6
@@ -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 "&amp;"
42
+ if (ch === "<") return "&lt;"
43
+ if (ch === ">") return "&gt;"
44
+ if (ch === '"') return "&quot;"
45
+ return "&#39;"
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)