zugzbot-sdd 1.5.26 → 1.5.27

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.
Files changed (63) hide show
  1. package/.opencode/skills/sdd-auto-api-mocker/SKILL.md +62 -0
  2. package/.opencode/skills/sdd-root-cause-diagnostician/SKILL.md +48 -0
  3. package/.opencode/skills/sdd-semantic-context-pruner/SKILL.md +45 -0
  4. package/.opencode/tools/gas_clasp_tools.js +97 -0
  5. package/.opencode/tools/index.js +1 -0
  6. package/.opencode/tools/sdd_checkpoint.js +8 -2
  7. package/.opencode/tools/sdd_regression_detector.js +11 -5
  8. package/.opencode/tools/sdd_requirement_tracker.js +35 -24
  9. package/.opencode/tools/sdd_spec_compliance_linter.js +16 -3
  10. package/.opencode/tools/sdd_spec_validator.js +5 -5
  11. package/.opencode/tools/sdd_transition.js +13 -6
  12. package/.opencode/tools/sdd_ui_auditor.js +34 -14
  13. package/bin/zugzbot.js +39 -6
  14. package/opencode.json +39 -6
  15. package/package.json +1 -1
  16. package/skills/sdd-auto-api-mocker/SKILL.md +62 -0
  17. package/skills/sdd-root-cause-diagnostician/SKILL.md +48 -0
  18. package/skills/sdd-semantic-context-pruner/SKILL.md +45 -0
  19. package/tools/gas_clasp_tools.ts +106 -0
  20. package/tools/index.ts +2 -1
  21. package/tools/sdd_checkpoint.ts +7 -2
  22. package/tools/sdd_regression_detector.ts +13 -5
  23. package/tools/sdd_requirement_tracker.ts +38 -26
  24. package/tools/sdd_spec_compliance_linter.ts +17 -3
  25. package/tools/sdd_spec_validator.ts +6 -6
  26. package/tools/sdd_transition.ts +12 -6
  27. package/tools/sdd_ui_auditor.ts +35 -21
  28. package/docs_opencode/acp.md +0 -165
  29. package/docs_opencode/acp.pdf +0 -0
  30. package/docs_opencode/agents.md +0 -803
  31. package/docs_opencode/agents.pdf +0 -0
  32. package/docs_opencode/commands.md +0 -354
  33. package/docs_opencode/commands.pdf +0 -0
  34. package/docs_opencode/custom-tools.md +0 -209
  35. package/docs_opencode/custom-tools.pdf +0 -0
  36. package/docs_opencode/ecosystem.md +0 -81
  37. package/docs_opencode/ecosystem.pdf +0 -0
  38. package/docs_opencode/formatters.md +0 -142
  39. package/docs_opencode/formatters.pdf +0 -0
  40. package/docs_opencode/keybinds.md +0 -205
  41. package/docs_opencode/keybinds.pdf +0 -0
  42. package/docs_opencode/lsp.md +0 -202
  43. package/docs_opencode/lsp.pdf +0 -0
  44. package/docs_opencode/mcp-servers.md +0 -565
  45. package/docs_opencode/mcp-servers.pdf +0 -0
  46. package/docs_opencode/models.md +0 -234
  47. package/docs_opencode/models.pdf +0 -0
  48. package/docs_opencode/permissions.md +0 -248
  49. package/docs_opencode/permissions.pdf +0 -0
  50. package/docs_opencode/plugins.md +0 -409
  51. package/docs_opencode/plugins.pdf +0 -0
  52. package/docs_opencode/rules.md +0 -189
  53. package/docs_opencode/rules.pdf +0 -0
  54. package/docs_opencode/sdk.md +0 -522
  55. package/docs_opencode/sdk.pdf +0 -0
  56. package/docs_opencode/server.md +0 -324
  57. package/docs_opencode/server.pdf +0 -0
  58. package/docs_opencode/skills.md +0 -235
  59. package/docs_opencode/skills.pdf +0 -0
  60. package/docs_opencode/themes.md +0 -378
  61. package/docs_opencode/themes.pdf +0 -0
  62. package/docs_opencode/tools.md +0 -364
  63. package/docs_opencode/tools.pdf +0 -0
@@ -0,0 +1,62 @@
1
+ # Skill: SDD Automatic API Mocker
2
+
3
+ Esta habilidad permite al enjambre (`@sdd-builder`, `@sdd-tester`) autogenerar simulaciones (mocks) locales inteligentes de APIs de terceros y objetos globales difíciles de simular (ej. Google Apps Script como `SpreadsheetApp`, clasp, APIs de pago o servicios externos).
4
+
5
+ ## Trigger
6
+
7
+ Se activa de manera automática en la **Fase 2 (Construcción)** o **Fase 3 (Calidad)** cuando:
8
+ 1. El archivo `diagnostics.md` o `spec.md` indica dependencias de APIs o globales inaccesibles localmente.
9
+ 2. Los tests unitarios fallan debido a referencias indefinidas (`ReferenceError` o `NetworkError`).
10
+
11
+ ## Directrices de Simulación (Mocks)
12
+
13
+ Al simular una API, el `@sdd-builder` debe seguir estas reglas estructuradas:
14
+
15
+ ### 1. Inyección de Mocks en Entornos Globales
16
+ Si el host es un entorno web o un sandbox interactivo sin Node.js (ej. Google Apps Script), inyecta los mocks al inicio del archivo de pruebas o mediante un archivo `global_mock.js` aislado:
17
+
18
+ ```javascript
19
+ // global_mock.js
20
+ if (typeof SpreadsheetApp === 'undefined') {
21
+ globalThis.SpreadsheetApp = {
22
+ getActiveSpreadsheet: () => ({
23
+ getActiveSheet: () => ({
24
+ getName: () => "Sheet1",
25
+ getDataRange: () => ({
26
+ getValues: () => [["Encabezado 1", "Encabezado 2"], ["Fila 1 Col 1", "Fila 1 Col 2"]]
27
+ }),
28
+ getRange: () => ({
29
+ setValue: () => {},
30
+ getValue: () => "CeldaSimulada"
31
+ })
32
+ })
33
+ })
34
+ };
35
+ }
36
+ ```
37
+
38
+ ### 2. Estructura de Aislamiento de Red (Fetch/REST APIs)
39
+ Evita llamadas reales a servicios HTTP/HTTPS externos interceptando `globalThis.fetch` o inyectando simuladores específicos de API:
40
+
41
+ ```javascript
42
+ // mock_fetch.js
43
+ const mockResponses = {
44
+ "https://api.github.com/repos/": { status: 200, json: async () => ({ name: "zugzbot" }) }
45
+ };
46
+
47
+ globalThis.fetch = async (url) => {
48
+ const match = Object.keys(mockResponses).find(k => url.startsWith(k));
49
+ if (match) return mockResponses[match];
50
+ throw new Error(`[Mock Error] Llamada a red no mockeada para URL: ${url}`);
51
+ };
52
+ ```
53
+
54
+ ## Criterios de Aceptación (QA)
55
+
56
+ - `[ ]` **No Invasivo:** Ningún mock o código de simulación debe colarse en los archivos de producción final. Deben permanecer exclusivamente bajo la carpeta de pruebas `tests/` o archivos temporales de QA.
57
+ - `[ ]` **Transparencia:** Las aserciones de la UI o lógica del negocio no deben detectar la diferencia entre el entorno mockeado y el real.
58
+ - `[ ]` **Limpieza:** Una vez completada la validación, el `@sdd-tester` debe restaurar los entornos globales modificados.
59
+
60
+ ## Tags
61
+
62
+ #sdd #mocking #sandbox #isolation #gas #api
@@ -0,0 +1,48 @@
1
+ # Skill: SDD Root Cause Diagnostician
2
+
3
+ Esta habilidad dota al enjambre de Inteligencia Artificial de capacidades explicativas profundas al detectar fallas en las Fases de Calidad (F3) y Construcción (F2), impidiendo bucles correctivos infinitos mediante el aislamiento del error real y la inyección de guías de remediación inmediatas.
4
+
5
+ ## Trigger
6
+
7
+ Se activa cuando:
8
+ 1. `tools/sdd_regression_detector` retorna `status: "FAILED"` o `status: "FAILED_LOCAL"`.
9
+ 2. Las pruebas unitarias o linters fallan de manera persistente tras un reintento.
10
+
11
+ ## Proceso de Diagnóstico Semántico
12
+
13
+ Al ocurrir una falla, el `@sdd-tester` o `@sdd-builder` debe invocar este proceso cognitivo:
14
+
15
+ ### 1. Extracción del Sintoma vs Causa
16
+ No asumas que el primer error en la pila de llamadas (stacktrace) es el origen real.
17
+ * **Acción:** Busca la línea superior del stacktrace que pertenezca a los archivos modificados listados en `spec.md`. Compara los tipos de parámetros esperados con los provistos.
18
+
19
+ ### 2. Generación del Reporte de Causa Raíz
20
+ Crea un archivo temporal `.openspec/diagnostics/root_cause.md` detallando de forma directa y visual el origen del quiebre:
21
+
22
+ ```markdown
23
+ # 🔍 Diagnóstico de Causa Raíz: [nombre-falla]
24
+
25
+ ## 1. El Quiebre Detectado
26
+ - **Síntoma:** `TypeError: Cannot read properties of undefined (reading 'getName')`
27
+ - **Ubicación:** `src/components/Sidebar.tsx:L45`
28
+
29
+ ## 2. El Origen Real (Causa Raíz)
30
+ El objeto `activeSheet` se consulta de forma asíncrona sin un bloque guardián. En entornos locales mockeados, esta resolución tarda 5ms más de lo esperado, haciendo que la llamada posterior se ejecute sobre un valor `null`.
31
+
32
+ ## 3. Pista de Remediación (Remediation Hint)
33
+ Asegurar la validación del objeto antes de invocar propiedades:
34
+ ```typescript
35
+ const sheetName = activeSheet ? activeSheet.getName() : "Sin Nombre";
36
+ ```
37
+
38
+ ### 3. Inyección en el Bucle de Retorno
39
+ Cuando el ciclo correctivo transicione la fase hacia atrás (Fase 2) o repita la actual, el orquestador `@zugzbot` debe inyectar este archivo `root_cause.md` como entrada prioritaria de contexto en el chat "lienzo en blanco" del `@sdd-builder`.
40
+
41
+ ## Criterios de Aceptación (QA)
42
+
43
+ - `[ ]` **Precisión:** El reporte de causa raíz debe contener una pista de remediación de código lista para ser asimilada por el constructor.
44
+ - `[ ]` **Sin Bucles:** Ningún corrective loop debe superar las 3 iteraciones una vez inyectadas las pistas de causa raíz.
45
+
46
+ ## Tags
47
+
48
+ #sdd #corrective-loops #debugging #diagnostics #root-cause #recovery
@@ -0,0 +1,45 @@
1
+ # Skill: SDD Semantic Context Pruning
2
+
3
+ Esta habilidad define las reglas y heurísticas de ingeniería de prompts para recortar selectivamente los cuerpos de las funciones y código irrelevante en las solicitudes al LLM. Esto reduce el consumo de tokens de entrada hasta en un 70% y mejora sustancialmente la precisión y velocidad de los agentes.
4
+
5
+ ## Trigger
6
+
7
+ Se ejecuta activamente en la **Fase 0 (Explorer)**, **Fase 1 (Planner)** y al delegar tareas a subagentes de **Construcción (F2)**.
8
+
9
+ ## Reglas Operativas de Poda de Contexto
10
+
11
+ El planificador u orquestador debe seguir estas directivas antes de inyectar archivos grandes en el contexto del LLM:
12
+
13
+ ### 1. Poda por Dependencia Localizada (Impacto Acotado)
14
+ No inyectes archivos completos si no están listados en el `spec.md` o en las líneas directamente afectadas obtenidas por `git diff`.
15
+ * **Acción:** Si una función del archivo A llama a una función del archivo B, pero el cambio solo afecta a A, inyecta el archivo A completo y **únicamente la firma de la función** del archivo B.
16
+
17
+ ### 2. Estructura de Poda de Código (Signatures Only)
18
+ Cuando debas referenciar módulos externos gigantes, reemplaza el cuerpo del método por un comentario descriptivo:
19
+
20
+ ```typescript
21
+ // ANTES (Cargar todo es ruidoso y costoso):
22
+ export function processTransaction(tx: Transaction) {
23
+ // ... 300 líneas de lógica de validación, firma, logs, db calls, etc. ...
24
+ }
25
+
26
+ // DESPUÉS (Firma limpia y tipada):
27
+ /**
28
+ * Procesa una transacción financiera en base a reglas de negocio.
29
+ * @param tx Objeto de transacción tipado.
30
+ */
31
+ export function processTransaction(tx: Transaction): TransactionResult; // [Cuerpo omitido por Poda Semántica]
32
+ ```
33
+
34
+ ### 3. Poda Incremental de Archivos de Historial
35
+ Evita enviar historiales acumulados enteros de fallas de compilación o conversaciones previas largas.
36
+ * **Acción:** Provee únicamente el último log de error y el archivo `sdd-lock.json` actualizado para dotar al subagente de un hilo limpio (lienzo en blanco).
37
+
38
+ ## Criterios de Aceptación (QA)
39
+
40
+ - `[ ]` **Eficiencia:** Las delegaciones de prompts a subagentes complejos no deben exceder el 30% del tamaño total del repositorio.
41
+ - `[ ]` **Sin Amnesia:** La información requerida de tipos, parámetros de retorno y convenciones del repositorio (`AGENTS.md`) debe preservarse de forma compacta en todo momento.
42
+
43
+ ## Tags
44
+
45
+ #sdd #context-pruning #prompt-engineering #token-economy #optimization
@@ -0,0 +1,97 @@
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: "Herramienta premium de integración con Google Apps Script (GAS) a través de Clasp. Permite realizar push, pull, consultar despliegues y verificar configuraciones de clasp de forma nativa.",
7
+ args: {
8
+ action: tool.schema.enum(["push", "pull", "status", "deployments"]).describe("La acción de clasp a realizar: push (subir cambios a GAS), pull (descargar cambios de GAS), status (verificar archivos subibles/ignorados), deployments (listar despliegues y URLs)"),
9
+ changeName: tool.schema.string().optional().describe("Nombre del cambio activo en .openspec/changes/ para verificar logs de QA previos")
10
+ },
11
+ async execute(args, context) {
12
+ const projectRoot = context.worktree || context.directory;
13
+ const claspConfigPath = path.join(projectRoot, ".clasp.json");
14
+ if (!fs.existsSync(claspConfigPath)) {
15
+ return JSON.stringify({
16
+ status: "FAILED",
17
+ reason: "❌ ERROR: No se encontró el archivo de configuración '.clasp.json' en la raíz del proyecto. ¿Es este un proyecto de Google Apps Script válido?"
18
+ }, null, 2);
19
+ }
20
+ let claspJson = {};
21
+ try {
22
+ claspJson = JSON.parse(fs.readFileSync(claspConfigPath, "utf-8"));
23
+ }
24
+ catch (e) {
25
+ return JSON.stringify({
26
+ status: "FAILED",
27
+ reason: "❌ ERROR: El archivo de configuración '.clasp.json' tiene un formato JSON inválido."
28
+ }, null, 2);
29
+ }
30
+ const scriptId = claspJson.scriptId;
31
+ if (!scriptId) {
32
+ return JSON.stringify({
33
+ status: "FAILED",
34
+ reason: "❌ ERROR: No se definió 'scriptId' en '.clasp.json'."
35
+ }, null, 2);
36
+ }
37
+ try {
38
+ // 1. Configurar comandos clasp
39
+ let claspCmd = "npx clasp";
40
+ if (args.action === "push") {
41
+ const output = execSync(`${claspCmd} push`, { cwd: projectRoot, encoding: "utf-8", stdio: "pipe" });
42
+ return JSON.stringify({
43
+ status: "SUCCESS",
44
+ action: "push",
45
+ scriptId,
46
+ message: "✅ CLASP PUSH EXITOSO: Los archivos locales se han subido a Google Apps Script sin colisiones.",
47
+ output: output.trim()
48
+ }, null, 2);
49
+ }
50
+ if (args.action === "pull") {
51
+ const output = execSync(`${claspCmd} pull`, { cwd: projectRoot, encoding: "utf-8", stdio: "pipe" });
52
+ return JSON.stringify({
53
+ status: "SUCCESS",
54
+ action: "pull",
55
+ scriptId,
56
+ message: "✅ CLASP PULL EXITOSO: El código remoto de Google Apps Script se ha descargado a local.",
57
+ output: output.trim()
58
+ }, null, 2);
59
+ }
60
+ if (args.action === "status") {
61
+ const output = execSync(`${claspCmd} status`, { cwd: projectRoot, encoding: "utf-8", stdio: "pipe" });
62
+ return JSON.stringify({
63
+ status: "SUCCESS",
64
+ action: "status",
65
+ scriptId,
66
+ message: "✅ CLASP STATUS:",
67
+ output: output.trim()
68
+ }, null, 2);
69
+ }
70
+ if (args.action === "deployments") {
71
+ const output = execSync(`${claspCmd} deployments`, { cwd: projectRoot, encoding: "utf-8", stdio: "pipe" });
72
+ const webAppUrl = `https://script.google.com/macros/s/${scriptId}/exec`;
73
+ return JSON.stringify({
74
+ status: "SUCCESS",
75
+ action: "deployments",
76
+ scriptId,
77
+ webAppUrl,
78
+ message: "✅ DESPLIEGUES ACTIVOS OBTENIDOS:",
79
+ output: output.trim()
80
+ }, null, 2);
81
+ }
82
+ }
83
+ catch (err) {
84
+ const stderr = err.stderr?.toString() || err.message || "";
85
+ return JSON.stringify({
86
+ status: "FAILED",
87
+ action: args.action,
88
+ scriptId,
89
+ reason: `❌ ERROR de ejecución de clasp ${args.action}: ${stderr.trim()}`
90
+ }, null, 2);
91
+ }
92
+ return JSON.stringify({
93
+ status: "FAILED",
94
+ reason: "Acción no reconocida."
95
+ }, null, 2);
96
+ }
97
+ });
@@ -21,3 +21,4 @@ export { default as sdd_test_scaffold_generator } from './sdd_test_scaffold_gene
21
21
  export { default as sdd_spec_compliance_linter } from './sdd_spec_compliance_linter.js';
22
22
  export { default as sdd_sandbox_patcher } from './sdd_sandbox_patcher.js';
23
23
  export { default as sdd_context_pruner } from './sdd_context_pruner.js';
24
+ export { default as gas_clasp_tools } from './gas_clasp_tools.js';
@@ -10,9 +10,15 @@ export default tool({
10
10
  },
11
11
  async execute(args, context) {
12
12
  const projectRoot = context.worktree || context.directory;
13
- const lockfilePath = path.join(projectRoot, ".openspec/sdd-lock.json");
13
+ let lockfilePath = path.join(projectRoot, ".openspec/sdd-lock.json");
14
14
  if (!fs.existsSync(lockfilePath)) {
15
- return "[SDD Checkpoint] ERROR: No existe sdd-lock.json. No se puede crear checkpoint.";
15
+ const altLockPath = path.join(projectRoot, "openspec/sdd-lock.json");
16
+ if (fs.existsSync(altLockPath)) {
17
+ lockfilePath = altLockPath;
18
+ }
19
+ else {
20
+ return "[SDD Checkpoint] ERROR: No existe sdd-lock.json. No se puede crear checkpoint.";
21
+ }
16
22
  }
17
23
  let lockfile = {};
18
24
  try {
@@ -74,13 +74,18 @@ function sanitizeGitPath(line) {
74
74
  // Analiza los errores de una salida de compilador y los mapea de forma limpia
75
75
  function parseCompilerErrors(errorOutput) {
76
76
  const errors = new Set();
77
- const lines = errorOutput.split("\n").filter(l => l.trim());
77
+ const lines = errorOutput.split("\n").map(l => l.trim()).filter(Boolean);
78
+ let currentFile = "";
78
79
  lines.forEach(line => {
79
- // Captura líneas que parezcan errores de compilador (archivo con número de línea y mensaje)
80
- const fileMatch = line.match(/^([a-zA-Z0-9_\-./]+)\(/) || line.match(/^([a-zA-Z0-9_\-./]+):\d+/) || line.match(/in\s+([a-zA-Z0-9_\-./]+\.py)/);
80
+ // Captura líneas que parezcan referirse a un archivo con error
81
+ const fileMatch = line.match(/^([a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+)/i);
81
82
  if (fileMatch) {
82
- // Normalizar simplificando detalles variables de error
83
- errors.add(line.trim());
83
+ currentFile = fileMatch[1];
84
+ }
85
+ // Si la línea contiene palabras clave de error o aviso típico de compilación
86
+ if (line.includes("error") || line.includes("warning") || line.includes("Failed") || line.includes("Mismatched") || line.includes("Unclosed")) {
87
+ const errorMsg = currentFile ? `[${currentFile}] ${line}` : line;
88
+ errors.add(errorMsg);
84
89
  }
85
90
  });
86
91
  return errors;
@@ -103,6 +108,7 @@ export default tool({
103
108
  const hasGit = fs.existsSync(path.join(projectRoot, ".git"));
104
109
  if (hasGit) {
105
110
  try {
111
+ execSync("git config core.quotepath false", { cwd: projectRoot, stdio: "ignore" });
106
112
  const gitOutput = execSync("git status --porcelain", { cwd: projectRoot, encoding: "utf-8" });
107
113
  gitOutput.split("\n").forEach(line => {
108
114
  if (!line || line.length < 4)
@@ -150,36 +150,47 @@ export default tool({
150
150
  }
151
151
  const auditResults = [];
152
152
  criteria.forEach(crit => {
153
- // Extraer palabras clave de más de 3 letras ignorando conectores
154
- const keywords = crit
155
- .toLowerCase()
156
- .replace(/[^a-záéíóúñü0-9\s]/g, "")
157
- .split(/\s+/)
158
- .filter(word => word.length > 3 && !["debe", "para", "como", "esta", "este", "consecuente"].includes(word));
153
+ // Verificar si el criterio individual tiene un bypass manual explícito
154
+ const isManualCrit = crit.toLowerCase().includes("[manual]") ||
155
+ crit.toLowerCase().includes("[e2e]") ||
156
+ crit.toLowerCase().includes("[qa manual]");
159
157
  let matched = false;
160
158
  let matchedFile = "";
161
159
  let matchedSnippet = "";
162
- for (const testFile of testFiles) {
163
- try {
164
- const testContent = fs.readFileSync(testFile, "utf-8");
165
- const testLines = testContent.split("\n");
166
- // Búsqueda aproximada: comprobar si el test contiene palabras clave del criterio
167
- for (let i = 0; i < testLines.length; i++) {
168
- const line = testLines[i].toLowerCase();
169
- // Si coincide con más del 60% de las palabras clave de un criterio en la misma línea
170
- const matchCount = keywords.filter(keyword => line.includes(keyword)).length;
171
- const threshold = Math.max(1, Math.floor(keywords.length * 0.5));
172
- if (matchCount >= threshold && threshold > 0) {
173
- matched = true;
174
- matchedFile = path.basename(testFile);
175
- matchedSnippet = `Línea ${i + 1}: ${testLines[i].trim()}`;
176
- break;
160
+ if (isManualCrit) {
161
+ matched = true;
162
+ matchedFile = "QA Manual / E2E Bypass";
163
+ matchedSnippet = "Validación manual explícita declarada en spec.md";
164
+ }
165
+ else {
166
+ // Extraer palabras clave de más de 3 letras ignorando conectores
167
+ const keywords = crit
168
+ .toLowerCase()
169
+ .replace(/[^a-záéíóúñü0-9\s]/g, "")
170
+ .split(/\s+/)
171
+ .filter(word => word.length > 3 && !["debe", "para", "como", "esta", "este", "consecuente"].includes(word));
172
+ for (const testFile of testFiles) {
173
+ try {
174
+ const testContent = fs.readFileSync(testFile, "utf-8");
175
+ const testLines = testContent.split("\n");
176
+ // Búsqueda aproximada: comprobar si el test contiene palabras clave del criterio
177
+ for (let i = 0; i < testLines.length; i++) {
178
+ const line = testLines[i].toLowerCase();
179
+ // Si coincide con más del 60% de las palabras clave de un criterio en la misma línea
180
+ const matchCount = keywords.filter(keyword => line.includes(keyword)).length;
181
+ const threshold = Math.max(1, Math.floor(keywords.length * 0.5));
182
+ if (matchCount >= threshold && threshold > 0) {
183
+ matched = true;
184
+ matchedFile = path.basename(testFile);
185
+ matchedSnippet = `Línea ${i + 1}: ${testLines[i].trim()}`;
186
+ break;
187
+ }
177
188
  }
189
+ if (matched)
190
+ break;
178
191
  }
179
- if (matched)
180
- break;
192
+ catch (e) { }
181
193
  }
182
- catch (e) { }
183
194
  }
184
195
  auditResults.push({
185
196
  criterio: crit,
@@ -11,7 +11,11 @@ export default tool({
11
11
  const report = [];
12
12
  report.push(`━━━ sdd_spec_compliance_linter: ${args.changeName} ━━━`);
13
13
  const openspecDir = path.join(projectRoot, ".openspec");
14
- const specFile = path.join(openspecDir, "changes", args.changeName, "spec.md");
14
+ const changeDir = path.join(openspecDir, "changes", args.changeName);
15
+ let specFile = path.join(changeDir, "specs/spec.md");
16
+ if (!fs.existsSync(specFile)) {
17
+ specFile = path.join(changeDir, "spec.md");
18
+ }
15
19
  let specContent = "";
16
20
  if (fs.existsSync(specFile)) {
17
21
  specContent = fs.readFileSync(specFile, "utf-8");
@@ -24,7 +28,12 @@ export default tool({
24
28
  }
25
29
  if (!specContent) {
26
30
  report.push("⚠ No se encontró ningún archivo `spec.md` activo para el cambio. Imposible realizar cruce de especificaciones.");
27
- return report.join("\n");
31
+ return JSON.stringify({
32
+ status: "FAILED",
33
+ complianceRate: 0,
34
+ message: "No se encontró el archivo spec.md.",
35
+ report: report.join("\n")
36
+ }, null, 2);
28
37
  }
29
38
  // Extraer requerimientos
30
39
  const requirements = [];
@@ -108,6 +117,10 @@ export default tool({
108
117
  else {
109
118
  report.push("⚠ Atención: Completa los requerimientos huérfanos indicados arriba para evitar regresiones lógicas.");
110
119
  }
111
- return report.join("\n");
120
+ return JSON.stringify({
121
+ status: complianceRate === 100 ? "APPROVED" : "FAILED",
122
+ complianceRate,
123
+ report: report.join("\n")
124
+ }, null, 2);
112
125
  }
113
126
  });
@@ -40,13 +40,13 @@ export default tool({
40
40
  const specContent = fs.readFileSync(specPath, "utf-8");
41
41
  // Secciones requeridas y sus expresiones regulares de validación según complejidad
42
42
  const requiredSections = [
43
- { name: "Plano Técnico / Título", regex: /^# Plano Técnico de Especificación/m },
44
- { name: "1. Diagnóstico y Archivos Afectados", regex: /^## 1\. Diagnóstico/m },
45
- { name: "3. Propuesta de Solución y Arquitectura", regex: /^## 3\. Propuesta/m },
46
- { name: "5. Criterios de Aceptación y Calidad", regex: /^## 5\. Criterios/m }
43
+ { name: "Plano Técnico / Título", regex: /^#\s*Plano\s+Técnico/mi },
44
+ { name: "1. Diagnóstico y Archivos Afectados", regex: /^##\s*1\.\s*Diagnóstico/mi },
45
+ { name: "3. Propuesta de Solución y Arquitectura", regex: /^##\s*3\.\s*Propuesta/mi },
46
+ { name: "5. Criterios de Aceptación y Calidad", regex: /^##\s*5\.\s*Criterios/mi }
47
47
  ];
48
48
  if (complexity !== "low") {
49
- requiredSections.push({ name: "2. Consenso de Encuesta con el Usuario", regex: /^## 2\. Consenso/m }, { name: "4. Especificaciones BDD (Comportamiento)", regex: /^## 4\. Especificaciones BDD|Feature:/m });
49
+ requiredSections.push({ name: "2. Consenso de Encuesta con el Usuario", regex: /^##\s*2\.\s*Consenso/mi }, { name: "4. Especificaciones BDD (Comportamiento)", regex: /^##\s*4\.\s*Especificaciones BDD|Feature:/mi });
50
50
  }
51
51
  const missingSections = [];
52
52
  requiredSections.forEach(section => {
@@ -128,7 +128,7 @@ export default tool({
128
128
  if (fs.existsSync(specPath)) {
129
129
  try {
130
130
  const specContent = fs.readFileSync(specPath, "utf-8");
131
- const qaSectionIndex = specContent.indexOf("## 5. Criterios de Aceptación");
131
+ const qaSectionIndex = specContent.search(/##\s*5\s*[\.\s-]?\s*Criterios/i);
132
132
  if (qaSectionIndex !== -1) {
133
133
  const qaContent = specContent.substring(qaSectionIndex);
134
134
  const lines = qaContent.split("\n");
@@ -165,9 +165,9 @@ export default tool({
165
165
  const reportContent = fs.readFileSync(reportPath, "utf-8");
166
166
  // Buscar sección QA con fallback: probar ## QA (template tester) primero,
167
167
  // luego ## 3. Correspondencia de Criterios (template anterior), luego buscar - [x] en cualquier parte
168
- let qaSectionIndex = reportContent.indexOf("## QA");
168
+ let qaSectionIndex = reportContent.search(/##\s*QA/i);
169
169
  if (qaSectionIndex === -1) {
170
- qaSectionIndex = reportContent.indexOf("## 3. Correspondencia de Criterios");
170
+ qaSectionIndex = reportContent.search(/##\s*3\s*[\.\s-]?\s*Correspondencia/i);
171
171
  }
172
172
  if (qaSectionIndex !== -1) {
173
173
  const qaContent = reportContent.substring(qaSectionIndex);
@@ -370,9 +370,16 @@ export default tool({
370
370
  }
371
371
  // 2. Hacer commit automático de los artefactos .openspec/
372
372
  execSync("git add .openspec/", { cwd: projectRoot, stdio: "ignore" });
373
- const commitMsg = `docs(sdd): transición a fase ${args.nextPhase} - ${args.reason.replace(/"/g, '\\"')}`;
374
- execSync(`git commit -m "${commitMsg}"`, { cwd: projectRoot, stdio: "ignore" });
375
- gitStatus = ` [Git: Rama '${branchName}' actualizada con commit semántico]`;
373
+ // Verificar si hay cambios reales preparados en el stage (staged changes)
374
+ const hasStagedChanges = execSync("git diff --cached --name-only", { cwd: projectRoot, encoding: "utf-8" }).trim().length > 0;
375
+ if (hasStagedChanges) {
376
+ const commitMsg = `docs(sdd): transición a fase ${args.nextPhase} - ${args.reason.replace(/"/g, '\\"')}`;
377
+ execSync(`git commit -m "${commitMsg}"`, { cwd: projectRoot, stdio: "ignore" });
378
+ gitStatus = ` [Git: Rama '${branchName}' actualizada con commit semántico]`;
379
+ }
380
+ else {
381
+ gitStatus = ` [Git: Sin cambios nuevos en especificaciones para archivar]`;
382
+ }
376
383
  }
377
384
  catch (e) {
378
385
  gitStatus = ` [Git Warning: No se pudo realizar commit automático: ${e.message || e}]`;
@@ -77,7 +77,6 @@ function checkHtmlTagBalance(filePath, content) {
77
77
  return [];
78
78
  }
79
79
  const issues = [];
80
- const tagsToCheck = ["div", "span", "section", "p", "button", "main", "header", "footer", "a", "ul", "ol", "li"];
81
80
  let cleaned = content;
82
81
  if (ext === ".html") {
83
82
  cleaned = content.replace(/<!--[\s\S]*?-->/g, "");
@@ -87,22 +86,43 @@ function checkHtmlTagBalance(filePath, content) {
87
86
  else {
88
87
  cleaned = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ""); // JS/TS comments
89
88
  }
90
- for (const tag of tagsToCheck) {
91
- const openRegex = new RegExp(`<${tag}\\b[^>]*[^/]>`, "gi");
92
- const closeRegex = new RegExp(`</${tag}\\s*>`, "gi");
93
- let openCount = 0;
94
- let closeCount = 0;
95
- let match;
96
- while ((match = openRegex.exec(cleaned)) !== null) {
97
- openCount++;
98
- }
99
- while ((match = closeRegex.exec(cleaned)) !== null) {
100
- closeCount++;
89
+ // Strip TypeScript generic type arguments from function calls or declarations (e.g. createSignal<string[]>)
90
+ // JSX tags are never directly preceded by a word character (like \w+), whereas TS generics are.
91
+ cleaned = cleaned.replace(/(\w+)<([A-Za-z0-9_\[\]\s|]+)>/g, '$1');
92
+ const tagRegex = /<(\/?[a-zA-Z0-9:-]+)(?:\s+[^>]*?)?>/g;
93
+ const stack = [];
94
+ const selfClosingTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
95
+ let match;
96
+ while ((match = tagRegex.exec(cleaned)) !== null) {
97
+ const fullTag = match[0];
98
+ const tagName = match[1].toLowerCase();
99
+ if (fullTag.endsWith('/>'))
100
+ continue;
101
+ if (selfClosingTags.has(tagName))
102
+ continue;
103
+ if (fullTag.startsWith('<?') || fullTag.endsWith('?>'))
104
+ continue;
105
+ if (fullTag.startsWith('<!'))
106
+ continue;
107
+ if (tagName.startsWith('/')) {
108
+ const closingName = tagName.substring(1);
109
+ if (stack.length === 0) {
110
+ issues.push(`Etiqueta de cierre sin apertura: </${closingName}>`);
111
+ break;
112
+ }
113
+ const lastOpen = stack.pop();
114
+ if (lastOpen !== closingName) {
115
+ issues.push(`Anidamiento roto: se esperaba </${lastOpen}> pero se encontró </${closingName}>`);
116
+ break;
117
+ }
101
118
  }
102
- if (openCount !== closeCount) {
103
- issues.push(`Desbalance en etiquetas '<${tag}>': Encontradas ${openCount} de apertura y ${closeCount} de cierre. Esto puede quebrar el DOM global de la aplicación.`);
119
+ else {
120
+ stack.push(tagName);
104
121
  }
105
122
  }
123
+ if (stack.length > 0 && issues.length === 0) {
124
+ issues.push(`Etiquetas sin cerrar: <${stack.join('>, <')}>`);
125
+ }
106
126
  return issues;
107
127
  }
108
128
  export default tool({