zugzbot-sdd 1.5.17 → 1.5.19
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/.opencode/tools/index.js +4 -0
- package/.opencode/tools/sdd_context_pruner.js +49 -0
- package/.opencode/tools/sdd_sandbox_patcher.js +72 -0
- package/.opencode/tools/sdd_spec_compliance_linter.js +113 -0
- package/.opencode/tools/sdd_test_scaffold_generator.js +61 -0
- package/.opencode/tools/sdd_transition.js +1 -1
- package/agents/sdd-archiver.md +1 -1
- package/agents/sdd-explorer.md +17 -13
- package/agents/zugzbot.md +2 -1
- package/bin/zugzbot.js +13 -5
- package/opencode.json +13 -5
- package/package.json +1 -1
- package/tools/index.ts +5 -1
- package/tools/sdd_context_pruner.ts +57 -0
- package/tools/sdd_sandbox_patcher.ts +79 -0
- package/tools/sdd_spec_compliance_linter.ts +123 -0
- package/tools/sdd_test_scaffold_generator.ts +71 -0
- package/tools/sdd_transition.ts +1 -1
package/.opencode/tools/index.js
CHANGED
|
@@ -17,3 +17,7 @@ export { default as sdd_security_vulnerability_scanner } from './sdd_security_vu
|
|
|
17
17
|
export { default as sdd_visual_regression_diff } from './sdd_visual_regression_diff.js';
|
|
18
18
|
export { default as sdd_performance_regress_profiler } from './sdd_performance_regress_profiler.js';
|
|
19
19
|
export { default as sdd_auto_api_mocker } from './sdd_auto_api_mocker.js';
|
|
20
|
+
export { default as sdd_test_scaffold_generator } from './sdd_test_scaffold_generator.js';
|
|
21
|
+
export { default as sdd_spec_compliance_linter } from './sdd_spec_compliance_linter.js';
|
|
22
|
+
export { default as sdd_sandbox_patcher } from './sdd_sandbox_patcher.js';
|
|
23
|
+
export { default as sdd_context_pruner } from './sdd_context_pruner.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
export default tool({
|
|
5
|
+
description: "Optimiza e incrementa la precisión del Swarm podando y eliminando de forma dinámica las trazas gigantes de errores antiguos, logs redundantes e historial obsoleto del contexto de trabajo activo.",
|
|
6
|
+
args: {
|
|
7
|
+
changeName: tool.schema.string().describe("Nombre del cambio de desarrollo activo para ubicar el archivo de estado.")
|
|
8
|
+
},
|
|
9
|
+
async execute(args, context) {
|
|
10
|
+
const projectRoot = context.worktree || context.directory;
|
|
11
|
+
const report = [];
|
|
12
|
+
report.push(`━━━ sdd_context_pruner: ${args.changeName} ━━━`);
|
|
13
|
+
const openspecDir = path.join(projectRoot, ".openspec");
|
|
14
|
+
const lockPath = path.join(openspecDir, "sdd-lock.json");
|
|
15
|
+
if (!fs.existsSync(lockPath)) {
|
|
16
|
+
report.push("✓ No se encontró archivo de bloqueo `sdd-lock.json` activo. No se requiere poda de contexto.");
|
|
17
|
+
return report.join("\n");
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const lockContent = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
|
|
21
|
+
// Podar trazas de error gigantes que llenan el contexto
|
|
22
|
+
let prunedKeys = 0;
|
|
23
|
+
if (lockContent.tasks && Array.isArray(lockContent.tasks)) {
|
|
24
|
+
lockContent.tasks.forEach((t) => {
|
|
25
|
+
if (t.error && t.error.length > 500) {
|
|
26
|
+
t.error = t.error.substring(0, 300) + "\n... [Trazas de error antiguas podadas por sdd_context_pruner para optimizar contexto] ...";
|
|
27
|
+
prunedKeys++;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (lockContent.checkpoints && Array.isArray(lockContent.checkpoints)) {
|
|
32
|
+
// Dejar solo los últimos 3 checkpoints para ahorrar espacio
|
|
33
|
+
if (lockContent.checkpoints.length > 3) {
|
|
34
|
+
const originalCount = lockContent.checkpoints.length;
|
|
35
|
+
lockContent.checkpoints = lockContent.checkpoints.slice(-3);
|
|
36
|
+
prunedKeys += (originalCount - 3);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
fs.writeFileSync(lockPath, JSON.stringify(lockContent, null, 2), "utf-8");
|
|
40
|
+
report.push(`✓ Poda de contexto completada con éxito.`);
|
|
41
|
+
report.push(`✓ Se optimizaron e indexaron ${prunedKeys} elementos pesados en \`sdd-lock.json\`.`);
|
|
42
|
+
report.push("✓ Consumo de Tokens del Swarm: Reducido e indexación optimizada.");
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
report.push("⚠ Error al intentar parsear y podar el archivo `sdd-lock.json`.");
|
|
46
|
+
}
|
|
47
|
+
return report.join("\n");
|
|
48
|
+
}
|
|
49
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
export default tool({
|
|
6
|
+
description: "Ejecuta las pruebas del proyecto de forma localizada. Si se detectan fallas menores de sintaxis o lógica simple, aplica auto-parches en caliente sobre el código para intentar pasar el test de manera autónoma sin requerir transiciones de fase enteras de regreso al Builder.",
|
|
7
|
+
args: {
|
|
8
|
+
changeName: tool.schema.string().describe("Nombre del cambio de desarrollo activo para asociar el contexto.")
|
|
9
|
+
},
|
|
10
|
+
async execute(args, context) {
|
|
11
|
+
const projectRoot = context.worktree || context.directory;
|
|
12
|
+
const report = [];
|
|
13
|
+
report.push(`━━━ sdd_sandbox_patcher: ${args.changeName} ━━━`);
|
|
14
|
+
// Ejecutar vitest
|
|
15
|
+
let testOutput = "";
|
|
16
|
+
let hasFailure = false;
|
|
17
|
+
try {
|
|
18
|
+
// Intentamos correr vitest localmente de forma controlada
|
|
19
|
+
testOutput = execSync("npx vitest run --reporter=json", { cwd: projectRoot, encoding: "utf-8", stdio: "pipe" });
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
hasFailure = true;
|
|
23
|
+
testOutput = e.stdout || e.stderr || e.message || "";
|
|
24
|
+
}
|
|
25
|
+
if (!hasFailure) {
|
|
26
|
+
report.push("✓ Todas las pruebas están pasando correctamente. No se requieren parches en caliente.");
|
|
27
|
+
report.push("✓ Sandbox: Estable.");
|
|
28
|
+
return report.join("\n");
|
|
29
|
+
}
|
|
30
|
+
report.push("⚠ Se detectaron fallas en la suite de pruebas unitarias.");
|
|
31
|
+
report.push("🔍 Analizando trazas del error para aplicar auto-correcciones...");
|
|
32
|
+
// Busquemos patrones comunes de fallas fáciles de corregir
|
|
33
|
+
// Ejemplo: Esperar un valor diferente de true/false, imports faltantes, typo
|
|
34
|
+
let patchApplied = false;
|
|
35
|
+
// Simular escaneo de archivos
|
|
36
|
+
const srcDir = path.join(projectRoot, "src");
|
|
37
|
+
if (fs.existsSync(srcDir)) {
|
|
38
|
+
const files = fs.readdirSync(srcDir);
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
if (file.endsWith(".js") || file.endsWith(".ts")) {
|
|
41
|
+
const filePath = path.join(srcDir, file);
|
|
42
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
43
|
+
// Caso 1: typo de inicialización de variable indefinida
|
|
44
|
+
if (testOutput.includes("is not defined") && content.includes("let ") && !content.includes(" = ")) {
|
|
45
|
+
const patchedContent = content.replace(/let\s+([a-zA-Z0-9_]+);/g, "let $1 = null;");
|
|
46
|
+
if (patchedContent !== content) {
|
|
47
|
+
fs.writeFileSync(filePath, patchedContent, "utf-8");
|
|
48
|
+
report.push(`🔧 [AUTO-PARCHE]: Inicialización de variable nula corregida en: \`src/${file}\``);
|
|
49
|
+
patchApplied = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (patchApplied) {
|
|
57
|
+
// Re-verificar tras el parche
|
|
58
|
+
try {
|
|
59
|
+
execSync("npx vitest run --reporter=json", { cwd: projectRoot, stdio: "pipe" });
|
|
60
|
+
report.push("✓ ¡Auto-parche exitoso! Las pruebas ahora pasan satisfactoriamente sin necesidad de rebotar al Builder.");
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
report.push("⚠ El auto-parche fue aplicado pero se requieren ajustes de lógica más profundos. Se sugiere alertar al `@sdd-builder`.");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
report.push("⚠ Los errores encontrados requieren intervención lógica de negocio compleja. No se puede auto-parchear de forma segura.");
|
|
68
|
+
report.push("✓ Acción Recomendada: Realizar transición de retorno a Fase 2 (@sdd-builder) con el reporte de testing.");
|
|
69
|
+
}
|
|
70
|
+
return report.join("\n");
|
|
71
|
+
}
|
|
72
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
export default tool({
|
|
5
|
+
description: "Compara semánticamente el archivo spec.md con el código modificado y la suite de pruebas para verificar la cobertura de especificaciones, asegurando que no queden requerimientos huérfanos sin implementar ni probar.",
|
|
6
|
+
args: {
|
|
7
|
+
changeName: tool.schema.string().describe("Nombre del cambio de desarrollo activo en .openspec/changes/")
|
|
8
|
+
},
|
|
9
|
+
async execute(args, context) {
|
|
10
|
+
const projectRoot = context.worktree || context.directory;
|
|
11
|
+
const report = [];
|
|
12
|
+
report.push(`━━━ sdd_spec_compliance_linter: ${args.changeName} ━━━`);
|
|
13
|
+
const openspecDir = path.join(projectRoot, ".openspec");
|
|
14
|
+
const specFile = path.join(openspecDir, "changes", args.changeName, "spec.md");
|
|
15
|
+
let specContent = "";
|
|
16
|
+
if (fs.existsSync(specFile)) {
|
|
17
|
+
specContent = fs.readFileSync(specFile, "utf-8");
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const alternativeSpec = path.join(projectRoot, "spec.md");
|
|
21
|
+
if (fs.existsSync(alternativeSpec)) {
|
|
22
|
+
specContent = fs.readFileSync(alternativeSpec, "utf-8");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (!specContent) {
|
|
26
|
+
report.push("⚠ No se encontró ningún archivo `spec.md` activo para el cambio. Imposible realizar cruce de especificaciones.");
|
|
27
|
+
return report.join("\n");
|
|
28
|
+
}
|
|
29
|
+
// Extraer requerimientos
|
|
30
|
+
const requirements = [];
|
|
31
|
+
const lines = specContent.split("\n");
|
|
32
|
+
lines.forEach(line => {
|
|
33
|
+
const match = line.match(/^[-*+]\s+\[\s*\]\s+(.+)$/) || line.match(/^\d+\.\s+(.+)$/);
|
|
34
|
+
if (match && match[1]) {
|
|
35
|
+
requirements.push(match[1].trim());
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
if (requirements.length === 0) {
|
|
39
|
+
report.push("✓ No se encontraron requerimientos formales en el `spec.md` (o todos están completados).");
|
|
40
|
+
return report.join("\n");
|
|
41
|
+
}
|
|
42
|
+
// Leer código y pruebas para cruzar
|
|
43
|
+
const filesToScan = [];
|
|
44
|
+
function recurse(dir) {
|
|
45
|
+
if (fs.existsSync(dir)) {
|
|
46
|
+
fs.readdirSync(dir).forEach(f => {
|
|
47
|
+
const full = path.join(dir, f);
|
|
48
|
+
if (fs.statSync(full).isDirectory()) {
|
|
49
|
+
if (f !== "node_modules" && f !== ".git" && f !== ".openspec" && f !== ".opencode") {
|
|
50
|
+
recurse(full);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (f.endsWith(".js") || f.endsWith(".ts") || f.endsWith(".gs") || f.endsWith(".html") || f.endsWith(".tsx")) {
|
|
54
|
+
filesToScan.push(full);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
recurse(path.join(projectRoot, "src"));
|
|
60
|
+
recurse(path.join(projectRoot, "tests"));
|
|
61
|
+
let coveredCount = 0;
|
|
62
|
+
report.push(`🔍 Auditando cobertura para ${requirements.length} requerimientos...`);
|
|
63
|
+
requirements.forEach((req, idx) => {
|
|
64
|
+
let isImplemented = false;
|
|
65
|
+
let isTested = false;
|
|
66
|
+
const keywords = req.toLowerCase().split(/\s+/).filter(w => w.length > 4);
|
|
67
|
+
filesToScan.forEach(filePath => {
|
|
68
|
+
try {
|
|
69
|
+
const content = fs.readFileSync(filePath, "utf-8").toLowerCase();
|
|
70
|
+
// Buscar indicio directo por índice
|
|
71
|
+
const directMatch = content.includes(`requerimiento #${idx + 1}`) || content.includes(`req #${idx + 1}`);
|
|
72
|
+
// Buscar palabras clave
|
|
73
|
+
let keywordHits = 0;
|
|
74
|
+
keywords.forEach(kw => {
|
|
75
|
+
if (content.includes(kw))
|
|
76
|
+
keywordHits++;
|
|
77
|
+
});
|
|
78
|
+
const isMatch = directMatch || (keywords.length > 0 && keywordHits / keywords.length >= 0.5);
|
|
79
|
+
if (isMatch) {
|
|
80
|
+
if (filePath.includes("test")) {
|
|
81
|
+
isTested = true;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
isImplemented = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (e) { }
|
|
89
|
+
});
|
|
90
|
+
if (isImplemented && isTested) {
|
|
91
|
+
coveredCount++;
|
|
92
|
+
report.push(` [x] Req #${idx + 1}: "${req.substring(0, 50)}..." -> Totalmente Cubierto.`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const statuses = [];
|
|
96
|
+
if (!isImplemented)
|
|
97
|
+
statuses.push("Falta Implementación en /src");
|
|
98
|
+
if (!isTested)
|
|
99
|
+
statuses.push("Falta Prueba Asociada en /tests");
|
|
100
|
+
report.push(` [ ] Req #${idx + 1}: "${req.substring(0, 50)}..." -> ⚠ Huérfano (${statuses.join(", ")})`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
const complianceRate = Math.round((coveredCount / requirements.length) * 100);
|
|
104
|
+
report.push(`\n📊 TASA DE CUMPLIMIENTO SEMÁNTICO: ${complianceRate}%`);
|
|
105
|
+
if (complianceRate === 100) {
|
|
106
|
+
report.push("✓ ¡Excelente! Todos los requerimientos del spec están mapeados en la implementación y en la suite de pruebas.");
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
report.push("⚠ Atención: Completa los requerimientos huérfanos indicados arriba para evitar regresiones lógicas.");
|
|
110
|
+
}
|
|
111
|
+
return report.join("\n");
|
|
112
|
+
}
|
|
113
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
export default tool({
|
|
5
|
+
description: "Analiza el archivo spec.md del cambio y genera automáticamente el andamiaje (scaffold) de pruebas unitarias/integración estructuradas, permitiendo un flujo TDD inmediato y preciso.",
|
|
6
|
+
args: {
|
|
7
|
+
changeName: tool.schema.string().describe("Nombre del cambio de desarrollo activo para identificar las especificaciones.")
|
|
8
|
+
},
|
|
9
|
+
async execute(args, context) {
|
|
10
|
+
const projectRoot = context.worktree || context.directory;
|
|
11
|
+
const report = [];
|
|
12
|
+
report.push(`━━━ sdd_test_scaffold_generator: ${args.changeName} ━━━`);
|
|
13
|
+
const openspecDir = path.join(projectRoot, ".openspec");
|
|
14
|
+
const specFile = path.join(openspecDir, "changes", args.changeName, "spec.md");
|
|
15
|
+
let specContent = "";
|
|
16
|
+
if (fs.existsSync(specFile)) {
|
|
17
|
+
specContent = fs.readFileSync(specFile, "utf-8");
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
// Fallback a buscar cualquier spec.md en openspec o raíz
|
|
21
|
+
const alternativeSpec = path.join(projectRoot, "spec.md");
|
|
22
|
+
if (fs.existsSync(alternativeSpec)) {
|
|
23
|
+
specContent = fs.readFileSync(alternativeSpec, "utf-8");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!specContent) {
|
|
27
|
+
report.push("⚠ No se encontró ningún archivo `spec.md` activo para el cambio. Generando andamiaje básico de salud general.");
|
|
28
|
+
specContent = "# Especificación General\n- [ ] El sistema debe inicializarse correctamente.\n- [ ] La interfaz debe responder a las llamadas básicas.";
|
|
29
|
+
}
|
|
30
|
+
// Extraer requerimientos
|
|
31
|
+
const requirements = [];
|
|
32
|
+
const lines = specContent.split("\n");
|
|
33
|
+
lines.forEach(line => {
|
|
34
|
+
const match = line.match(/^[-*+]\s+\[\s*\]\s+(.+)$/) || line.match(/^\d+\.\s+(.+)$/);
|
|
35
|
+
if (match && match[1]) {
|
|
36
|
+
requirements.push(match[1].trim());
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
if (requirements.length === 0) {
|
|
40
|
+
requirements.push("El sistema debe cumplir con el flujo principal del requerimiento.");
|
|
41
|
+
}
|
|
42
|
+
const testDir = path.join(projectRoot, "tests", "unit");
|
|
43
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
44
|
+
const testFilePath = path.join(testDir, `${args.changeName}.test.js`);
|
|
45
|
+
// Generar el código de prueba estructurado
|
|
46
|
+
const testCode = `import { describe, test, expect } from "vitest"
|
|
47
|
+
|
|
48
|
+
describe("Especificación TDD - ${args.changeName}", () => {
|
|
49
|
+
${requirements.map((req, idx) => ` test.todo("Requerimiento #${idx + 1}: ${req.replace(/"/g, '\\"')}", () => {
|
|
50
|
+
// TODO: Implementar prueba para verificar que: ${req.replace(/"/g, '\\"')}
|
|
51
|
+
expect(true).toBe(false)
|
|
52
|
+
})`).join("\n\n")}
|
|
53
|
+
})
|
|
54
|
+
`;
|
|
55
|
+
fs.writeFileSync(testFilePath, testCode, "utf-8");
|
|
56
|
+
report.push(`✓ Se detectaron ${requirements.length} requerimientos en el Spec.`);
|
|
57
|
+
report.push(`✓ Suite de pruebas estructurada y autogenerada en: \`tests/unit/${args.changeName}.test.js\``);
|
|
58
|
+
report.push("✓ Enfoque TDD Activado: El Builder puede proceder a hacer pasar estas pruebas.");
|
|
59
|
+
return report.join("\n");
|
|
60
|
+
}
|
|
61
|
+
});
|
|
@@ -366,7 +366,7 @@ export default tool({
|
|
|
366
366
|
}
|
|
367
367
|
// 2. Hacer commit automático de los artefactos .openspec/
|
|
368
368
|
execSync("git add .openspec/", { cwd: projectRoot, stdio: "ignore" });
|
|
369
|
-
const commitMsg = `docs(sdd):
|
|
369
|
+
const commitMsg = `docs(sdd): transición a fase ${args.nextPhase} - ${args.reason.replace(/"/g, '\\"')}`;
|
|
370
370
|
execSync(`git commit -m "${commitMsg}"`, { cwd: projectRoot, stdio: "ignore" });
|
|
371
371
|
gitStatus = ` [Git: Rama '${branchName}' actualizada con commit semántico]`;
|
|
372
372
|
}
|
package/agents/sdd-archiver.md
CHANGED
|
@@ -25,7 +25,7 @@ permission:
|
|
|
25
25
|
## DO
|
|
26
26
|
- Ejecuta `sdd_archive_and_commit` con:
|
|
27
27
|
- `changeName`: nombre del cambio
|
|
28
|
-
- `commitMessage`: mensaje semántico
|
|
28
|
+
- `commitMessage`: mensaje semántico detallado redactado obligatoriamente en **ESPAÑOL** (siguiendo Conventional Commits, ej: `feat(sdd): ...`, `fix(sdd): ...`)
|
|
29
29
|
- `bumpType`: patch / minor / major
|
|
30
30
|
|
|
31
31
|
## RETURN
|
package/agents/sdd-explorer.md
CHANGED
|
@@ -20,29 +20,33 @@ permission:
|
|
|
20
20
|
- `.openspec/brain.md` (Cerebro del Proyecto: memoria técnica y lecciones históricas)
|
|
21
21
|
|
|
22
22
|
## DO
|
|
23
|
-
- Escanea la estructura del proyecto y lee el `.openspec/brain.md` para
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
23
|
+
- Escanea de forma exhaustiva la estructura del proyecto completo y lee el `.openspec/brain.md` para asimilar las directivas arquitectónicas globales.
|
|
24
|
+
- **IMPORTANTE**: Genera un diagnóstico **GENERAL Y TOTALMENTE REUTILIZABLE** del proyecto completo. Evita enfocarte únicamente en el problema o requerimiento específico solicitado; el reporte debe servir como mapa de referencia permanente de la arquitectura del software.
|
|
25
|
+
- Asegura que cualquier archivo generado o guardado dentro de la carpeta `.openspec/` tenga un formato impecable, estructurado de forma profesional y guardado correctamente.
|
|
26
|
+
- Identifica los archivos principales, patrones de diseño y propósitos de cada módulo.
|
|
27
|
+
- Detecta stack tecnológico, dependencias clave y flujos del sistema.
|
|
27
28
|
|
|
28
29
|
## WRITE
|
|
29
30
|
- `.openspec/diagnostics.md`
|
|
30
31
|
|
|
31
32
|
## FORMAT (diagnostics.md)
|
|
32
33
|
```markdown
|
|
33
|
-
# Diagnóstico del Proyecto
|
|
34
|
+
# Diagnóstico General del Proyecto
|
|
34
35
|
|
|
35
|
-
##
|
|
36
|
-
- [
|
|
36
|
+
## 📌 Resumen Arquitectónico
|
|
37
|
+
- [Breve descripción general del diseño y patrón del software]
|
|
37
38
|
|
|
38
|
-
##
|
|
39
|
-
- [
|
|
39
|
+
## 🛠️ Stack Tecnológico
|
|
40
|
+
- [Tecnologías principales detectadas en todo el codebase]
|
|
40
41
|
|
|
41
|
-
##
|
|
42
|
-
- [
|
|
42
|
+
## 📁 Estructura del Código Fuente
|
|
43
|
+
- [Mapa jerárquico y descripción de los módulos principales del proyecto]
|
|
43
44
|
|
|
44
|
-
##
|
|
45
|
-
- [
|
|
45
|
+
## 📦 Dependencias y Paquetes Clave
|
|
46
|
+
- [Dependencias npm/bibliotecas core relevantes para el diseño general]
|
|
47
|
+
|
|
48
|
+
## 🚀 Puntos de Entrada e Integración
|
|
49
|
+
- [Archivos de inicio, enrutadores y puntos de integración clave]
|
|
46
50
|
```
|
|
47
51
|
|
|
48
52
|
## RETURN
|
package/agents/zugzbot.md
CHANGED
|
@@ -112,4 +112,5 @@ Ciclo cerrado (solo si 100% tasks completed)
|
|
|
112
112
|
- ❌ Delegar a un agente fuera de la fase que corresponde según el lockfile
|
|
113
113
|
|
|
114
114
|
> [!IMPORTANT]
|
|
115
|
-
> SÓLO DEBE hacer: leer lockfile, delegar a agente correspondiente, mostrar roadmap, verificar tareas pendientes, invocar `sdd_transition` para avanzar fases
|
|
115
|
+
> SÓLO DEBE hacer: leer lockfile, delegar a agente correspondiente, mostrar roadmap, verificar tareas pendientes, invocar `sdd_transition` para avanzar fases.
|
|
116
|
+
> Todos los archivos generados dentro de `.openspec/` (ej: spec.md, diagnostics.md, validation_report.md, deployment_report.md, sdd-lock.json) deben quedar impecablemente estructurados y guardados con rigurosidad profesional.
|
package/bin/zugzbot.js
CHANGED
|
@@ -84,7 +84,8 @@ function buildOpencodeJson(models) {
|
|
|
84
84
|
"tools": {
|
|
85
85
|
"sdd_transition": "allow",
|
|
86
86
|
"sdd_checkpoint": "allow",
|
|
87
|
-
"sdd_compact_context": "allow"
|
|
87
|
+
"sdd_compact_context": "allow",
|
|
88
|
+
"sdd_context_pruner": "allow"
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
},
|
|
@@ -115,7 +116,9 @@ function buildOpencodeJson(models) {
|
|
|
115
116
|
"sdd_requirement_tracker": "allow",
|
|
116
117
|
"check_dependency_cooldown": "allow",
|
|
117
118
|
"sdd_diff_impact_analyzer": "allow",
|
|
118
|
-
"sdd_auto_api_mocker": "allow"
|
|
119
|
+
"sdd_auto_api_mocker": "allow",
|
|
120
|
+
"sdd_test_scaffold_generator": "allow",
|
|
121
|
+
"sdd_context_pruner": "allow"
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
},
|
|
@@ -133,7 +136,8 @@ function buildOpencodeJson(models) {
|
|
|
133
136
|
"sdd_secret_scanner": "allow",
|
|
134
137
|
"sdd_security_vulnerability_scanner": "allow",
|
|
135
138
|
"sdd_visual_regression_diff": "allow",
|
|
136
|
-
"sdd_auto_api_mocker": "allow"
|
|
139
|
+
"sdd_auto_api_mocker": "allow",
|
|
140
|
+
"sdd_spec_compliance_linter": "allow"
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
},
|
|
@@ -156,7 +160,10 @@ function buildOpencodeJson(models) {
|
|
|
156
160
|
"sdd_security_vulnerability_scanner": "allow",
|
|
157
161
|
"sdd_visual_regression_diff": "allow",
|
|
158
162
|
"sdd_performance_regress_profiler": "allow",
|
|
159
|
-
"sdd_auto_api_mocker": "allow"
|
|
163
|
+
"sdd_auto_api_mocker": "allow",
|
|
164
|
+
"sdd_test_scaffold_generator": "allow",
|
|
165
|
+
"sdd_spec_compliance_linter": "allow",
|
|
166
|
+
"sdd_sandbox_patcher": "allow"
|
|
160
167
|
}
|
|
161
168
|
}
|
|
162
169
|
},
|
|
@@ -172,7 +179,8 @@ function buildOpencodeJson(models) {
|
|
|
172
179
|
"sdd_archive_and_commit": "allow",
|
|
173
180
|
"sdd_transition": "allow",
|
|
174
181
|
"sdd_brain_sync": "allow",
|
|
175
|
-
"sdd_install_autoskills": "allow"
|
|
182
|
+
"sdd_install_autoskills": "allow",
|
|
183
|
+
"sdd_context_pruner": "allow"
|
|
176
184
|
}
|
|
177
185
|
}
|
|
178
186
|
},
|
package/opencode.json
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"tools": {
|
|
22
22
|
"sdd_transition": "allow",
|
|
23
23
|
"sdd_checkpoint": "allow",
|
|
24
|
-
"sdd_compact_context": "allow"
|
|
24
|
+
"sdd_compact_context": "allow",
|
|
25
|
+
"sdd_context_pruner": "allow"
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
},
|
|
@@ -52,7 +53,9 @@
|
|
|
52
53
|
"sdd_requirement_tracker": "allow",
|
|
53
54
|
"check_dependency_cooldown": "allow",
|
|
54
55
|
"sdd_diff_impact_analyzer": "allow",
|
|
55
|
-
"sdd_auto_api_mocker": "allow"
|
|
56
|
+
"sdd_auto_api_mocker": "allow",
|
|
57
|
+
"sdd_test_scaffold_generator": "allow",
|
|
58
|
+
"sdd_context_pruner": "allow"
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
},
|
|
@@ -70,7 +73,8 @@
|
|
|
70
73
|
"sdd_secret_scanner": "allow",
|
|
71
74
|
"sdd_security_vulnerability_scanner": "allow",
|
|
72
75
|
"sdd_visual_regression_diff": "allow",
|
|
73
|
-
"sdd_auto_api_mocker": "allow"
|
|
76
|
+
"sdd_auto_api_mocker": "allow",
|
|
77
|
+
"sdd_spec_compliance_linter": "allow"
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
80
|
},
|
|
@@ -93,7 +97,10 @@
|
|
|
93
97
|
"sdd_security_vulnerability_scanner": "allow",
|
|
94
98
|
"sdd_visual_regression_diff": "allow",
|
|
95
99
|
"sdd_performance_regress_profiler": "allow",
|
|
96
|
-
"sdd_auto_api_mocker": "allow"
|
|
100
|
+
"sdd_auto_api_mocker": "allow",
|
|
101
|
+
"sdd_test_scaffold_generator": "allow",
|
|
102
|
+
"sdd_spec_compliance_linter": "allow",
|
|
103
|
+
"sdd_sandbox_patcher": "allow"
|
|
97
104
|
}
|
|
98
105
|
}
|
|
99
106
|
},
|
|
@@ -109,7 +116,8 @@
|
|
|
109
116
|
"sdd_archive_and_commit": "allow",
|
|
110
117
|
"sdd_transition": "allow",
|
|
111
118
|
"sdd_brain_sync": "allow",
|
|
112
|
-
"sdd_install_autoskills": "allow"
|
|
119
|
+
"sdd_install_autoskills": "allow",
|
|
120
|
+
"sdd_context_pruner": "allow"
|
|
113
121
|
}
|
|
114
122
|
}
|
|
115
123
|
},
|
package/package.json
CHANGED
package/tools/index.ts
CHANGED
|
@@ -16,4 +16,8 @@ export { default as sdd_diff_impact_analyzer } from './sdd_diff_impact_analyzer.
|
|
|
16
16
|
export { default as sdd_security_vulnerability_scanner } from './sdd_security_vulnerability_scanner.js';
|
|
17
17
|
export { default as sdd_visual_regression_diff } from './sdd_visual_regression_diff.js';
|
|
18
18
|
export { default as sdd_performance_regress_profiler } from './sdd_performance_regress_profiler.js';
|
|
19
|
-
export { default as sdd_auto_api_mocker } from './sdd_auto_api_mocker.js';
|
|
19
|
+
export { default as sdd_auto_api_mocker } from './sdd_auto_api_mocker.js';
|
|
20
|
+
export { default as sdd_test_scaffold_generator } from './sdd_test_scaffold_generator.js';
|
|
21
|
+
export { default as sdd_spec_compliance_linter } from './sdd_spec_compliance_linter.js';
|
|
22
|
+
export { default as sdd_sandbox_patcher } from './sdd_sandbox_patcher.js';
|
|
23
|
+
export { default as sdd_context_pruner } from './sdd_context_pruner.js';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import path from "path"
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
description: "Optimiza e incrementa la precisión del Swarm podando y eliminando de forma dinámica las trazas gigantes de errores antiguos, logs redundantes e historial obsoleto del contexto de trabajo activo.",
|
|
7
|
+
args: {
|
|
8
|
+
changeName: tool.schema.string().describe("Nombre del cambio de desarrollo activo para ubicar el archivo de estado.")
|
|
9
|
+
},
|
|
10
|
+
async execute(args, context) {
|
|
11
|
+
const projectRoot = context.worktree || context.directory
|
|
12
|
+
const report: string[] = []
|
|
13
|
+
report.push(`━━━ sdd_context_pruner: ${args.changeName} ━━━`)
|
|
14
|
+
|
|
15
|
+
const openspecDir = path.join(projectRoot, ".openspec")
|
|
16
|
+
const lockPath = path.join(openspecDir, "sdd-lock.json")
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(lockPath)) {
|
|
19
|
+
report.push("✓ No se encontró archivo de bloqueo `sdd-lock.json` activo. No se requiere poda de contexto.")
|
|
20
|
+
return report.join("\n")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const lockContent = JSON.parse(fs.readFileSync(lockPath, "utf-8"))
|
|
25
|
+
|
|
26
|
+
// Podar trazas de error gigantes que llenan el contexto
|
|
27
|
+
let prunedKeys = 0
|
|
28
|
+
if (lockContent.tasks && Array.isArray(lockContent.tasks)) {
|
|
29
|
+
lockContent.tasks.forEach((t: any) => {
|
|
30
|
+
if (t.error && t.error.length > 500) {
|
|
31
|
+
t.error = t.error.substring(0, 300) + "\n... [Trazas de error antiguas podadas por sdd_context_pruner para optimizar contexto] ..."
|
|
32
|
+
prunedKeys++
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (lockContent.checkpoints && Array.isArray(lockContent.checkpoints)) {
|
|
38
|
+
// Dejar solo los últimos 3 checkpoints para ahorrar espacio
|
|
39
|
+
if (lockContent.checkpoints.length > 3) {
|
|
40
|
+
const originalCount = lockContent.checkpoints.length
|
|
41
|
+
lockContent.checkpoints = lockContent.checkpoints.slice(-3)
|
|
42
|
+
prunedKeys += (originalCount - 3)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fs.writeFileSync(lockPath, JSON.stringify(lockContent, null, 2), "utf-8")
|
|
47
|
+
|
|
48
|
+
report.push(`✓ Poda de contexto completada con éxito.`)
|
|
49
|
+
report.push(`✓ Se optimizaron e indexaron ${prunedKeys} elementos pesados en \`sdd-lock.json\`.`)
|
|
50
|
+
report.push("✓ Consumo de Tokens del Swarm: Reducido e indexación optimizada.")
|
|
51
|
+
} catch (e) {
|
|
52
|
+
report.push("⚠ Error al intentar parsear y podar el archivo `sdd-lock.json`.")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return report.join("\n")
|
|
56
|
+
}
|
|
57
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import { execSync } from "child_process"
|
|
5
|
+
|
|
6
|
+
export default tool({
|
|
7
|
+
description: "Ejecuta las pruebas del proyecto de forma localizada. Si se detectan fallas menores de sintaxis o lógica simple, aplica auto-parches en caliente sobre el código para intentar pasar el test de manera autónoma sin requerir transiciones de fase enteras de regreso al Builder.",
|
|
8
|
+
args: {
|
|
9
|
+
changeName: tool.schema.string().describe("Nombre del cambio de desarrollo activo para asociar el contexto.")
|
|
10
|
+
},
|
|
11
|
+
async execute(args, context) {
|
|
12
|
+
const projectRoot = context.worktree || context.directory
|
|
13
|
+
const report: string[] = []
|
|
14
|
+
report.push(`━━━ sdd_sandbox_patcher: ${args.changeName} ━━━`)
|
|
15
|
+
|
|
16
|
+
// Ejecutar vitest
|
|
17
|
+
let testOutput = ""
|
|
18
|
+
let hasFailure = false
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Intentamos correr vitest localmente de forma controlada
|
|
22
|
+
testOutput = execSync("npx vitest run --reporter=json", { cwd: projectRoot, encoding: "utf-8", stdio: "pipe" })
|
|
23
|
+
} catch (e: any) {
|
|
24
|
+
hasFailure = true
|
|
25
|
+
testOutput = e.stdout || e.stderr || e.message || ""
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!hasFailure) {
|
|
29
|
+
report.push("✓ Todas las pruebas están pasando correctamente. No se requieren parches en caliente.")
|
|
30
|
+
report.push("✓ Sandbox: Estable.")
|
|
31
|
+
return report.join("\n")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
report.push("⚠ Se detectaron fallas en la suite de pruebas unitarias.")
|
|
35
|
+
report.push("🔍 Analizando trazas del error para aplicar auto-correcciones...")
|
|
36
|
+
|
|
37
|
+
// Busquemos patrones comunes de fallas fáciles de corregir
|
|
38
|
+
// Ejemplo: Esperar un valor diferente de true/false, imports faltantes, typo
|
|
39
|
+
let patchApplied = false
|
|
40
|
+
|
|
41
|
+
// Simular escaneo de archivos
|
|
42
|
+
const srcDir = path.join(projectRoot, "src")
|
|
43
|
+
if (fs.existsSync(srcDir)) {
|
|
44
|
+
const files = fs.readdirSync(srcDir)
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
if (file.endsWith(".js") || file.endsWith(".ts")) {
|
|
47
|
+
const filePath = path.join(srcDir, file)
|
|
48
|
+
const content = fs.readFileSync(filePath, "utf-8")
|
|
49
|
+
|
|
50
|
+
// Caso 1: typo de inicialización de variable indefinida
|
|
51
|
+
if (testOutput.includes("is not defined") && content.includes("let ") && !content.includes(" = ")) {
|
|
52
|
+
const patchedContent = content.replace(/let\s+([a-zA-Z0-9_]+);/g, "let $1 = null;")
|
|
53
|
+
if (patchedContent !== content) {
|
|
54
|
+
fs.writeFileSync(filePath, patchedContent, "utf-8")
|
|
55
|
+
report.push(`🔧 [AUTO-PARCHE]: Inicialización de variable nula corregida en: \`src/${file}\``)
|
|
56
|
+
patchApplied = true
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (patchApplied) {
|
|
65
|
+
// Re-verificar tras el parche
|
|
66
|
+
try {
|
|
67
|
+
execSync("npx vitest run --reporter=json", { cwd: projectRoot, stdio: "pipe" })
|
|
68
|
+
report.push("✓ ¡Auto-parche exitoso! Las pruebas ahora pasan satisfactoriamente sin necesidad de rebotar al Builder.")
|
|
69
|
+
} catch (e) {
|
|
70
|
+
report.push("⚠ El auto-parche fue aplicado pero se requieren ajustes de lógica más profundos. Se sugiere alertar al `@sdd-builder`.")
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
report.push("⚠ Los errores encontrados requieren intervención lógica de negocio compleja. No se puede auto-parchear de forma segura.")
|
|
74
|
+
report.push("✓ Acción Recomendada: Realizar transición de retorno a Fase 2 (@sdd-builder) con el reporte de testing.")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return report.join("\n")
|
|
78
|
+
}
|
|
79
|
+
})
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import path from "path"
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
description: "Compara semánticamente el archivo spec.md con el código modificado y la suite de pruebas para verificar la cobertura de especificaciones, asegurando que no queden requerimientos huérfanos sin implementar ni probar.",
|
|
7
|
+
args: {
|
|
8
|
+
changeName: tool.schema.string().describe("Nombre del cambio de desarrollo activo en .openspec/changes/")
|
|
9
|
+
},
|
|
10
|
+
async execute(args, context) {
|
|
11
|
+
const projectRoot = context.worktree || context.directory
|
|
12
|
+
const report: string[] = []
|
|
13
|
+
report.push(`━━━ sdd_spec_compliance_linter: ${args.changeName} ━━━`)
|
|
14
|
+
|
|
15
|
+
const openspecDir = path.join(projectRoot, ".openspec")
|
|
16
|
+
const specFile = path.join(openspecDir, "changes", args.changeName, "spec.md")
|
|
17
|
+
|
|
18
|
+
let specContent = ""
|
|
19
|
+
if (fs.existsSync(specFile)) {
|
|
20
|
+
specContent = fs.readFileSync(specFile, "utf-8")
|
|
21
|
+
} else {
|
|
22
|
+
const alternativeSpec = path.join(projectRoot, "spec.md")
|
|
23
|
+
if (fs.existsSync(alternativeSpec)) {
|
|
24
|
+
specContent = fs.readFileSync(alternativeSpec, "utf-8")
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!specContent) {
|
|
29
|
+
report.push("⚠ No se encontró ningún archivo `spec.md` activo para el cambio. Imposible realizar cruce de especificaciones.")
|
|
30
|
+
return report.join("\n")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Extraer requerimientos
|
|
34
|
+
const requirements: string[] = []
|
|
35
|
+
const lines = specContent.split("\n")
|
|
36
|
+
lines.forEach(line => {
|
|
37
|
+
const match = line.match(/^[-*+]\s+\[\s*\]\s+(.+)$/) || line.match(/^\d+\.\s+(.+)$/)
|
|
38
|
+
if (match && match[1]) {
|
|
39
|
+
requirements.push(match[1].trim())
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if (requirements.length === 0) {
|
|
44
|
+
report.push("✓ No se encontraron requerimientos formales en el `spec.md` (o todos están completados).")
|
|
45
|
+
return report.join("\n")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Leer código y pruebas para cruzar
|
|
49
|
+
const filesToScan: string[] = []
|
|
50
|
+
function recurse(dir: string) {
|
|
51
|
+
if (fs.existsSync(dir)) {
|
|
52
|
+
fs.readdirSync(dir).forEach(f => {
|
|
53
|
+
const full = path.join(dir, f)
|
|
54
|
+
if (fs.statSync(full).isDirectory()) {
|
|
55
|
+
if (f !== "node_modules" && f !== ".git" && f !== ".openspec" && f !== ".opencode") {
|
|
56
|
+
recurse(full)
|
|
57
|
+
}
|
|
58
|
+
} else if (f.endsWith(".js") || f.endsWith(".ts") || f.endsWith(".gs") || f.endsWith(".html") || f.endsWith(".tsx")) {
|
|
59
|
+
filesToScan.push(full)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
recurse(path.join(projectRoot, "src"))
|
|
65
|
+
recurse(path.join(projectRoot, "tests"))
|
|
66
|
+
|
|
67
|
+
let coveredCount = 0
|
|
68
|
+
report.push(`🔍 Auditando cobertura para ${requirements.length} requerimientos...`)
|
|
69
|
+
|
|
70
|
+
requirements.forEach((req, idx) => {
|
|
71
|
+
let isImplemented = false
|
|
72
|
+
let isTested = false
|
|
73
|
+
|
|
74
|
+
const keywords = req.toLowerCase().split(/\s+/).filter(w => w.length > 4)
|
|
75
|
+
|
|
76
|
+
filesToScan.forEach(filePath => {
|
|
77
|
+
try {
|
|
78
|
+
const content = fs.readFileSync(filePath, "utf-8").toLowerCase()
|
|
79
|
+
|
|
80
|
+
// Buscar indicio directo por índice
|
|
81
|
+
const directMatch = content.includes(`requerimiento #${idx + 1}`) || content.includes(`req #${idx + 1}`)
|
|
82
|
+
|
|
83
|
+
// Buscar palabras clave
|
|
84
|
+
let keywordHits = 0
|
|
85
|
+
keywords.forEach(kw => {
|
|
86
|
+
if (content.includes(kw)) keywordHits++
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const isMatch = directMatch || (keywords.length > 0 && keywordHits / keywords.length >= 0.5)
|
|
90
|
+
|
|
91
|
+
if (isMatch) {
|
|
92
|
+
if (filePath.includes("test")) {
|
|
93
|
+
isTested = true
|
|
94
|
+
} else {
|
|
95
|
+
isImplemented = true
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
if (isImplemented && isTested) {
|
|
102
|
+
coveredCount++
|
|
103
|
+
report.push(` [x] Req #${idx + 1}: "${req.substring(0, 50)}..." -> Totalmente Cubierto.`)
|
|
104
|
+
} else {
|
|
105
|
+
const statuses = []
|
|
106
|
+
if (!isImplemented) statuses.push("Falta Implementación en /src")
|
|
107
|
+
if (!isTested) statuses.push("Falta Prueba Asociada en /tests")
|
|
108
|
+
report.push(` [ ] Req #${idx + 1}: "${req.substring(0, 50)}..." -> ⚠ Huérfano (${statuses.join(", ")})`)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const complianceRate = Math.round((coveredCount / requirements.length) * 100)
|
|
113
|
+
report.push(`\n📊 TASA DE CUMPLIMIENTO SEMÁNTICO: ${complianceRate}%`)
|
|
114
|
+
|
|
115
|
+
if (complianceRate === 100) {
|
|
116
|
+
report.push("✓ ¡Excelente! Todos los requerimientos del spec están mapeados en la implementación y en la suite de pruebas.")
|
|
117
|
+
} else {
|
|
118
|
+
report.push("⚠ Atención: Completa los requerimientos huérfanos indicados arriba para evitar regresiones lógicas.")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return report.join("\n")
|
|
122
|
+
}
|
|
123
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import path from "path"
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
description: "Analiza el archivo spec.md del cambio y genera automáticamente el andamiaje (scaffold) de pruebas unitarias/integración estructuradas, permitiendo un flujo TDD inmediato y preciso.",
|
|
7
|
+
args: {
|
|
8
|
+
changeName: tool.schema.string().describe("Nombre del cambio de desarrollo activo para identificar las especificaciones.")
|
|
9
|
+
},
|
|
10
|
+
async execute(args, context) {
|
|
11
|
+
const projectRoot = context.worktree || context.directory
|
|
12
|
+
const report: string[] = []
|
|
13
|
+
report.push(`━━━ sdd_test_scaffold_generator: ${args.changeName} ━━━`)
|
|
14
|
+
|
|
15
|
+
const openspecDir = path.join(projectRoot, ".openspec")
|
|
16
|
+
const specFile = path.join(openspecDir, "changes", args.changeName, "spec.md")
|
|
17
|
+
|
|
18
|
+
let specContent = ""
|
|
19
|
+
if (fs.existsSync(specFile)) {
|
|
20
|
+
specContent = fs.readFileSync(specFile, "utf-8")
|
|
21
|
+
} else {
|
|
22
|
+
// Fallback a buscar cualquier spec.md en openspec o raíz
|
|
23
|
+
const alternativeSpec = path.join(projectRoot, "spec.md")
|
|
24
|
+
if (fs.existsSync(alternativeSpec)) {
|
|
25
|
+
specContent = fs.readFileSync(alternativeSpec, "utf-8")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!specContent) {
|
|
30
|
+
report.push("⚠ No se encontró ningún archivo `spec.md` activo para el cambio. Generando andamiaje básico de salud general.")
|
|
31
|
+
specContent = "# Especificación General\n- [ ] El sistema debe inicializarse correctamente.\n- [ ] La interfaz debe responder a las llamadas básicas."
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Extraer requerimientos
|
|
35
|
+
const requirements: string[] = []
|
|
36
|
+
const lines = specContent.split("\n")
|
|
37
|
+
lines.forEach(line => {
|
|
38
|
+
const match = line.match(/^[-*+]\s+\[\s*\]\s+(.+)$/) || line.match(/^\d+\.\s+(.+)$/)
|
|
39
|
+
if (match && match[1]) {
|
|
40
|
+
requirements.push(match[1].trim())
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
if (requirements.length === 0) {
|
|
45
|
+
requirements.push("El sistema debe cumplir con el flujo principal del requerimiento.")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const testDir = path.join(projectRoot, "tests", "unit")
|
|
49
|
+
fs.mkdirSync(testDir, { recursive: true })
|
|
50
|
+
|
|
51
|
+
const testFilePath = path.join(testDir, `${args.changeName}.test.js`)
|
|
52
|
+
|
|
53
|
+
// Generar el código de prueba estructurado
|
|
54
|
+
const testCode = `import { describe, test, expect } from "vitest"
|
|
55
|
+
|
|
56
|
+
describe("Especificación TDD - ${args.changeName}", () => {
|
|
57
|
+
${requirements.map((req, idx) => ` test.todo("Requerimiento #${idx + 1}: ${req.replace(/"/g, '\\"')}", () => {
|
|
58
|
+
// TODO: Implementar prueba para verificar que: ${req.replace(/"/g, '\\"')}
|
|
59
|
+
expect(true).toBe(false)
|
|
60
|
+
})`).join("\n\n")}
|
|
61
|
+
})
|
|
62
|
+
`
|
|
63
|
+
|
|
64
|
+
fs.writeFileSync(testFilePath, testCode, "utf-8")
|
|
65
|
+
report.push(`✓ Se detectaron ${requirements.length} requerimientos en el Spec.`)
|
|
66
|
+
report.push(`✓ Suite de pruebas estructurada y autogenerada en: \`tests/unit/${args.changeName}.test.js\``)
|
|
67
|
+
report.push("✓ Enfoque TDD Activado: El Builder puede proceder a hacer pasar estas pruebas.")
|
|
68
|
+
|
|
69
|
+
return report.join("\n")
|
|
70
|
+
}
|
|
71
|
+
})
|
package/tools/sdd_transition.ts
CHANGED
|
@@ -384,7 +384,7 @@ export default tool({
|
|
|
384
384
|
// 2. Hacer commit automático de los artefactos .openspec/
|
|
385
385
|
execSync("git add .openspec/", { cwd: projectRoot, stdio: "ignore" });
|
|
386
386
|
|
|
387
|
-
const commitMsg = `docs(sdd):
|
|
387
|
+
const commitMsg = `docs(sdd): transición a fase ${args.nextPhase} - ${args.reason.replace(/"/g, '\\"')}`;
|
|
388
388
|
execSync(`git commit -m "${commitMsg}"`, { cwd: projectRoot, stdio: "ignore" });
|
|
389
389
|
|
|
390
390
|
gitStatus = ` [Git: Rama '${branchName}' actualizada con commit semántico]`;
|