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,454 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* kanban.check.mjs — verificación real del Kanban (sin browser, sin jsdom).
|
|
4
|
+
*
|
|
5
|
+
* Qué prueba:
|
|
6
|
+
* A. Contrato estático de `kanban.html`: 7 columnas, controles de filtro,
|
|
7
|
+
* dialog de detalle, badges de gate, drag-drop visual y autosuficiencia
|
|
8
|
+
* (nada de CDNs: tiene que abrir con file://).
|
|
9
|
+
* B. Lógica pura de `kanban-source.js` ejecutada en `node:vm` (el mismo
|
|
10
|
+
* archivo que carga el browser): normalización, filtros, agrupación,
|
|
11
|
+
* badges, checklist, HTML de card y de modal.
|
|
12
|
+
* C. Dataset embebido en el HTML: parsea, tiene la forma esperada y
|
|
13
|
+
* normalizeDataset no pierde cards.
|
|
14
|
+
* D. Parser de gates.md (`build-kanban-data.mjs`) contra un markdown real
|
|
15
|
+
* generado por `renderGate` (src/core/gates.ts).
|
|
16
|
+
*
|
|
17
|
+
* Uso: node dashboard/kanban.check.mjs
|
|
18
|
+
* Exit code 0 solo si todo pasa.
|
|
19
|
+
*/
|
|
20
|
+
import fs from "node:fs/promises"
|
|
21
|
+
import path from "node:path"
|
|
22
|
+
import { fileURLToPath } from "node:url"
|
|
23
|
+
import vm from "node:vm"
|
|
24
|
+
import { parseGates } from "./build-kanban-data.mjs"
|
|
25
|
+
|
|
26
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
27
|
+
const htmlPath = path.join(here, "kanban.html")
|
|
28
|
+
const sourcePath = path.join(here, "kanban-source.js")
|
|
29
|
+
|
|
30
|
+
const results = []
|
|
31
|
+
function check(name, fn) {
|
|
32
|
+
try {
|
|
33
|
+
const detail = fn()
|
|
34
|
+
results.push({ ok: true, name, detail: detail || "" })
|
|
35
|
+
} catch (err) {
|
|
36
|
+
results.push({ ok: false, name, detail: err.message })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function eq(actual, expected, label) {
|
|
41
|
+
const a = JSON.stringify(actual)
|
|
42
|
+
const e = JSON.stringify(expected)
|
|
43
|
+
if (a !== e) throw new Error((label ? label + ": " : "") + "esperaba " + e + " y obtuve " + a)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ok(condition, message) {
|
|
47
|
+
if (!condition) throw new Error(message)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function includes(haystack, needle, label) {
|
|
51
|
+
if (!haystack.includes(needle)) throw new Error((label ? label + ": " : "") + "falta " + JSON.stringify(needle))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function notIncludes(haystack, needle, label) {
|
|
55
|
+
if (haystack.includes(needle)) throw new Error((label ? label + ": " : "") + "no debería contener " + JSON.stringify(needle))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
59
|
+
// Carga de artefactos
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
const html = await fs.readFile(htmlPath, "utf8")
|
|
63
|
+
const source = await fs.readFile(sourcePath, "utf8")
|
|
64
|
+
|
|
65
|
+
// Ejecuta el mismo archivo que el browser carga como script clásico.
|
|
66
|
+
const sandbox = { console }
|
|
67
|
+
vm.createContext(sandbox)
|
|
68
|
+
vm.runInContext(source, sandbox, { filename: "kanban-source.js" })
|
|
69
|
+
const Core = sandbox.KanbanSource
|
|
70
|
+
check("kanban-source.js expone KanbanSource", () => {
|
|
71
|
+
ok(Core && typeof Core.normalizeDataset === "function", "no se exportó KanbanSource")
|
|
72
|
+
return Object.keys(Core).length + " exports"
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
76
|
+
// A. Contrato estático del HTML
|
|
77
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
const EXPECTED_COLUMNS = [
|
|
80
|
+
["todo", "TODO"],
|
|
81
|
+
["in_progress", "IN_PROGRESS"],
|
|
82
|
+
["review", "REVIEW"],
|
|
83
|
+
["done", "DONE"],
|
|
84
|
+
["rework", "REWORK"],
|
|
85
|
+
["blocked", "BLOCKED"],
|
|
86
|
+
["escalated", "ESCALATED"]
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
check("A1 · las 7 columnas existen en el HTML (TODO..ESCALATED)", () => {
|
|
90
|
+
for (const [id, label] of EXPECTED_COLUMNS) {
|
|
91
|
+
includes(html, 'class="column" data-state="' + id + '"', "columna " + id)
|
|
92
|
+
includes(html, 'id="col-' + id + '-title">' + label + "<", "título " + label)
|
|
93
|
+
includes(html, 'data-cards="' + id + '"', "contenedor de cards " + id)
|
|
94
|
+
}
|
|
95
|
+
eq(Core.COLUMN_IDS, EXPECTED_COLUMNS.map(([id]) => id), "orden de columnas en la fuente")
|
|
96
|
+
eq((html.match(/data-dropzone="true"/g) || []).length, 7, "dropzones")
|
|
97
|
+
return "7 columnas + 7 dropzones"
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
check("A2 · filtros por owner, tipo y estado", () => {
|
|
101
|
+
includes(html, 'id="filter-owner"', "select owner")
|
|
102
|
+
includes(html, 'id="filter-type"', "select tipo")
|
|
103
|
+
includes(html, 'id="filter-state"', "select estado")
|
|
104
|
+
includes(source, "f.owner", "filtro owner en lógica")
|
|
105
|
+
includes(source, "card.type !== f.type", "filtro tipo en lógica")
|
|
106
|
+
includes(source, "card.state !== f.state", "filtro estado en lógica")
|
|
107
|
+
return "3 selects + lógica matchesFilters"
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
check("A3 · dialog modal con detalle completo", () => {
|
|
111
|
+
includes(html, "<dialog id=\"card-modal\"", "dialog")
|
|
112
|
+
includes(html, 'aria-labelledby="card-modal-title"', "label del dialog")
|
|
113
|
+
for (const section of ["scope", "acceptance", "gate", "artifacts", "evidence"]) {
|
|
114
|
+
includes(source, 'data-detail="' + section + '"', "sección " + section)
|
|
115
|
+
}
|
|
116
|
+
includes(html, "modalContent.innerHTML = Core.modalHtml(card)", "render del modal")
|
|
117
|
+
includeFocusTrap()
|
|
118
|
+
return "scope · acceptance · gate · artifacts · evidence"
|
|
119
|
+
|
|
120
|
+
function includeFocusTrap() {
|
|
121
|
+
includes(html, 'id="card-modal-close"', "botón cerrar")
|
|
122
|
+
includes(html, "modal.showModal()", "showModal")
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
check("A4 · badges PASS / FAIL / NOT_RUN con colores distintos", () => {
|
|
127
|
+
eq(Core.GATE_LABELS, { pass: "PASS", fail: "FAIL", not_run: "NOT_RUN", na: "N/A" }, "labels")
|
|
128
|
+
const css = {
|
|
129
|
+
pass: /\.gate-badge--pass\s*\{\s*color:\s*var\(--accent\)/.test(html),
|
|
130
|
+
fail: /\.gate-badge--fail\s*\{\s*color:\s*var\(--danger\)/.test(html),
|
|
131
|
+
notRun: /\.gate-badge--not-run\s*\{\s*color:\s*var\(--warn\)/.test(html)
|
|
132
|
+
}
|
|
133
|
+
ok(css.pass && css.fail && css.notRun, "faltan reglas de color por outcome: " + JSON.stringify(css))
|
|
134
|
+
includes(html, "--accent: #4ADE80", "token accent")
|
|
135
|
+
includes(html, "--danger: #F87171", "token danger")
|
|
136
|
+
includes(html, "--warn: #FBBF24", "token warn")
|
|
137
|
+
return "PASS=accent FAIL=danger NOT_RUN=warn"
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
check("A5 · drag-drop visual, sin persistencia", () => {
|
|
141
|
+
includes(source, 'draggable="true"', "cards draggables")
|
|
142
|
+
for (const handler of ["onDragStart", "onDragOver", "onDrop", "onDragEnd"]) {
|
|
143
|
+
includes(html, handler, "handler " + handler)
|
|
144
|
+
}
|
|
145
|
+
const start = html.indexOf("// DRAG-DROP:start")
|
|
146
|
+
const end = html.indexOf("// DRAG-DROP:end")
|
|
147
|
+
ok(start !== -1 && end > start, "faltan los markers del bloque drag-drop")
|
|
148
|
+
const dnd = html.slice(start, end)
|
|
149
|
+
for (const forbidden of ["fetch(", "localStorage", "sessionStorage", "XMLHttpRequest", "navigator.sendBeacon"]) {
|
|
150
|
+
notIncludes(dnd, forbidden, "drag-drop no debe persistir")
|
|
151
|
+
}
|
|
152
|
+
includes(dnd, "data-visual-state", "marca visual del destino")
|
|
153
|
+
includes(dnd, "no persiste", "el usuario ve que no persiste")
|
|
154
|
+
return "drop mueve el nodo, no escribe storage ni red"
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
check("A6 · autosuficiente: abre con file:// (sin CDNs ni módulos)", () => {
|
|
158
|
+
notIncludes(html, 'type="module"', "no puede usar type=module (bloqueado en file://)")
|
|
159
|
+
includes(html, '<script src="./kanban-source.js"></script>', "script clásico local")
|
|
160
|
+
const remote = /(?:src|href)="https?:\/\//g.exec(html)
|
|
161
|
+
ok(!remote, "referencia remota encontrada: " + (remote ? remote[0] : ""))
|
|
162
|
+
notIncludes(html, "transition: all", "DESIGN.md prohíbe transition:all")
|
|
163
|
+
includes(html, "prefers-reduced-motion", "motion reducido")
|
|
164
|
+
includes(html, 'role="status" aria-live="polite"', "aria-live para anuncios")
|
|
165
|
+
return "sin red, sin build step, con fallback de movimiento"
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
169
|
+
// B. Lógica pura
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
const fixture = {
|
|
173
|
+
meta: { runId: "run-test", objective: "fixture", status: "running" },
|
|
174
|
+
agents: {
|
|
175
|
+
"e-alpha": { id: "e-alpha", displayName: "Alpha Dev", role: "frontend", state: "working" },
|
|
176
|
+
"e-beta": { id: "e-beta", displayName: "Beta Reviewer", role: "qa", state: "idle_available" }
|
|
177
|
+
},
|
|
178
|
+
cards: [
|
|
179
|
+
{
|
|
180
|
+
id: "T-01",
|
|
181
|
+
title: "card <con> html & comillas \"x\"",
|
|
182
|
+
type: "implementation",
|
|
183
|
+
state: "todo",
|
|
184
|
+
owner: "e-alpha",
|
|
185
|
+
reviewer: "e-beta",
|
|
186
|
+
acceptance: ["criterio A", "criterio B", "criterio C"],
|
|
187
|
+
gate: "npm test",
|
|
188
|
+
scope: { mayTouch: ["dashboard/**"], forbidden: ["src/**"] },
|
|
189
|
+
rejections: 2,
|
|
190
|
+
attempts: 3,
|
|
191
|
+
dependsOn: ["T-00"],
|
|
192
|
+
artifacts: ".oficina\\artifacts\\T-01",
|
|
193
|
+
result: null
|
|
194
|
+
},
|
|
195
|
+
{ id: "T-02", title: "review", type: "testing", state: "review", owner: null, reviewer: "e-beta", acceptance: [], scope: { mayTouch: [], forbidden: [] }, gate: "g", rejections: 0, attempts: 0, artifacts: ".oficina\\artifacts\\T-02" },
|
|
196
|
+
{ id: "T-03", title: "done", type: "docs", state: "done", owner: "e-beta", reviewer: null, acceptance: ["ok"], scope: { mayTouch: [], forbidden: [] }, gate: "g", rejections: 0, attempts: 1, artifacts: ".oficina\\artifacts\\T-03" },
|
|
197
|
+
{ id: "T-04", title: "blocked", type: "infra", state: "blocked", owner: "e-alpha", reviewer: null, acceptance: [], scope: { mayTouch: [], forbidden: [] }, gate: "g", rejections: 0, attempts: 0, artifacts: ".oficina\\artifacts\\T-04" },
|
|
198
|
+
{ id: "T-05", title: "escalated", type: "security", state: "escalated", owner: null, reviewer: null, acceptance: [], scope: { mayTouch: [], forbidden: [] }, gate: "g", rejections: 0, attempts: 0, artifacts: ".oficina\\artifacts\\T-05" },
|
|
199
|
+
{ id: "T-06", title: "rework", type: "bugfix", state: "rework", owner: "e-alpha", reviewer: "e-beta", acceptance: [], scope: { mayTouch: [], forbidden: [] }, gate: "g", rejections: 1, attempts: 2, artifacts: ".oficina\\artifacts\\T-06" },
|
|
200
|
+
{ id: "T-07", title: "in progress", type: "implementation", state: "in_progress", owner: "e-alpha", reviewer: null, acceptance: [], scope: { mayTouch: [], forbidden: [] }, gate: "g", rejections: 0, attempts: 0, artifacts: ".oficina\\artifacts\\T-07" },
|
|
201
|
+
{ id: "T-08", title: "estado desconocido", type: "refactor", state: "mystery", owner: null, reviewer: null, acceptance: [], scope: { mayTouch: [], forbidden: [] }, gate: "g", rejections: 0, attempts: 0, artifacts: "" }
|
|
202
|
+
],
|
|
203
|
+
gates: {
|
|
204
|
+
"T-01": {
|
|
205
|
+
ranAt: "2026-10-29T10:00:00.000Z",
|
|
206
|
+
technical: [{ phase: "build", command: "npm run build", outcome: "pass", detail: "exit 0", reason: "" }],
|
|
207
|
+
security: [{ phase: "secrets", command: "scan", outcome: "pass", detail: "0 hallazgos", reason: "" }],
|
|
208
|
+
technicalOutcome: "pass",
|
|
209
|
+
securityOutcome: "pass"
|
|
210
|
+
},
|
|
211
|
+
"T-03": {
|
|
212
|
+
ranAt: "2026-10-29T11:00:00.000Z",
|
|
213
|
+
technical: [{ phase: "test", command: "npm test", outcome: "fail", detail: "1 failed", reason: "regresión" }],
|
|
214
|
+
security: [],
|
|
215
|
+
technicalOutcome: "fail",
|
|
216
|
+
securityOutcome: "not_run"
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
evidence: {
|
|
220
|
+
"T-01": [
|
|
221
|
+
{ requirement: "criterio A", requirementIndex: 0, kind: "test", level: 2, ref: "tests/a.test.ts", note: "cubre A", passed: true },
|
|
222
|
+
{ requirement: "criterio B", requirementIndex: 1, kind: "artifact", level: 1, ref: "handoff.md", note: "documentado", passed: true }
|
|
223
|
+
],
|
|
224
|
+
"T-03": [
|
|
225
|
+
{ requirement: "ok", requirementIndex: 0, kind: "export", level: 2, ref: "tests/x", passed: false }
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const view = Core.normalizeDataset(fixture)
|
|
231
|
+
|
|
232
|
+
check("B1 · normalizeDataset resuelve agentes y cards", () => {
|
|
233
|
+
eq(view.cards.length, 8, "cards")
|
|
234
|
+
eq(Object.keys(view.agents).sort(), ["e-alpha", "e-beta"], "agentes")
|
|
235
|
+
const t1 = view.cards.find((c) => c.id === "T-01")
|
|
236
|
+
eq(t1.owner.name, "Alpha Dev", "owner resuelto")
|
|
237
|
+
eq(t1.reviewer.id, "e-beta", "reviewer resuelto")
|
|
238
|
+
eq(t1.owner.initials, "AD", "iniciales del owner")
|
|
239
|
+
return "8 cards · owners y reviewers normalizados"
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
check("B2 · estado desconocido cae en TODO (sin perder la card)", () => {
|
|
243
|
+
const t8 = view.cards.find((c) => c.id === "T-08")
|
|
244
|
+
eq(t8.state, "todo", "fallback de estado")
|
|
245
|
+
eq(t8.rawState, "mystery", "estado original preservado")
|
|
246
|
+
return "mystery → todo"
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
check("B3 · groupByColumn cubre las 7 columnas", () => {
|
|
250
|
+
const groups = Core.groupByColumn(view.cards)
|
|
251
|
+
eq(Object.keys(groups), EXPECTED_COLUMNS.map(([id]) => id), "claves")
|
|
252
|
+
eq(groups.in_progress.map((c) => c.id), ["T-07"], "in_progress")
|
|
253
|
+
eq(groups.done.map((c) => c.id), ["T-03"], "done")
|
|
254
|
+
eq(groups.rework.map((c) => c.id), ["T-06"], "rework")
|
|
255
|
+
eq(groups.blocked.map((c) => c.id), ["T-04"], "blocked")
|
|
256
|
+
eq(groups.escalated.map((c) => c.id), ["T-05"], "escalated")
|
|
257
|
+
return "todo=2 in_progress=1 review=1 done=1 rework=1 blocked=1 escalated=1"
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
check("B4 · filtro por owner (incluye sin asignar) y por tipo/estado/q", () => {
|
|
261
|
+
eq(Core.filterCards(view.cards, { owner: "e-alpha" }).map((c) => c.id), ["T-01", "T-04", "T-06", "T-07"], "owner e-alpha")
|
|
262
|
+
eq(Core.filterCards(view.cards, { owner: "unassigned" }).map((c) => c.id), ["T-02", "T-05", "T-08"], "sin asignar")
|
|
263
|
+
eq(Core.filterCards(view.cards, { type: "implementation" }).map((c) => c.id), ["T-01", "T-07"], "tipo")
|
|
264
|
+
eq(Core.filterCards(view.cards, { state: "review" }).map((c) => c.id), ["T-02"], "estado")
|
|
265
|
+
eq(Core.filterCards(view.cards, { q: "rewor" }).map((c) => c.id), ["T-06"], "búsqueda por título")
|
|
266
|
+
eq(Core.filterCards(view.cards, { owner: "e-alpha", type: "implementation", state: "in_progress" }).map((c) => c.id), ["T-07"], "filtros combinados")
|
|
267
|
+
return "owner · unassigned · type · state · q · combinado"
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
check("B5 · gateBadge mapea los tres estados visibles", () => {
|
|
271
|
+
eq(Core.gateBadge("pass").label, "PASS", "pass")
|
|
272
|
+
eq(Core.gateBadge("pass").className, "gate-badge gate-badge--pass", "clase pass")
|
|
273
|
+
eq(Core.gateBadge("FAIL").label, "FAIL", "fail")
|
|
274
|
+
eq(Core.gateBadge("fail").className, "gate-badge gate-badge--fail", "clase fail")
|
|
275
|
+
eq(Core.gateBadge("not_run").label, "NOT_RUN", "not_run")
|
|
276
|
+
eq(Core.gateBadge(undefined).label, "NOT_RUN", "ausente → NOT_RUN")
|
|
277
|
+
eq(Core.gateBadge("na").label, "N/A", "na")
|
|
278
|
+
return "PASS · FAIL · NOT_RUN · N/A"
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
check("B6 · gate de requisitos derivado de la evidencia", () => {
|
|
282
|
+
eq(Core.requirementsOutcome([]), "na", "sin criterios")
|
|
283
|
+
eq(Core.requirementsOutcome([{ evidence: [{ passed: true }] }, { evidence: [{ passed: true }] }]), "pass", "todo cubierto")
|
|
284
|
+
eq(Core.requirementsOutcome([{ evidence: [{ passed: true }] }, { evidence: [] }]), "not_run", "cobertura incompleta ≠ pass")
|
|
285
|
+
eq(Core.requirementsOutcome([{ evidence: [{ passed: false }] }]), "fail", "evidencia de fallo")
|
|
286
|
+
return "na · pass · not_run · fail"
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
check("B7 · gateOutcome agrega sin promediar (FAIL > NOT_RUN > PASS)", () => {
|
|
290
|
+
const gate = (t, s, r) => ({ technical: { outcome: t }, security: { outcome: s }, requirements: { outcome: r } })
|
|
291
|
+
eq(Core.gateOutcome(gate("pass", "pass", "pass")), "pass", "todo verde")
|
|
292
|
+
eq(Core.gateOutcome(gate("pass", "fail", "pass")), "fail", "un fail alcanza")
|
|
293
|
+
eq(Core.gateOutcome(gate("pass", "pass", "not_run")), "not_run", "not_run no es verde")
|
|
294
|
+
eq(Core.gateOutcome(gate("na", "na", "na")), "na", "todo n/a")
|
|
295
|
+
eq(Core.gateOutcome(null), "not_run", "sin gate")
|
|
296
|
+
return "sin promedios: fail domina, not_run bloquea"
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
check("B8 · acceptanceProgress y checklist por card", () => {
|
|
300
|
+
const t1 = view.cards.find((c) => c.id === "T-01")
|
|
301
|
+
eq(Core.acceptanceProgress(t1), { total: 3, covered: 2, missing: 1, pct: 67 }, "progreso T-01")
|
|
302
|
+
eq(t1.acceptance[0].satisfied, true, "criterio 0 cubierto")
|
|
303
|
+
eq(t1.acceptance[2].satisfied, false, "criterio 2 sin evidencia")
|
|
304
|
+
const t3 = view.cards.find((c) => c.id === "T-03")
|
|
305
|
+
eq(t3.gate.requirements.outcome, "fail", "evidencia passed:false → FAIL")
|
|
306
|
+
eq(t3.gateOutcome, "fail", "gate agregado T-03")
|
|
307
|
+
return "2/3 · evidencia enlazada por índice y por texto"
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
check("B9 · cardHtml expone id, título, avatares, rechazos, checklist y badges", () => {
|
|
311
|
+
const t1 = view.cards.find((c) => c.id === "T-01")
|
|
312
|
+
const htmlCard = Core.cardHtml(t1)
|
|
313
|
+
includes(htmlCard, 'data-card-id="T-01"', "id")
|
|
314
|
+
includes(htmlCard, "card <con> html & comillas "x"", "título escapado")
|
|
315
|
+
notIncludes(htmlCard, "<con>", "no debe inyectar HTML del título")
|
|
316
|
+
includes(htmlCard, 'data-owner="e-alpha"', "owner")
|
|
317
|
+
includes(htmlCard, 'data-reviewer="e-beta"', "reviewer")
|
|
318
|
+
includes(htmlCard, ">AD<", "iniciales owner")
|
|
319
|
+
includes(htmlCard, 'data-rejections="2"', "rejection count")
|
|
320
|
+
includes(htmlCard, "criterio A", "checklist")
|
|
321
|
+
includes(htmlCard, "check-item is-checked", "criterio cubierto")
|
|
322
|
+
includes(htmlCard, "check-item is-empty", "criterio sin evidencia")
|
|
323
|
+
includes(htmlCard, 'data-outcome="pass"', "badge PASS")
|
|
324
|
+
includes(htmlCard, 'data-outcome="not_run"', "badge NOT_RUN")
|
|
325
|
+
includes(htmlCard, 'draggable="true"', "draggable")
|
|
326
|
+
includes(htmlCard, 'role="button"', "accesible por teclado")
|
|
327
|
+
|
|
328
|
+
const t2 = view.cards.find((c) => c.id === "T-02")
|
|
329
|
+
const htmlUnassigned = Core.cardHtml(t2)
|
|
330
|
+
includes(htmlUnassigned, "sin asignar", "sin owner")
|
|
331
|
+
includes(htmlUnassigned, "sin criterios declarados", "sin criterios")
|
|
332
|
+
return "T-01 completo + T-02 sin owner"
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
check("B10 · modalHtml incluye scope, acceptance, gate, artifacts y evidence", () => {
|
|
336
|
+
const t1 = view.cards.find((c) => c.id === "T-01")
|
|
337
|
+
const modal = Core.modalHtml(t1)
|
|
338
|
+
for (const section of ["scope", "acceptance", "gate", "artifacts", "evidence"]) {
|
|
339
|
+
includes(modal, 'data-detail="' + section + '"', "sección " + section)
|
|
340
|
+
}
|
|
341
|
+
includes(modal, "<code>dashboard/**</code>", "mayTouch")
|
|
342
|
+
includes(modal, "<code>src/**</code>", "forbidden")
|
|
343
|
+
includes(modal, "criterio A", "acceptance")
|
|
344
|
+
includes(modal, "npm test", "gate")
|
|
345
|
+
includes(modal, "../.oficina/artifacts/T-01/handoff.md", "link handoff")
|
|
346
|
+
includes(modal, "../.oficina/artifacts/T-01/evidence.md", "link evidence")
|
|
347
|
+
includes(modal, "../.oficina/artifacts/T-01/gates.md", "link gates")
|
|
348
|
+
includes(modal, "tests/a.test.ts", "evidencia citada")
|
|
349
|
+
includes(modal, "data-evidence-kind=\"test\"", "tipo de evidencia")
|
|
350
|
+
includes(modal, 'data-gate-section="technical"', "gate técnico")
|
|
351
|
+
includes(modal, 'data-gate-section="requirements"', "gate de requisitos")
|
|
352
|
+
notIncludes(modal, 'class="detail-result"', "sin result no inventa un veredicto")
|
|
353
|
+
notIncludes(modal, "transition: all", "sin animación global")
|
|
354
|
+
return "5 secciones + links a artifacts y evidencia"
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
check("B11 · helpers deterministas", () => {
|
|
358
|
+
eq(Core.initials("e-xr-immersive-developer"), "XD", "iniciales de id")
|
|
359
|
+
eq(Core.initials(""), "·", "vacío")
|
|
360
|
+
eq(Core.formatRelative("2026-10-29T10:00:00.000Z", Date.parse("2026-10-29T10:05:00.000Z")), "hace 5m", "relativo")
|
|
361
|
+
eq(Core.percent(0, 0), 0, "división por cero")
|
|
362
|
+
eq(Core.escapeHtml("<a href=\"x\">&'"), "<a href="x">&'", "escape")
|
|
363
|
+
return "initials · formatRelative · percent · escapeHtml"
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
367
|
+
// C. Dataset embebido real
|
|
368
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
check("C1 · dataset embebido parsea y tiene la forma esperada", () => {
|
|
371
|
+
const match = /<script[^>]+id="kanban-dataset"[^>]*>([\s\S]*?)<\/script>/.exec(html)
|
|
372
|
+
ok(match, "no encontré el script #kanban-dataset")
|
|
373
|
+
const dataset = JSON.parse(match[1])
|
|
374
|
+
ok(Array.isArray(dataset.cards), "cards no es array")
|
|
375
|
+
ok(dataset.cards.length > 0, "el dataset está vacío: corré node dashboard/build-kanban-data.mjs")
|
|
376
|
+
const real = Core.normalizeDataset(dataset)
|
|
377
|
+
eq(real.cards.length, dataset.cards.length, "cards normalizadas")
|
|
378
|
+
const allowed = ["pass", "fail", "not_run", "na"]
|
|
379
|
+
for (const card of real.cards) {
|
|
380
|
+
ok(allowed.includes(card.gateOutcome), "gateOutcome inválido en " + card.id)
|
|
381
|
+
ok(typeof card.title === "string" && card.title.length > 0, "título vacío en " + card.id)
|
|
382
|
+
ok(Array.isArray(card.acceptance), "acceptance no es array en " + card.id)
|
|
383
|
+
ok(card.scope && Array.isArray(card.scope.mayTouch), "scope inválido en " + card.id)
|
|
384
|
+
}
|
|
385
|
+
return dataset.cards.length + " cards reales · " + Object.keys(dataset.agents).length + " agentes"
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
check("C2 · el HTML renderiza datos reales sin placeholder", () => {
|
|
389
|
+
const match = /<script[^>]+id="kanban-dataset"[^>]*>([\s\S]*?)<\/script>/.exec(html)
|
|
390
|
+
const dataset = JSON.parse(match[1])
|
|
391
|
+
const real = Core.normalizeDataset(dataset)
|
|
392
|
+
const seeded = dataset.cards.filter((card) => card.id === "T-07")
|
|
393
|
+
ok(seeded.length === 1, "falta la card T-07 en el snapshot")
|
|
394
|
+
const rendered = Core.columnBodyHtml({ id: "todo", label: "TODO", hint: "" }, [real.cards.find((c) => c.id === "T-07")])
|
|
395
|
+
includes(rendered, 'data-card-id="T-07"', "render de T-07")
|
|
396
|
+
return "T-07 presente y renderizable"
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
400
|
+
// D. Parser de gates.md
|
|
401
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
check("D1 · parseGates lee el formato de renderGate", () => {
|
|
404
|
+
const markdown = [
|
|
405
|
+
"# Gates — T-99",
|
|
406
|
+
"",
|
|
407
|
+
"**Corredor:** 2026-10-29T03:28:55.630Z",
|
|
408
|
+
"",
|
|
409
|
+
"## Técnico",
|
|
410
|
+
"",
|
|
411
|
+
"| # | Fase | Comando | Resultado | Detalle | Motivo |",
|
|
412
|
+
"|---|------|---------|-----------|---------|--------|",
|
|
413
|
+
"| 1 | build | `npm run build` | PASS | exit 0 en 11254ms | — |",
|
|
414
|
+
"| 2 | test | `npm test` | FAIL | exit 1 | regresión en gates |",
|
|
415
|
+
"",
|
|
416
|
+
"## Seguridad",
|
|
417
|
+
"",
|
|
418
|
+
"| # | Fase | Comando | Resultado | Detalle | Motivo |",
|
|
419
|
+
"|---|------|---------|-----------|---------|--------|",
|
|
420
|
+
"| 1 | secrecy | `scan` | NA | sin scanner | — |",
|
|
421
|
+
"",
|
|
422
|
+
"**Gate técnico:** FAIL",
|
|
423
|
+
"",
|
|
424
|
+
"**Gate seguridad:** NA",
|
|
425
|
+
"",
|
|
426
|
+
"**Veredicto:** FAIL"
|
|
427
|
+
].join("\n")
|
|
428
|
+
const parsed = parseGates(markdown)
|
|
429
|
+
eq(parsed.ranAt, "2026-10-29T03:28:55.630Z", "corredor")
|
|
430
|
+
eq(parsed.technical.length, 2, "fases técnicas")
|
|
431
|
+
eq(parsed.technical[1].outcome, "fail", "FAIL parseado")
|
|
432
|
+
eq(parsed.technical[1].reason, "regresión en gates", "motivo")
|
|
433
|
+
eq(parsed.security.length, 1, "fases de seguridad")
|
|
434
|
+
eq(parsed.security[0].outcome, "na", "NA parseado")
|
|
435
|
+
eq(parsed.technicalOutcome, "fail", "outcome técnico")
|
|
436
|
+
eq(parsed.securityOutcome, "na", "outcome seguridad")
|
|
437
|
+
eq(parsed.verdict, "fail", "veredicto")
|
|
438
|
+
eq(parseGates("").verdict, "not_run", "sin gates.md")
|
|
439
|
+
return "fases + outcomes + motivo + not_run sin archivo"
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
443
|
+
// Reporte
|
|
444
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
const failed = results.filter((r) => !r.ok)
|
|
447
|
+
const width = Math.max(...results.map((r) => r.name.length))
|
|
448
|
+
for (const r of results) {
|
|
449
|
+
const status = r.ok ? "PASS" : "FAIL"
|
|
450
|
+
console.log(status + " " + r.name.padEnd(width) + " " + r.detail)
|
|
451
|
+
}
|
|
452
|
+
console.log("")
|
|
453
|
+
console.log((results.length - failed.length) + "/" + results.length + " checks en verde" + (failed.length ? " · " + failed.length + " FALLARON" : ""))
|
|
454
|
+
process.exitCode = failed.length ? 1 : 0
|