zugzbot-sdd 1.5.0
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/AGENTS.md +212 -0
- package/README.md +112 -0
- package/ZUGZ.md +91 -0
- package/agents/aux-handyman.md +36 -0
- package/agents/aux-oracle.md +39 -0
- package/agents/sdd-archiver.md +33 -0
- package/agents/sdd-builder.md +29 -0
- package/agents/sdd-deployer.md +43 -0
- package/agents/sdd-explorer.md +49 -0
- package/agents/sdd-planner.md +59 -0
- package/agents/sdd-tester.md +51 -0
- package/agents/zugzbot.md +84 -0
- package/bin/zugzbot.js +249 -0
- package/bun.lock +259 -0
- package/commands/sdd-archiver.md +11 -0
- package/commands/sdd-builder.md +11 -0
- package/commands/sdd-deployer.md +12 -0
- package/commands/sdd-explorer.md +11 -0
- package/commands/sdd-planner.md +11 -0
- package/commands/sdd-tester.md +12 -0
- package/commands/sdd.md +11 -0
- package/eslint.config.js +51 -0
- package/opencode.json +121 -0
- package/package.json +46 -0
- package/plugin.json +10 -0
- package/plugins/plugin_sdd_core.ts +54 -0
- package/plugins/plugin_tui.tsx +318 -0
- package/sdd +1228 -0
- package/skills/sdd-dependency-cooldown/SKILL.md +40 -0
- package/skills/sdd-tree-generator/SKILL.md +40 -0
- package/skills-lock.json +35 -0
- package/tests/static/dom_structure.test.js +57 -0
- package/tests/static/tag_balance.test.js +74 -0
- package/tests/unit/harness_structure.test.js +65 -0
- package/tools/brain-utils.ts +122 -0
- package/tools/check_dependency_cooldown.ts +134 -0
- package/tools/index.ts +14 -0
- package/tools/sdd_archive_and_commit.ts +207 -0
- package/tools/sdd_bdd_tester.ts +163 -0
- package/tools/sdd_brain_sync.ts +160 -0
- package/tools/sdd_checkpoint.ts +142 -0
- package/tools/sdd_compact_context.ts +122 -0
- package/tools/sdd_generate_tree.ts +64 -0
- package/tools/sdd_install_autoskills.ts +100 -0
- package/tools/sdd_regression_detector.ts +241 -0
- package/tools/sdd_requirement_tracker.ts +236 -0
- package/tools/sdd_secret_scanner.ts +205 -0
- package/tools/sdd_spec_validator.ts +139 -0
- package/tools/sdd_transition.ts +375 -0
- package/tools/sdd_ui_auditor.ts +310 -0
- package/tsconfig.json +28 -0
- package/zugz-models.json +23 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import path from "path"
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
description: "Crea una consolidación técnica de alta densidad (compaction snapshot) de los artefactos de la fase actual para optimizar tokens y reiniciar la memoria del subagente.",
|
|
7
|
+
args: {
|
|
8
|
+
changeName: tool.schema.string().optional().describe("Nombre del cambio en kebab-case. Si se omite, se autodetectará del sdd-lock.")
|
|
9
|
+
},
|
|
10
|
+
async execute(args, context) {
|
|
11
|
+
const projectRoot = context.worktree || context.directory;
|
|
12
|
+
|
|
13
|
+
// 1. Detectar cambio
|
|
14
|
+
let changeName = args.changeName;
|
|
15
|
+
if (!changeName) {
|
|
16
|
+
const lockfilePath = path.join(projectRoot, ".openspec/sdd-lock.json");
|
|
17
|
+
const altLockPath = path.join(projectRoot, "openspec/sdd-lock.json");
|
|
18
|
+
let activeLockPath = fs.existsSync(lockfilePath) ? lockfilePath : (fs.existsSync(altLockPath) ? altLockPath : null);
|
|
19
|
+
if (activeLockPath) {
|
|
20
|
+
try {
|
|
21
|
+
const lockObj = JSON.parse(fs.readFileSync(activeLockPath, "utf-8"));
|
|
22
|
+
if (lockObj.change_name && lockObj.change_name !== "nuevo-cambio") {
|
|
23
|
+
changeName = lockObj.change_name;
|
|
24
|
+
}
|
|
25
|
+
} catch (e) {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!changeName || changeName === "nuevo-cambio") {
|
|
30
|
+
return `[Compactor Blocked] Error: No se pudo resolver el nombre del cambio activo.`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const changeDir = path.join(projectRoot, ".openspec/changes", changeName);
|
|
34
|
+
if (!fs.existsSync(changeDir)) {
|
|
35
|
+
return `[Compactor Blocked] Error: No se encuentra el directorio del cambio en ${path.relative(projectRoot, changeDir)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Leer artefactos individuales
|
|
39
|
+
const proposalPath = path.join(changeDir, "proposal.md");
|
|
40
|
+
const specPath = path.join(changeDir, "specs/spec.md");
|
|
41
|
+
const archPath = path.join(changeDir, "orchestrator_architecture.md");
|
|
42
|
+
const tasksPath = path.join(changeDir, "orchestrator_tasks.md");
|
|
43
|
+
|
|
44
|
+
let proposalSummary = "No disponible";
|
|
45
|
+
let specsSummary = "No disponible";
|
|
46
|
+
let archSummary = "No disponible";
|
|
47
|
+
let tasksSummary = "No disponible";
|
|
48
|
+
|
|
49
|
+
if (fs.existsSync(proposalPath)) {
|
|
50
|
+
const content = fs.readFileSync(proposalPath, "utf-8");
|
|
51
|
+
const descMatch = content.match(/## Description[\s\S]*?(?=##|$)/i) || content.match(/#[\s\S]*?(?=##|$)/i);
|
|
52
|
+
proposalSummary = descMatch ? descMatch[0].trim() : content.substring(0, 500);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (fs.existsSync(specPath)) {
|
|
56
|
+
const content = fs.readFileSync(specPath, "utf-8");
|
|
57
|
+
const scens: string[] = [];
|
|
58
|
+
const lines = content.split("\n");
|
|
59
|
+
lines.forEach(l => {
|
|
60
|
+
if (l.trim().startsWith("Scenario:") || l.trim().startsWith("Escenario:")) {
|
|
61
|
+
scens.push(l.trim());
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
specsSummary = scens.length > 0 ? `Escenarios validados:\n${scens.map(s => `- ${s}`).join("\n")}` : "Escenarios BDD no estructurados.";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (fs.existsSync(archPath)) {
|
|
68
|
+
const content = fs.readFileSync(archPath, "utf-8");
|
|
69
|
+
const mermaid = content.match(/```mermaid[\s\S]*?```/i);
|
|
70
|
+
archSummary = mermaid ? `Esquema Arquitectónico:\n${mermaid[0]}` : "Sin diagramas Mermaid de arquitectura.";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (fs.existsSync(tasksPath)) {
|
|
74
|
+
const content = fs.readFileSync(tasksPath, "utf-8");
|
|
75
|
+
const total = (content.match(/- \[[ xX]\]/g) || []).length;
|
|
76
|
+
const completed = (content.match(/- \[[xX]\]/g) || []).length;
|
|
77
|
+
tasksSummary = `Checklist de Tareas: ${completed}/${total} completadas.\n` +
|
|
78
|
+
content.split("\n").filter(l => l.includes("- [ ]") || l.includes("- [x]")).slice(0, 10).join("\n") +
|
|
79
|
+
(total > 10 ? "\n... (y más)" : "");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 3. Escribir el Snapshot consolidado
|
|
83
|
+
const snapshotPath = path.join(changeDir, "compaction_snapshot.md");
|
|
84
|
+
const nowStr = new Date().toISOString().split('T')[0];
|
|
85
|
+
|
|
86
|
+
const markdown = `# 🧠 Consolidado de Contexto de Alta Densidad (SDD Compaction)
|
|
87
|
+
Fecha de consolidación: ${nowStr}
|
|
88
|
+
Cambio Activo: \`${changeName}\`
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 📜 Propuesta y Objetivos
|
|
93
|
+
${proposalSummary}
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 📐 Especificaciones y Escenarios
|
|
98
|
+
${specsSummary}
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 🏛️ Estructura Arquitectónica
|
|
103
|
+
${archSummary}
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 📋 Estado del Checklist
|
|
108
|
+
${tasksSummary}
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
> [!TIP]
|
|
113
|
+
> **Acción Recomendada para Limpiar Memoria de Contexto:**
|
|
114
|
+
> Si eres un subagente y ves este archivo, tu memoria ha sido compactada con éxito.
|
|
115
|
+
> Lee **únicamente** este archivo de consolidación para entender el estado actual y los contratos técnicos previos. Descarta la lectura repetitiva de chats históricos o archivos de logs antiguos.
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
fs.writeFileSync(snapshotPath, markdown, "utf-8");
|
|
119
|
+
|
|
120
|
+
return `[SDD Compactor] Consolidado de alta densidad generado con éxito en ${path.relative(projectRoot, snapshotPath)}.`;
|
|
121
|
+
}
|
|
122
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import path from "path"
|
|
4
|
+
|
|
5
|
+
function buildTreeString(dir: string, prefix = "", depth = 0, maxDepth = 2): string {
|
|
6
|
+
if (depth > maxDepth) return ""
|
|
7
|
+
let result = ""
|
|
8
|
+
let entries: string[] = []
|
|
9
|
+
try {
|
|
10
|
+
entries = fs.readdirSync(dir)
|
|
11
|
+
} catch (e) {
|
|
12
|
+
return result
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const exclude = [
|
|
16
|
+
"node_modules",
|
|
17
|
+
".git",
|
|
18
|
+
".openspec",
|
|
19
|
+
".opencode",
|
|
20
|
+
"dist",
|
|
21
|
+
".next",
|
|
22
|
+
"build",
|
|
23
|
+
"coverage",
|
|
24
|
+
".DS_Store"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const filtered = entries.filter(e => !exclude.includes(e))
|
|
28
|
+
|
|
29
|
+
filtered.forEach((entry, idx) => {
|
|
30
|
+
const fullPath = path.join(dir, entry)
|
|
31
|
+
let isDir = false
|
|
32
|
+
try {
|
|
33
|
+
isDir = fs.statSync(fullPath).isDirectory()
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
const isLast = idx === filtered.length - 1
|
|
38
|
+
const connector = isLast ? "└── " : "├── "
|
|
39
|
+
result += prefix + connector + entry + (isDir ? "/" : "") + "\n"
|
|
40
|
+
|
|
41
|
+
if (isDir && depth < maxDepth) {
|
|
42
|
+
const newPrefix = prefix + (isLast ? " " : "│ ")
|
|
43
|
+
result += buildTreeString(fullPath, newPrefix, depth + 1, maxDepth)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return result
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default tool({
|
|
51
|
+
description: "Escanea la estructura de directorios del proyecto de forma nativa e instantánea de hasta 3 niveles, filtrando carpetas pesadas como node_modules o .git, para retornar el árbol visual del proyecto sin costo de tokens.",
|
|
52
|
+
args: {
|
|
53
|
+
maxDepth: tool.schema.number().optional().default(2).describe("Nivel máximo de profundidad para el escaneo recursivo de directorios (por defecto 2, máx 3)")
|
|
54
|
+
},
|
|
55
|
+
async execute(args, context) {
|
|
56
|
+
const projectRoot = context.worktree || context.directory
|
|
57
|
+
const depth = Math.min(Math.max(args.maxDepth, 1), 3)
|
|
58
|
+
|
|
59
|
+
const tree = buildTreeString(projectRoot, "", 0, depth)
|
|
60
|
+
const projectName = path.basename(projectRoot)
|
|
61
|
+
|
|
62
|
+
return `Estructura de Carpetas del Proyecto: ${projectName}\n\n/\n${tree || " (Vacío o error en lectura)"}`
|
|
63
|
+
}
|
|
64
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
function readdirSafe(dir: string): string[] {
|
|
7
|
+
try { return fs.readdirSync(dir) } catch { return [] }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function rmrf(dir: string) {
|
|
11
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function moveSkill(srcDir: string, skillName: string, destBase: string): string {
|
|
15
|
+
const src = path.join(srcDir, skillName)
|
|
16
|
+
const dest = path.join(destBase, skillName)
|
|
17
|
+
|
|
18
|
+
if (fs.existsSync(dest)) rmrf(dest)
|
|
19
|
+
fs.mkdirSync(destBase, { recursive: true })
|
|
20
|
+
fs.renameSync(src, dest)
|
|
21
|
+
|
|
22
|
+
const skillFile = path.join(dest, "SKILL.md")
|
|
23
|
+
if (!fs.existsSync(skillFile)) {
|
|
24
|
+
const files = readdirSafe(dest).filter(f => f.endsWith(".md"))
|
|
25
|
+
if (files.length === 0) return `⚠️ ${skillName}: movido pero sin SKILL.md`
|
|
26
|
+
return `⚠️ ${skillName}: movido (SKILL.md no encontrado, .md alternativo: ${files.join(", ")})`
|
|
27
|
+
}
|
|
28
|
+
return `✓ ${skillName} → .opencode/skills/${skillName}/`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default tool({
|
|
32
|
+
description: "Ejecuta npx autoskills y migra automáticamente los skills instalados en .agents/skills/ a .opencode/skills/. Integra detección post-instalación y reporte.",
|
|
33
|
+
args: {
|
|
34
|
+
dryRun: tool.schema.boolean().optional().default(false).describe("Si es true, solo muestra qué se movería sin ejecutar autoskills ni mover archivos")
|
|
35
|
+
},
|
|
36
|
+
async execute(args, context) {
|
|
37
|
+
const projectRoot = context.worktree || context.directory
|
|
38
|
+
const agentsSkillsDir = path.join(projectRoot, ".agents/skills")
|
|
39
|
+
const opencodeSkillsDir = path.join(projectRoot, ".opencode/skills")
|
|
40
|
+
|
|
41
|
+
const report: string[] = []
|
|
42
|
+
report.push("━━━ sdd_install_autoskills ━━━")
|
|
43
|
+
|
|
44
|
+
const beforeSkills = new Set(readdirSafe(agentsSkillsDir))
|
|
45
|
+
|
|
46
|
+
if (args.dryRun) {
|
|
47
|
+
report.push("[DRY-RUN] Saltando ejecución de npx autoskills")
|
|
48
|
+
} else {
|
|
49
|
+
try {
|
|
50
|
+
report.push("▶ Ejecutando npx -y autoskills --yes...")
|
|
51
|
+
const output = execSync("npx -y autoskills --yes 2>&1", {
|
|
52
|
+
cwd: projectRoot,
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
timeout: 120000
|
|
55
|
+
})
|
|
56
|
+
const lines = output.split("\n").filter((l: string) => l.trim())
|
|
57
|
+
report.push(...lines.slice(0, 30).map((l: string) => ` ${l}`))
|
|
58
|
+
if (lines.length > 30) report.push(` ... (${lines.length - 30} líneas más)`)
|
|
59
|
+
} catch (e: any) {
|
|
60
|
+
report.push(`❌ Error ejecutando autoskills: ${e.message || e}`)
|
|
61
|
+
return report.join("\n")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const afterSkills = readdirSafe(agentsSkillsDir)
|
|
66
|
+
const newSkills = afterSkills.filter(s => !beforeSkills.has(s))
|
|
67
|
+
const allSkills = afterSkills.filter(s => {
|
|
68
|
+
const skillPath = path.join(agentsSkillsDir, s)
|
|
69
|
+
try { return fs.statSync(skillPath).isDirectory() } catch { return false }
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if (allSkills.length === 0) {
|
|
73
|
+
report.push("📭 No se encontraron skills en .agents/skills/ para migrar")
|
|
74
|
+
return report.join("\n")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (args.dryRun) {
|
|
78
|
+
report.push("\n[DRY-RUN] Skills que se migrarían:")
|
|
79
|
+
for (const skill of allSkills) {
|
|
80
|
+
const tag = newSkills.includes(skill) ? "(nuevo)" : "(pre-existente)"
|
|
81
|
+
report.push(` 📦 ${skill} ${tag} → .opencode/skills/${skill}/`)
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
report.push("\n📦 Migrando skills a .opencode/skills/:")
|
|
85
|
+
for (const skill of allSkills) {
|
|
86
|
+
const result = moveSkill(agentsSkillsDir, skill, opencodeSkillsDir)
|
|
87
|
+
report.push(` ${result}${newSkills.includes(skill) ? " (nuevo)" : " (movido)"}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const remaining = readdirSafe(agentsSkillsDir)
|
|
91
|
+
if (remaining.length === 0) {
|
|
92
|
+
try { fs.rmdirSync(agentsSkillsDir) } catch {}
|
|
93
|
+
report.push("🧹 .agents/skills/ eliminado (quedó vacío)")
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
report.push("━━━ fin ━━━")
|
|
98
|
+
return report.join("\n")
|
|
99
|
+
}
|
|
100
|
+
})
|
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
function decodeGitPath(gitPath: string): string {
|
|
7
|
+
let cleaned = gitPath.replace(/^"|"$/g, "")
|
|
8
|
+
if (cleaned.includes("\\")) {
|
|
9
|
+
try {
|
|
10
|
+
const bytes: number[] = []
|
|
11
|
+
let i = 0
|
|
12
|
+
while (i < cleaned.length) {
|
|
13
|
+
if (cleaned[i] === "\\" && i + 3 < cleaned.length && /^[0-7]{3}$/.test(cleaned.substring(i + 1, i + 4))) {
|
|
14
|
+
const octalVal = cleaned.substring(i + 1, i + 4)
|
|
15
|
+
bytes.push(parseInt(octalVal, 8))
|
|
16
|
+
i += 4
|
|
17
|
+
} else {
|
|
18
|
+
if (cleaned[i] === "\\" && i + 1 < cleaned.length) {
|
|
19
|
+
const next = cleaned[i + 1]
|
|
20
|
+
if (next === "n") { bytes.push(10); i += 2 }
|
|
21
|
+
else if (next === "t") { bytes.push(9); i += 2 }
|
|
22
|
+
else if (next === "\\") { bytes.push(92); i += 2 }
|
|
23
|
+
else if (next === "\"") { bytes.push(34); i += 2 }
|
|
24
|
+
else { bytes.push(cleaned.charCodeAt(i)); i++ }
|
|
25
|
+
} else {
|
|
26
|
+
const code = cleaned.charCodeAt(i)
|
|
27
|
+
if (code < 128) {
|
|
28
|
+
bytes.push(code)
|
|
29
|
+
} else {
|
|
30
|
+
const buf = Buffer.from(cleaned[i], "utf-8")
|
|
31
|
+
for (let b = 0; b < buf.length; b++) {
|
|
32
|
+
bytes.push(buf[b])
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
i++
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return Buffer.from(bytes).toString("utf-8")
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return cleaned.replace(/\\([0-7]{3})/g, (match, octal) => {
|
|
42
|
+
return String.fromCharCode(parseInt(octal, 8))
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return cleaned
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sanitizeGitPath(line: string): string {
|
|
50
|
+
const content = line.substring(3).trim()
|
|
51
|
+
if (content.includes(" -> ")) {
|
|
52
|
+
const parts = content.split(" -> ")
|
|
53
|
+
return decodeGitPath(parts[1])
|
|
54
|
+
}
|
|
55
|
+
return decodeGitPath(content)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Analiza los errores de una salida de compilador y los mapea de forma limpia
|
|
59
|
+
function parseCompilerErrors(errorOutput: string): Set<string> {
|
|
60
|
+
const errors = new Set<string>();
|
|
61
|
+
const lines = errorOutput.split("\n").filter(l => l.trim());
|
|
62
|
+
lines.forEach(line => {
|
|
63
|
+
// Captura líneas que parezcan errores de compilador (archivo con número de línea y mensaje)
|
|
64
|
+
const fileMatch = line.match(/^([a-zA-Z0-9_\-./]+)\(/) || line.match(/^([a-zA-Z0-9_\-./]+):\d+/) || line.match(/in\s+([a-zA-Z0-9_\-./]+\.py)/);
|
|
65
|
+
if (fileMatch) {
|
|
66
|
+
// Normalizar simplificando detalles variables de error
|
|
67
|
+
errors.add(line.trim());
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return errors;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default tool({
|
|
74
|
+
description: "Analiza el espacio de trabajo de forma diferencial usando compilación estática (ej: tsc) para detectar ÚNICAMENTE nuevos errores introducidos en esta fase de desarrollo, ignorando fallas de compilación o linter preexistentes.",
|
|
75
|
+
args: {
|
|
76
|
+
runCheck: tool.schema.boolean().optional().default(true).describe("Si es true, ejecuta la validación global de regresiones.")
|
|
77
|
+
},
|
|
78
|
+
async execute(args, context) {
|
|
79
|
+
const projectRoot = context.worktree || context.directory;
|
|
80
|
+
|
|
81
|
+
if (!args.runCheck) {
|
|
82
|
+
return JSON.stringify({
|
|
83
|
+
status: "SKIPPED",
|
|
84
|
+
message: "Escaner de regresiones omitido."
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 1. Identificar archivos modificados de forma robusta
|
|
89
|
+
const modifiedFiles = new Set<string>();
|
|
90
|
+
const hasGit = fs.existsSync(path.join(projectRoot, ".git"));
|
|
91
|
+
if (hasGit) {
|
|
92
|
+
try {
|
|
93
|
+
const gitOutput = execSync("git status --porcelain", { cwd: projectRoot, encoding: "utf-8" });
|
|
94
|
+
gitOutput.split("\n").forEach(line => {
|
|
95
|
+
if (!line || line.length < 4) return;
|
|
96
|
+
const filePathRel = sanitizeGitPath(line);
|
|
97
|
+
modifiedFiles.add(filePathRel);
|
|
98
|
+
});
|
|
99
|
+
} catch (e) {}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. Determinar stack de compilación estática
|
|
103
|
+
let command = "";
|
|
104
|
+
let languageLabel = "";
|
|
105
|
+
|
|
106
|
+
if (fs.existsSync(path.join(projectRoot, "tsconfig.json"))) {
|
|
107
|
+
command = "npx tsc --noEmit --pretty false";
|
|
108
|
+
languageLabel = "TypeScript (tsc)";
|
|
109
|
+
} else if (fs.existsSync(path.join(projectRoot, "platformio.ini"))) {
|
|
110
|
+
command = "pio run";
|
|
111
|
+
languageLabel = "PlatformIO (C++)";
|
|
112
|
+
} else if (fs.existsSync(path.join(projectRoot, "Cargo.toml"))) {
|
|
113
|
+
command = "cargo check";
|
|
114
|
+
languageLabel = "Rust (cargo check)";
|
|
115
|
+
} else if (fs.existsSync(path.join(projectRoot, "build.gradle"))) {
|
|
116
|
+
command = "./gradlew compileJava";
|
|
117
|
+
languageLabel = "Java (Gradle)";
|
|
118
|
+
} else if (fs.existsSync(path.join(projectRoot, "pom.xml"))) {
|
|
119
|
+
command = "mvn compile";
|
|
120
|
+
languageLabel = "Java (Maven)";
|
|
121
|
+
} else if (fs.existsSync(path.join(projectRoot, "package.json"))) {
|
|
122
|
+
try {
|
|
123
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf-8"));
|
|
124
|
+
if (pkg.scripts?.build) {
|
|
125
|
+
command = "npm run build";
|
|
126
|
+
languageLabel = "JS/TS (npm run build)";
|
|
127
|
+
} else if (pkg.scripts?.lint) {
|
|
128
|
+
command = "npm run lint";
|
|
129
|
+
languageLabel = "JS/TS (npm run lint)";
|
|
130
|
+
}
|
|
131
|
+
} catch (e) {}
|
|
132
|
+
} else if (fs.existsSync(path.join(projectRoot, "requirements.txt")) || fs.existsSync(path.join(projectRoot, "pyproject.toml"))) {
|
|
133
|
+
command = "python3 -m py_compile $(find . -name '*.py' -not -path '*/.*' -not -path '*/node_modules/*')";
|
|
134
|
+
languageLabel = "Python (py_compile)";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!command) {
|
|
138
|
+
return JSON.stringify({
|
|
139
|
+
status: "APPROVED",
|
|
140
|
+
reason: "No se encontró un verificador estático compatible. Se asume conforme."
|
|
141
|
+
}, null, 2);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 3. OBTENER LECTURA BASELINE (Errores preexistentes) vía Stash Temporal si hay cambios en Git
|
|
145
|
+
const preExistingErrors = new Set<string>();
|
|
146
|
+
let stashCreated = false;
|
|
147
|
+
|
|
148
|
+
if (hasGit && modifiedFiles.size > 0) {
|
|
149
|
+
try {
|
|
150
|
+
// Crear un stash temporal de los cambios en caliente
|
|
151
|
+
execSync("git stash push --keep-index --include-untracked -m 'sdd_temp_baseline'", { cwd: projectRoot, stdio: "ignore" });
|
|
152
|
+
stashCreated = true;
|
|
153
|
+
|
|
154
|
+
// Ejecutar compilador en la versión limpia para ver fallos preexistentes
|
|
155
|
+
try {
|
|
156
|
+
execSync(command, { cwd: projectRoot, stdio: "pipe" });
|
|
157
|
+
} catch (e: any) {
|
|
158
|
+
const rawBaselineOutput = e.stdout?.toString() || e.stderr?.toString() || "";
|
|
159
|
+
parseCompilerErrors(rawBaselineOutput).forEach(err => preExistingErrors.add(err));
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {
|
|
162
|
+
// Fallback si falla el stasheo (ej. sin commits previos)
|
|
163
|
+
} finally {
|
|
164
|
+
// Restaurar cambios sí o sí
|
|
165
|
+
if (stashCreated) {
|
|
166
|
+
try {
|
|
167
|
+
execSync("git stash pop", { cwd: projectRoot, stdio: "ignore" });
|
|
168
|
+
} catch (e) {}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 4. EJECUTAR COMPILACIÓN FINAL (Con cambios en caliente activos)
|
|
174
|
+
try {
|
|
175
|
+
execSync(command, { cwd: projectRoot, stdio: "pipe" });
|
|
176
|
+
|
|
177
|
+
return JSON.stringify({
|
|
178
|
+
status: "APPROVED",
|
|
179
|
+
checker: languageLabel,
|
|
180
|
+
preExistingBypassedCount: preExistingErrors.size,
|
|
181
|
+
message: `✅ DETECTOR DE REGRESIONES: La validación estática de tipo '${languageLabel}' pasó de manera impecable. No se encontraron discrepancias de compilación en el espacio de trabajo.`
|
|
182
|
+
}, null, 2);
|
|
183
|
+
} catch (e: any) {
|
|
184
|
+
const errorOutput = e.stdout?.toString() || e.stderr?.toString() || e.message || "";
|
|
185
|
+
const currentErrors = parseCompilerErrors(errorOutput);
|
|
186
|
+
|
|
187
|
+
// Calcular errores nuevos de forma diferencial
|
|
188
|
+
const newErrors: string[] = [];
|
|
189
|
+
const localNewErrors: string[] = [];
|
|
190
|
+
|
|
191
|
+
currentErrors.forEach(err => {
|
|
192
|
+
// Si el error NO existía previamente
|
|
193
|
+
if (!preExistingErrors.has(err)) {
|
|
194
|
+
// Detectar si pertenece a un archivo que modificamos (error local) o a otro módulo (regresión)
|
|
195
|
+
const fileMatch = err.match(/^([a-zA-Z0-9_\-./]+)\(/) || err.match(/^([a-zA-Z0-9_\-./]+):\d+/) || err.match(/in\s+([a-zA-Z0-9_\-./]+\.py)/);
|
|
196
|
+
if (fileMatch) {
|
|
197
|
+
const filePath = fileMatch[1];
|
|
198
|
+
if (modifiedFiles.has(filePath)) {
|
|
199
|
+
localNewErrors.push(err);
|
|
200
|
+
} else if (!filePath.includes("node_modules") && !filePath.includes(".openspec") && !filePath.includes(".opencode")) {
|
|
201
|
+
newErrors.push(err);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
// Error genérico sin archivo directo
|
|
205
|
+
newErrors.push(err);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Si hay verdaderas regresiones (nuevos errores en módulos que NO modificamos)
|
|
211
|
+
if (newErrors.length > 0) {
|
|
212
|
+
return JSON.stringify({
|
|
213
|
+
status: "FAILED",
|
|
214
|
+
checker: languageLabel,
|
|
215
|
+
regressionCount: newErrors.length,
|
|
216
|
+
preExistingIgnoredCount: preExistingErrors.size,
|
|
217
|
+
regressions: newErrors.slice(0, 10),
|
|
218
|
+
message: `❌ DETECTOR DE REGRESIONES FALLIDO: Se han introducido errores de tipado o compilación en módulos externos que no modificaste directamente:\n\n${newErrors.slice(0, 5).map(r => ` - ⚠️ ${r}`).join("\n")}${newErrors.length > 5 ? `\n ... y ${newErrors.length - 5} regresiones más.` : ""}\n\nNota: Se detectaron y omitieron de forma segura ${preExistingErrors.size} errores de compilación preexistentes para no bloquear tu desarrollo.`
|
|
219
|
+
}, null, 2);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Si los nuevos errores son puramente en tus archivos modificados (error local de tu cambio)
|
|
223
|
+
if (localNewErrors.length > 0) {
|
|
224
|
+
return JSON.stringify({
|
|
225
|
+
status: "FAILED_LOCAL",
|
|
226
|
+
checker: languageLabel,
|
|
227
|
+
preExistingIgnoredCount: preExistingErrors.size,
|
|
228
|
+
message: `⚠️ VERIFICACIÓN DE CÓDIGO FALLIDA: Se detectaron errores de tipado/compilación locales en tus archivos modificados. Corrígelos antes de avanzar.\n\nSalida del compilador (Errores nuevos):\n${localNewErrors.slice(0, 10).map(e => ` - ${e}`).join("\n")}\n\nNota: Se omitieron de forma segura ${preExistingErrors.size} errores preexistentes.`
|
|
229
|
+
}, null, 2);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Si el compilador falló pero todos los errores resultaron ser preexistentes, ¡Damos luz verde!
|
|
233
|
+
return JSON.stringify({
|
|
234
|
+
status: "APPROVED",
|
|
235
|
+
checker: languageLabel,
|
|
236
|
+
preExistingBypassedCount: preExistingErrors.size,
|
|
237
|
+
message: `✅ DETECTOR DE REGRESIONES APROBADO: El compilador detectó ${preExistingErrors.size} errores de compilación, pero todos son preexistentes en la base de código. Se aprueba la transición para no bloquear tu cambio.`
|
|
238
|
+
}, null, 2);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
})
|