zugzbot-sdd 1.5.24 → 1.5.26
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.
|
@@ -0,0 +1,276 @@
|
|
|
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: "Audita estáticamente cualquier archivo (o conjunto de archivos modificados) para diagnosticar fallas de sintaxis, paréntesis/llaves rotas, errores sintácticos o problemas de formato en múltiples tecnologías (JS, TS, Python, JSON, HTML, GS, CSS, CPP, SH, YAML, PHP, MD).",
|
|
7
|
+
args: {
|
|
8
|
+
filePath: tool.schema.string().optional().describe("Ruta absoluta o relativa del archivo a auditar de forma quirúrgica. Si se omite, detectará automáticamente los archivos modificados vía git.")
|
|
9
|
+
},
|
|
10
|
+
async execute(args, context) {
|
|
11
|
+
const projectRoot = context.worktree || context.directory;
|
|
12
|
+
const diagnostics = [];
|
|
13
|
+
let filesToAudit = [];
|
|
14
|
+
if (args.filePath) {
|
|
15
|
+
const absolutePath = path.isAbsolute(args.filePath) ? args.filePath : path.join(projectRoot, args.filePath);
|
|
16
|
+
if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile()) {
|
|
17
|
+
filesToAudit.push(absolutePath);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
// Autodetectar vía git status
|
|
22
|
+
try {
|
|
23
|
+
const gitStatus = execSync("git status --porcelain", { cwd: projectRoot, encoding: "utf-8" });
|
|
24
|
+
gitStatus.split("\n").forEach(line => {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (trimmed) {
|
|
27
|
+
const parts = trimmed.split(/\s+/);
|
|
28
|
+
if (parts.length >= 2) {
|
|
29
|
+
const relPath = parts.slice(1).join(" ");
|
|
30
|
+
const absolutePath = path.join(projectRoot, relPath);
|
|
31
|
+
if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile()) {
|
|
32
|
+
filesToAudit.push(absolutePath);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (e) { }
|
|
39
|
+
}
|
|
40
|
+
if (filesToAudit.length === 0) {
|
|
41
|
+
return JSON.stringify({
|
|
42
|
+
status: "APPROVED",
|
|
43
|
+
message: "✓ No se encontraron archivos para auditar o todo está limpio.",
|
|
44
|
+
diagnostics: []
|
|
45
|
+
}, null, 2);
|
|
46
|
+
}
|
|
47
|
+
// Heurística de balanceo de paréntesis/llaves/corchetes general
|
|
48
|
+
const hasBalancedTokens = (str) => {
|
|
49
|
+
const stack = [];
|
|
50
|
+
const pairs = { ')': '(', '}': '{', ']': '[' };
|
|
51
|
+
let lineNum = 1;
|
|
52
|
+
for (let i = 0; i < str.length; i++) {
|
|
53
|
+
const char = str[i];
|
|
54
|
+
if (char === '\n') {
|
|
55
|
+
lineNum++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (['(', '{', '['].includes(char)) {
|
|
59
|
+
stack.push({ char, index: i, line: lineNum });
|
|
60
|
+
}
|
|
61
|
+
else if ([')', '}', ']'].includes(char)) {
|
|
62
|
+
const top = stack.pop();
|
|
63
|
+
if (!top || top.char !== pairs[char]) {
|
|
64
|
+
return {
|
|
65
|
+
balanced: false,
|
|
66
|
+
message: `Error de balanceo: Se encontró '${char}' en línea ${lineNum} pero se esperaba cierre para '${top ? top.char : 'ninguno'}' (línea ${top ? top.line : 'N/A'}).`
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (stack.length > 0) {
|
|
72
|
+
const unclosed = stack.pop();
|
|
73
|
+
return {
|
|
74
|
+
balanced: false,
|
|
75
|
+
message: `Error de balanceo: El token '${unclosed.char}' abierto en la línea ${unclosed.line} nunca fue cerrado.`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return { balanced: true };
|
|
79
|
+
};
|
|
80
|
+
for (const file of filesToAudit) {
|
|
81
|
+
const ext = path.extname(file).toLowerCase();
|
|
82
|
+
const relFile = path.relative(projectRoot, file);
|
|
83
|
+
try {
|
|
84
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
85
|
+
// 1. JSON
|
|
86
|
+
if (ext === ".json") {
|
|
87
|
+
try {
|
|
88
|
+
JSON.parse(content);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
const lineMatch = err.message.match(/line (\d+)/i);
|
|
92
|
+
diagnostics.push({
|
|
93
|
+
file: relFile,
|
|
94
|
+
type: "JSON Syntax Error",
|
|
95
|
+
line: lineMatch ? parseInt(lineMatch[1]) : undefined,
|
|
96
|
+
message: err.message
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 2. JavaScript / Google Apps Script (.js, .jsx, .gs)
|
|
101
|
+
else if (ext === ".js" || ext === ".jsx" || ext === ".gs") {
|
|
102
|
+
try {
|
|
103
|
+
execSync(`node --check "${file}"`, { stdio: "pipe" });
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const stderr = err.stderr?.toString() || err.message || "";
|
|
107
|
+
const lineMatch = stderr.match(/:(\d+)\r?\n/m) || stderr.match(/line (\d+)/i);
|
|
108
|
+
diagnostics.push({
|
|
109
|
+
file: relFile,
|
|
110
|
+
type: "JS/GS Syntax Error",
|
|
111
|
+
line: lineMatch ? parseInt(lineMatch[1]) : undefined,
|
|
112
|
+
message: stderr.split("\n")[0] || "Syntax error detected"
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 3. TypeScript / TSX
|
|
117
|
+
else if (ext === ".ts" || ext === ".tsx") {
|
|
118
|
+
const balanceCheck = hasBalancedTokens(content);
|
|
119
|
+
if (!balanceCheck.balanced) {
|
|
120
|
+
diagnostics.push({
|
|
121
|
+
file: relFile,
|
|
122
|
+
type: "TypeScript Syntax/Token Mismatch",
|
|
123
|
+
message: balanceCheck.message || "Falla de balanceo en tokens estructurados."
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 4. Python (.py)
|
|
128
|
+
else if (ext === ".py") {
|
|
129
|
+
try {
|
|
130
|
+
execSync(`python -m py_compile "${file}"`, { stdio: "pipe" });
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
const stderr = err.stderr?.toString() || err.message || "";
|
|
134
|
+
const lineMatch = stderr.match(/line (\d+)/i);
|
|
135
|
+
diagnostics.push({
|
|
136
|
+
file: relFile,
|
|
137
|
+
type: "Python Syntax Error",
|
|
138
|
+
line: lineMatch ? parseInt(lineMatch[1]) : undefined,
|
|
139
|
+
message: stderr.split("\n")[0] || "Python syntax compilation failed"
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// 5. HTML
|
|
144
|
+
else if (ext === ".html") {
|
|
145
|
+
const unclosedTags = [];
|
|
146
|
+
const tags = content.match(/<[^>]+>/g) || [];
|
|
147
|
+
const stack = [];
|
|
148
|
+
tags.forEach(tag => {
|
|
149
|
+
const match = tag.match(/<\/?([a-zA-Z0-9:-]+)/);
|
|
150
|
+
if (match) {
|
|
151
|
+
const name = match[1];
|
|
152
|
+
if (tag.startsWith("</")) {
|
|
153
|
+
if (stack.length > 0 && stack[stack.length - 1] === name) {
|
|
154
|
+
stack.pop();
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
unclosedTags.push(`Tag de cierre huérfano: ${tag}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else if (!tag.endsWith("/>") && !["img", "br", "hr", "input", "meta", "link"].includes(name)) {
|
|
161
|
+
stack.push(name);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
if (stack.length > 0 || unclosedTags.length > 0) {
|
|
166
|
+
diagnostics.push({
|
|
167
|
+
file: relFile,
|
|
168
|
+
type: "HTML Tag Mismatch",
|
|
169
|
+
message: `Estructura HTML mal balanceada. Tags sin cerrar: ${stack.join(", ")}. ${unclosedTags.join("; ")}`
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// 6. Shell Scripts (.sh, .bash)
|
|
174
|
+
else if (ext === ".sh" || ext === ".bash") {
|
|
175
|
+
try {
|
|
176
|
+
execSync(`bash -n "${file}"`, { stdio: "pipe" });
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
const stderr = err.stderr?.toString() || err.message || "";
|
|
180
|
+
const lineMatch = stderr.match(/: line (\d+):/i) || stderr.match(/:(\d+):/i);
|
|
181
|
+
diagnostics.push({
|
|
182
|
+
file: relFile,
|
|
183
|
+
type: "Shell Script Syntax Error",
|
|
184
|
+
line: lineMatch ? parseInt(lineMatch[1]) : undefined,
|
|
185
|
+
message: stderr.split("\n")[0] || "Shell script syntax check failed"
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// 7. CSS
|
|
190
|
+
else if (ext === ".css") {
|
|
191
|
+
const balanceCheck = hasBalancedTokens(content);
|
|
192
|
+
if (!balanceCheck.balanced) {
|
|
193
|
+
diagnostics.push({
|
|
194
|
+
file: relFile,
|
|
195
|
+
type: "CSS Rule Mismatch",
|
|
196
|
+
message: `Falla de balanceo en CSS: ${balanceCheck.message}`
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// 8. C / C++ (.c, .cpp, .h, .hpp)
|
|
201
|
+
else if (ext === ".c" || ext === ".cpp" || ext === ".h" || ext === ".hpp") {
|
|
202
|
+
const balanceCheck = hasBalancedTokens(content);
|
|
203
|
+
if (!balanceCheck.balanced) {
|
|
204
|
+
diagnostics.push({
|
|
205
|
+
file: relFile,
|
|
206
|
+
type: "C/C++ Syntax Token Mismatch",
|
|
207
|
+
message: `Falla estructural C/C++: ${balanceCheck.message}`
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// 9. YAML / YML
|
|
212
|
+
else if (ext === ".yaml" || ext === ".yml") {
|
|
213
|
+
// Chequeo básico de indentación y formato YAML sin requerir librerías externas pesadas
|
|
214
|
+
const lines = content.split("\n");
|
|
215
|
+
lines.forEach((line, idx) => {
|
|
216
|
+
const spaces = line.match(/^(\s*)/)?.[1].length || 0;
|
|
217
|
+
if (spaces % 2 !== 0 && line.trim().length > 0 && !line.trim().startsWith("#")) {
|
|
218
|
+
diagnostics.push({
|
|
219
|
+
file: relFile,
|
|
220
|
+
type: "YAML Indentation Warning",
|
|
221
|
+
line: idx + 1,
|
|
222
|
+
message: `Indentación sospechosa (${spaces} espacios). YAML prefiere múltiplos de 2.`
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// 10. PHP
|
|
228
|
+
else if (ext === ".php") {
|
|
229
|
+
try {
|
|
230
|
+
execSync(`php -l "${file}"`, { stdio: "pipe" });
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
const stderr = err.stderr?.toString() || err.message || "";
|
|
234
|
+
const lineMatch = stderr.match(/on line (\d+)/i);
|
|
235
|
+
diagnostics.push({
|
|
236
|
+
file: relFile,
|
|
237
|
+
type: "PHP Syntax Error",
|
|
238
|
+
line: lineMatch ? parseInt(lineMatch[1]) : undefined,
|
|
239
|
+
message: stderr.split("\n")[0] || "PHP syntax check failed"
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// 11. Markdown (.md)
|
|
244
|
+
else if (ext === ".md") {
|
|
245
|
+
const openBlocks = (content.match(/^```/gm) || []).length;
|
|
246
|
+
if (openBlocks % 2 !== 0) {
|
|
247
|
+
diagnostics.push({
|
|
248
|
+
file: relFile,
|
|
249
|
+
type: "Markdown Fenced Block Mismatch",
|
|
250
|
+
message: "Se detectó un bloque de código fenced (```) sin cerrar en el archivo markdown."
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
diagnostics.push({
|
|
257
|
+
file: relFile,
|
|
258
|
+
type: "Read/Compile Error",
|
|
259
|
+
message: `Imposible leer o analizar el archivo: ${e.message}`
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (diagnostics.length > 0) {
|
|
264
|
+
return JSON.stringify({
|
|
265
|
+
status: "FAILED",
|
|
266
|
+
message: `❌ VALIDACIÓN DE SINTAXIS MULTI-TECNOLOGÍA FALLIDA: Se detectaron ${diagnostics.length} errores estáticos o de formato en los archivos modificados.`,
|
|
267
|
+
diagnostics
|
|
268
|
+
}, null, 2);
|
|
269
|
+
}
|
|
270
|
+
return JSON.stringify({
|
|
271
|
+
status: "APPROVED",
|
|
272
|
+
message: "✓ Validación de sintaxis exitosa. Todos los archivos auditados se encuentran 100% íntegros y limpios.",
|
|
273
|
+
diagnostics: []
|
|
274
|
+
}, null, 2);
|
|
275
|
+
}
|
|
276
|
+
});
|
package/agents/sdd-builder.md
CHANGED
|
@@ -16,6 +16,7 @@ permission:
|
|
|
16
16
|
"sdd_visual_regression_diff": allow
|
|
17
17
|
"sdd_auto_api_mocker": allow
|
|
18
18
|
"sdd_spec_compliance_linter": allow
|
|
19
|
+
"sdd_syntax_and_linter_auditor": allow
|
|
19
20
|
---
|
|
20
21
|
|
|
21
22
|
# @sdd-builder
|
|
@@ -27,7 +28,7 @@ permission:
|
|
|
27
28
|
## DO
|
|
28
29
|
- Implementa los cambios en el código según el spec, asegurándote de revisar `.openspec/brain.md` para cumplir estrictamente con los patrones técnicos exitosos y evitar reintroducir malas prácticas.
|
|
29
30
|
- Usa `edit` para parches quirúrgicos (prohibido reescribir archivos completos).
|
|
30
|
-
- **Pre-chequeo Local de Sintaxis**:
|
|
31
|
+
- **Pre-chequeo Local de Sintaxis**: Ejecuta OBLIGATORIAMENTE la herramienta premium `sdd_syntax_and_linter_auditor` sobre tus archivos modificados antes de finalizar para certificar la total integridad sintáctica de tus cambios (erradicando paréntesis, corchetes o llaves abiertas, y problemas de balanceo en general).
|
|
31
32
|
- **Escaneo SAST Quirúrgico**: Ejecuta `sdd_security_vulnerability_scanner` sobre tus archivos modificados antes de cerrar tu implementación.
|
|
32
33
|
- **Validación de Secretos**: Corre `sdd_secret_scanner` para asegurarte de no dejar tokens/passwords temporales de desarrollo.
|
|
33
34
|
- **Linter de Especificación**: Ejecuta `sdd_spec_compliance_linter` para certificar que todos los criterios de aceptación del Spec estén cubiertos.
|
|
@@ -51,7 +52,7 @@ permission:
|
|
|
51
52
|
- ❌ Escribir o autogenerar suites de tests unitarios o de integración
|
|
52
53
|
- ❌ Ejecutar validación de linter o auditorías UI por cuenta propia (delegar a `@sdd-tester`)
|
|
53
54
|
- ❌ Realizar deploys, pushes, o publicaciones de ningún tipo
|
|
54
|
-
- ❌ Usar herramientas que no le fueron asignadas (`sdd_transition`, `sdd_ui_auditor`, `sdd_secret_scanner`, `sdd_security_vulnerability_scanner`, `sdd_visual_regression_diff`, `sdd_auto_api_mocker`, `sdd_spec_compliance_linter`)
|
|
55
|
+
- ❌ Usar herramientas que no le fueron asignadas (`sdd_transition`, `sdd_ui_auditor`, `sdd_secret_scanner`, `sdd_security_vulnerability_scanner`, `sdd_visual_regression_diff`, `sdd_auto_api_mocker`, `sdd_spec_compliance_linter`, `sdd_syntax_and_linter_auditor`)
|
|
55
56
|
- ❌ Modificar `package.json`, `tsconfig.json`, o archivos de configuración de proyecto
|
|
56
57
|
- ❌ Ignorar el spec.md — toda implementación debe trackear contra los criterios de aceptación del spec
|
|
57
58
|
|
package/agents/sdd-tester.md
CHANGED
|
@@ -22,6 +22,7 @@ permission:
|
|
|
22
22
|
"sdd_test_scaffold_generator": allow
|
|
23
23
|
"sdd_spec_compliance_linter": allow
|
|
24
24
|
"sdd_sandbox_patcher": allow
|
|
25
|
+
"sdd_syntax_and_linter_auditor": allow
|
|
25
26
|
---
|
|
26
27
|
|
|
27
28
|
# @sdd-tester
|
|
@@ -37,13 +38,8 @@ permission:
|
|
|
37
38
|
3. **Configuración Proactiva de Linter/Calidad**:
|
|
38
39
|
- Si el proyecto no cuenta con una configuración mínima de linter o validador de sintaxis estática (como `.eslintrc`, `tsconfig.json` o similar) o carece de dependencias básicas de validación, **DEBES configurar proactivamente** los archivos iniciales mínimos o instalar localmente los paquetes de desarrollo requeridos (`npm install --save-dev eslint` o configuraciones nativas ligeras) para asegurar que el entorno sea capaz de diagnosticar la calidad del código.
|
|
39
40
|
4. **Chequeo Obligatorio de Sintaxis y Compilación (Por Archivo Modificado)**:
|
|
40
|
-
- Identifica todos los archivos modificados en la sesión. Para
|
|
41
|
-
|
|
42
|
-
- **TypeScript (`.ts`, `.tsx`)**: Ejecutar `npx tsc --noEmit` o el compilador local aplicable para auditar tipos e integridad sintáctica.
|
|
43
|
-
- **Python (`.py`)**: Ejecuta `python -m py_compile <ruta-del-archivo>`.
|
|
44
|
-
- **JSON (`.json`)**: Ejecuta una verificación estructural sintáctica (ej: `node -e "JSON.parse(fs.readFileSync('<ruta-del-archivo>'))"`).
|
|
45
|
-
- **HTML (`.html`)**: Realiza análisis estructural o de balanceo de tags (ej. usando las herramientas de test locales).
|
|
46
|
-
- Si algún archivo modificado reporta un error de sintaxis o compilación, **BLOQUEA de inmediato la transición** (marcando success/blocked a `blocked`), a menos que sea un error simple corregible usando `sdd_sandbox_patcher`.
|
|
41
|
+
- Identifica todos los archivos modificados en la sesión. Para realizar un diagnóstico completo y universal de sintaxis, **DEBES ejecutar OBLIGATORIAMENTE la herramienta premium `sdd_syntax_and_linter_auditor`** sobre cada archivo modificado o para el conjunto completo de cambios.
|
|
42
|
+
- Si la herramienta reporta un estado `FAILED` con errores de sintaxis (paréntesis, llaves o corchetes rotos, archivos JSON mal formateados, tags HTML sin balancear, etc.), **BLOQUEA de inmediato la transición** (marcando success/blocked a `blocked`), a menos que sea un error simple corregible usando `sdd_sandbox_patcher`.
|
|
47
43
|
5. **Validación y Auditorías del Swarm**:
|
|
48
44
|
- Ejecuta `sdd_spec_compliance_linter` para cruzar requerimientos.
|
|
49
45
|
- Ejecuta `sdd_security_vulnerability_scanner` para detectar vulnerabilidades en el código.
|
|
@@ -94,7 +90,7 @@ permission:
|
|
|
94
90
|
- ❌ Reescribir archivos de código — solo autocorregir errores de sintaxis simples usando `sdd_sandbox_patcher`
|
|
95
91
|
- ❌ Escribir tests unitarios o de integración nuevos
|
|
96
92
|
- ❌ Modificar archivos fuera de `.openspec/changes/<change-name>/`
|
|
97
|
-
- ❌ Usar herramientas que no le fueron asignadas (`sdd_transition`, `sdd_ui_auditor`, `sdd_spec_validator`, `sdd_regression_detector`, `sdd_bdd_tester`, `sdd_requirement_tracker`, `sdd_diff_impact_analyzer`, `sdd_security_vulnerability_scanner`, `sdd_visual_regression_diff`, `sdd_performance_regress_profiler`, `sdd_auto_api_mocker`, `sdd_test_scaffold_generator`, `sdd_spec_compliance_linter`, `sdd_sandbox_patcher`)
|
|
93
|
+
- ❌ Usar herramientas que no le fueron asignadas (`sdd_transition`, `sdd_ui_auditor`, `sdd_spec_validator`, `sdd_regression_detector`, `sdd_bdd_tester`, `sdd_requirement_tracker`, `sdd_diff_impact_analyzer`, `sdd_security_vulnerability_scanner`, `sdd_visual_regression_diff`, `sdd_performance_regress_profiler`, `sdd_auto_api_mocker`, `sdd_test_scaffold_generator`, `sdd_spec_compliance_linter`, `sdd_sandbox_patcher`, `sdd_syntax_and_linter_auditor`)
|
|
98
94
|
|
|
99
95
|
> [!IMPORTANT]
|
|
100
96
|
> SÓLO DEBE hacer: ejecutar linter, auditorías UI, validaciones estáticas, generar `validation_report.md`, invocar `sdd_transition` al completar.
|
package/bin/zugzbot.js
CHANGED
|
@@ -137,7 +137,8 @@ function buildOpencodeJson(models) {
|
|
|
137
137
|
"sdd_security_vulnerability_scanner": "allow",
|
|
138
138
|
"sdd_visual_regression_diff": "allow",
|
|
139
139
|
"sdd_auto_api_mocker": "allow",
|
|
140
|
-
"sdd_spec_compliance_linter": "allow"
|
|
140
|
+
"sdd_spec_compliance_linter": "allow",
|
|
141
|
+
"sdd_syntax_and_linter_auditor": "allow"
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
},
|
|
@@ -163,7 +164,8 @@ function buildOpencodeJson(models) {
|
|
|
163
164
|
"sdd_auto_api_mocker": "allow",
|
|
164
165
|
"sdd_test_scaffold_generator": "allow",
|
|
165
166
|
"sdd_spec_compliance_linter": "allow",
|
|
166
|
-
"sdd_sandbox_patcher": "allow"
|
|
167
|
+
"sdd_sandbox_patcher": "allow",
|
|
168
|
+
"sdd_syntax_and_linter_auditor": "allow"
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
171
|
},
|
package/opencode.json
CHANGED
|
@@ -74,7 +74,8 @@
|
|
|
74
74
|
"sdd_security_vulnerability_scanner": "allow",
|
|
75
75
|
"sdd_visual_regression_diff": "allow",
|
|
76
76
|
"sdd_auto_api_mocker": "allow",
|
|
77
|
-
"sdd_spec_compliance_linter": "allow"
|
|
77
|
+
"sdd_spec_compliance_linter": "allow",
|
|
78
|
+
"sdd_syntax_and_linter_auditor": "allow"
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
},
|
|
@@ -100,7 +101,8 @@
|
|
|
100
101
|
"sdd_auto_api_mocker": "allow",
|
|
101
102
|
"sdd_test_scaffold_generator": "allow",
|
|
102
103
|
"sdd_spec_compliance_linter": "allow",
|
|
103
|
-
"sdd_sandbox_patcher": "allow"
|
|
104
|
+
"sdd_sandbox_patcher": "allow",
|
|
105
|
+
"sdd_syntax_and_linter_auditor": "allow"
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
},
|
package/package.json
CHANGED
|
@@ -0,0 +1,292 @@
|
|
|
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: "Audita estáticamente cualquier archivo (o conjunto de archivos modificados) para diagnosticar fallas de sintaxis, paréntesis/llaves rotas, errores sintácticos o problemas de formato en múltiples tecnologías (JS, TS, Python, JSON, HTML, GS, CSS, CPP, SH, YAML, PHP, MD).",
|
|
8
|
+
args: {
|
|
9
|
+
filePath: tool.schema.string().optional().describe("Ruta absoluta o relativa del archivo a auditar de forma quirúrgica. Si se omite, detectará automáticamente los archivos modificados vía git.")
|
|
10
|
+
},
|
|
11
|
+
async execute(args, context) {
|
|
12
|
+
const projectRoot = context.worktree || context.directory
|
|
13
|
+
const diagnostics: Array<{ file: string; type: string; line?: number; message: string }> = []
|
|
14
|
+
|
|
15
|
+
let filesToAudit: string[] = []
|
|
16
|
+
|
|
17
|
+
if (args.filePath) {
|
|
18
|
+
const absolutePath = path.isAbsolute(args.filePath) ? args.filePath : path.join(projectRoot, args.filePath)
|
|
19
|
+
if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile()) {
|
|
20
|
+
filesToAudit.push(absolutePath)
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
// Autodetectar vía git status
|
|
24
|
+
try {
|
|
25
|
+
const gitStatus = execSync("git status --porcelain", { cwd: projectRoot, encoding: "utf-8" })
|
|
26
|
+
gitStatus.split("\n").forEach(line => {
|
|
27
|
+
const trimmed = line.trim()
|
|
28
|
+
if (trimmed) {
|
|
29
|
+
const parts = trimmed.split(/\s+/)
|
|
30
|
+
if (parts.length >= 2) {
|
|
31
|
+
const relPath = parts.slice(1).join(" ")
|
|
32
|
+
const absolutePath = path.join(projectRoot, relPath)
|
|
33
|
+
if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile()) {
|
|
34
|
+
filesToAudit.push(absolutePath)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
} catch (e) {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (filesToAudit.length === 0) {
|
|
43
|
+
return JSON.stringify({
|
|
44
|
+
status: "APPROVED",
|
|
45
|
+
message: "✓ No se encontraron archivos para auditar o todo está limpio.",
|
|
46
|
+
diagnostics: []
|
|
47
|
+
}, null, 2)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Heurística de balanceo de paréntesis/llaves/corchetes general
|
|
51
|
+
const hasBalancedTokens = (str: string): { balanced: boolean; message?: string } => {
|
|
52
|
+
const stack: { char: string; index: number; line: number }[] = []
|
|
53
|
+
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' }
|
|
54
|
+
let lineNum = 1
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < str.length; i++) {
|
|
57
|
+
const char = str[i]
|
|
58
|
+
if (char === '\n') {
|
|
59
|
+
lineNum++
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (['(', '{', '['].includes(char)) {
|
|
64
|
+
stack.push({ char, index: i, line: lineNum })
|
|
65
|
+
} else if ([')', '}', ']'].includes(char)) {
|
|
66
|
+
const top = stack.pop()
|
|
67
|
+
if (!top || top.char !== pairs[char]) {
|
|
68
|
+
return {
|
|
69
|
+
balanced: false,
|
|
70
|
+
message: `Error de balanceo: Se encontró '${char}' en línea ${lineNum} pero se esperaba cierre para '${top ? top.char : 'ninguno'}' (línea ${top ? top.line : 'N/A'}).`
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (stack.length > 0) {
|
|
77
|
+
const unclosed = stack.pop()!
|
|
78
|
+
return {
|
|
79
|
+
balanced: false,
|
|
80
|
+
message: `Error de balanceo: El token '${unclosed.char}' abierto en la línea ${unclosed.line} nunca fue cerrado.`
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { balanced: true }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const file of filesToAudit) {
|
|
88
|
+
const ext = path.extname(file).toLowerCase()
|
|
89
|
+
const relFile = path.relative(projectRoot, file)
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const content = fs.readFileSync(file, "utf-8")
|
|
93
|
+
|
|
94
|
+
// 1. JSON
|
|
95
|
+
if (ext === ".json") {
|
|
96
|
+
try {
|
|
97
|
+
JSON.parse(content)
|
|
98
|
+
} catch (err: any) {
|
|
99
|
+
const lineMatch = err.message.match(/line (\d+)/i)
|
|
100
|
+
diagnostics.push({
|
|
101
|
+
file: relFile,
|
|
102
|
+
type: "JSON Syntax Error",
|
|
103
|
+
line: lineMatch ? parseInt(lineMatch[1]) : undefined,
|
|
104
|
+
message: err.message
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2. JavaScript / Google Apps Script (.js, .jsx, .gs)
|
|
110
|
+
else if (ext === ".js" || ext === ".jsx" || ext === ".gs") {
|
|
111
|
+
try {
|
|
112
|
+
execSync(`node --check "${file}"`, { stdio: "pipe" })
|
|
113
|
+
} catch (err: any) {
|
|
114
|
+
const stderr = err.stderr?.toString() || err.message || ""
|
|
115
|
+
const lineMatch = stderr.match(/:(\d+)\r?\n/m) || stderr.match(/line (\d+)/i)
|
|
116
|
+
diagnostics.push({
|
|
117
|
+
file: relFile,
|
|
118
|
+
type: "JS/GS Syntax Error",
|
|
119
|
+
line: lineMatch ? parseInt(lineMatch[1]) : undefined,
|
|
120
|
+
message: stderr.split("\n")[0] || "Syntax error detected"
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 3. TypeScript / TSX
|
|
126
|
+
else if (ext === ".ts" || ext === ".tsx") {
|
|
127
|
+
const balanceCheck = hasBalancedTokens(content)
|
|
128
|
+
if (!balanceCheck.balanced) {
|
|
129
|
+
diagnostics.push({
|
|
130
|
+
file: relFile,
|
|
131
|
+
type: "TypeScript Syntax/Token Mismatch",
|
|
132
|
+
message: balanceCheck.message || "Falla de balanceo en tokens estructurados."
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 4. Python (.py)
|
|
138
|
+
else if (ext === ".py") {
|
|
139
|
+
try {
|
|
140
|
+
execSync(`python -m py_compile "${file}"`, { stdio: "pipe" })
|
|
141
|
+
} catch (err: any) {
|
|
142
|
+
const stderr = err.stderr?.toString() || err.message || ""
|
|
143
|
+
const lineMatch = stderr.match(/line (\d+)/i)
|
|
144
|
+
diagnostics.push({
|
|
145
|
+
file: relFile,
|
|
146
|
+
type: "Python Syntax Error",
|
|
147
|
+
line: lineMatch ? parseInt(lineMatch[1]) : undefined,
|
|
148
|
+
message: stderr.split("\n")[0] || "Python syntax compilation failed"
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 5. HTML
|
|
154
|
+
else if (ext === ".html") {
|
|
155
|
+
const unclosedTags: string[] = []
|
|
156
|
+
const tags = content.match(/<[^>]+>/g) || []
|
|
157
|
+
const stack: string[] = []
|
|
158
|
+
|
|
159
|
+
tags.forEach(tag => {
|
|
160
|
+
const match = tag.match(/<\/?([a-zA-Z0-9:-]+)/)
|
|
161
|
+
if (match) {
|
|
162
|
+
const name = match[1]
|
|
163
|
+
if (tag.startsWith("</")) {
|
|
164
|
+
if (stack.length > 0 && stack[stack.length - 1] === name) {
|
|
165
|
+
stack.pop()
|
|
166
|
+
} else {
|
|
167
|
+
unclosedTags.push(`Tag de cierre huérfano: ${tag}`)
|
|
168
|
+
}
|
|
169
|
+
} else if (!tag.endsWith("/>") && !["img", "br", "hr", "input", "meta", "link"].includes(name)) {
|
|
170
|
+
stack.push(name)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
if (stack.length > 0 || unclosedTags.length > 0) {
|
|
176
|
+
diagnostics.push({
|
|
177
|
+
file: relFile,
|
|
178
|
+
type: "HTML Tag Mismatch",
|
|
179
|
+
message: `Estructura HTML mal balanceada. Tags sin cerrar: ${stack.join(", ")}. ${unclosedTags.join("; ")}`
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 6. Shell Scripts (.sh, .bash)
|
|
185
|
+
else if (ext === ".sh" || ext === ".bash") {
|
|
186
|
+
try {
|
|
187
|
+
execSync(`bash -n "${file}"`, { stdio: "pipe" })
|
|
188
|
+
} catch (err: any) {
|
|
189
|
+
const stderr = err.stderr?.toString() || err.message || ""
|
|
190
|
+
const lineMatch = stderr.match(/: line (\d+):/i) || stderr.match(/:(\d+):/i)
|
|
191
|
+
diagnostics.push({
|
|
192
|
+
file: relFile,
|
|
193
|
+
type: "Shell Script Syntax Error",
|
|
194
|
+
line: lineMatch ? parseInt(lineMatch[1]) : undefined,
|
|
195
|
+
message: stderr.split("\n")[0] || "Shell script syntax check failed"
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 7. CSS
|
|
201
|
+
else if (ext === ".css") {
|
|
202
|
+
const balanceCheck = hasBalancedTokens(content)
|
|
203
|
+
if (!balanceCheck.balanced) {
|
|
204
|
+
diagnostics.push({
|
|
205
|
+
file: relFile,
|
|
206
|
+
type: "CSS Rule Mismatch",
|
|
207
|
+
message: `Falla de balanceo en CSS: ${balanceCheck.message}`
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 8. C / C++ (.c, .cpp, .h, .hpp)
|
|
213
|
+
else if (ext === ".c" || ext === ".cpp" || ext === ".h" || ext === ".hpp") {
|
|
214
|
+
const balanceCheck = hasBalancedTokens(content)
|
|
215
|
+
if (!balanceCheck.balanced) {
|
|
216
|
+
diagnostics.push({
|
|
217
|
+
file: relFile,
|
|
218
|
+
type: "C/C++ Syntax Token Mismatch",
|
|
219
|
+
message: `Falla estructural C/C++: ${balanceCheck.message}`
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 9. YAML / YML
|
|
225
|
+
else if (ext === ".yaml" || ext === ".yml") {
|
|
226
|
+
// Chequeo básico de indentación y formato YAML sin requerir librerías externas pesadas
|
|
227
|
+
const lines = content.split("\n")
|
|
228
|
+
lines.forEach((line, idx) => {
|
|
229
|
+
const spaces = line.match(/^(\s*)/)?.[1].length || 0
|
|
230
|
+
if (spaces % 2 !== 0 && line.trim().length > 0 && !line.trim().startsWith("#")) {
|
|
231
|
+
diagnostics.push({
|
|
232
|
+
file: relFile,
|
|
233
|
+
type: "YAML Indentation Warning",
|
|
234
|
+
line: idx + 1,
|
|
235
|
+
message: `Indentación sospechosa (${spaces} espacios). YAML prefiere múltiplos de 2.`
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 10. PHP
|
|
242
|
+
else if (ext === ".php") {
|
|
243
|
+
try {
|
|
244
|
+
execSync(`php -l "${file}"`, { stdio: "pipe" })
|
|
245
|
+
} catch (err: any) {
|
|
246
|
+
const stderr = err.stderr?.toString() || err.message || ""
|
|
247
|
+
const lineMatch = stderr.match(/on line (\d+)/i)
|
|
248
|
+
diagnostics.push({
|
|
249
|
+
file: relFile,
|
|
250
|
+
type: "PHP Syntax Error",
|
|
251
|
+
line: lineMatch ? parseInt(lineMatch[1]) : undefined,
|
|
252
|
+
message: stderr.split("\n")[0] || "PHP syntax check failed"
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 11. Markdown (.md)
|
|
258
|
+
else if (ext === ".md") {
|
|
259
|
+
const openBlocks = (content.match(/^```/gm) || []).length
|
|
260
|
+
if (openBlocks % 2 !== 0) {
|
|
261
|
+
diagnostics.push({
|
|
262
|
+
file: relFile,
|
|
263
|
+
type: "Markdown Fenced Block Mismatch",
|
|
264
|
+
message: "Se detectó un bloque de código fenced (```) sin cerrar en el archivo markdown."
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
} catch (e: any) {
|
|
270
|
+
diagnostics.push({
|
|
271
|
+
file: relFile,
|
|
272
|
+
type: "Read/Compile Error",
|
|
273
|
+
message: `Imposible leer o analizar el archivo: ${e.message}`
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (diagnostics.length > 0) {
|
|
279
|
+
return JSON.stringify({
|
|
280
|
+
status: "FAILED",
|
|
281
|
+
message: `❌ VALIDACIÓN DE SINTAXIS MULTI-TECNOLOGÍA FALLIDA: Se detectaron ${diagnostics.length} errores estáticos o de formato en los archivos modificados.`,
|
|
282
|
+
diagnostics
|
|
283
|
+
}, null, 2)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return JSON.stringify({
|
|
287
|
+
status: "APPROVED",
|
|
288
|
+
message: "✓ Validación de sintaxis exitosa. Todos los archivos auditados se encuentran 100% íntegros y limpios.",
|
|
289
|
+
diagnostics: []
|
|
290
|
+
}, null, 2)
|
|
291
|
+
}
|
|
292
|
+
})
|