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
+ });
@@ -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**: Realiza un chequeo local rápido de sintaxis de tus archivos modificados antes de finalizar (ej: `node --check` para JS/GS, `python -m py_compile` para Python, o `npx tsc --noEmit` para TypeScript) para erradicar proactivamente errores básicos como paréntesis o llaves abiertas.
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
 
@@ -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 cada archivo modificado, **ejecuta OBLIGATORIAMENTE un comando de chequeo de sintaxis o compilación en seco en base a su extensión** antes de intentar correr pruebas dinámicas complejas:
41
- - **JavaScript (`.js`, `.jsx`, `.gs`)**: Ejecuta `node --check <ruta-del-archivo>` para validar la sintaxis general y detectar paréntesis, llaves o corchetes rotos, variables sin declarar, etc.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zugzbot-sdd",
3
- "version": "1.5.24",
3
+ "version": "1.5.26",
4
4
  "description": "Zugzbot SDD Swarm - Spec-Driven Development Harness for OpenCode",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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
+ })