zugzbot-sdd 1.5.26 → 1.5.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/skills/sdd-auto-api-mocker/SKILL.md +62 -0
- package/.opencode/skills/sdd-root-cause-diagnostician/SKILL.md +48 -0
- package/.opencode/skills/sdd-semantic-context-pruner/SKILL.md +45 -0
- package/.opencode/tools/gas_clasp_tools.js +97 -0
- package/.opencode/tools/index.js +1 -0
- package/.opencode/tools/sdd_checkpoint.js +8 -2
- package/.opencode/tools/sdd_regression_detector.js +11 -5
- package/.opencode/tools/sdd_requirement_tracker.js +35 -24
- package/.opencode/tools/sdd_spec_compliance_linter.js +16 -3
- package/.opencode/tools/sdd_spec_validator.js +5 -5
- package/.opencode/tools/sdd_transition.js +13 -6
- package/.opencode/tools/sdd_ui_auditor.js +34 -14
- package/AGENTS.md +7 -1
- package/bin/zugzbot.js +39 -6
- package/opencode.json +39 -6
- package/package.json +1 -1
- package/skills/sdd-auto-api-mocker/SKILL.md +62 -0
- package/skills/sdd-root-cause-diagnostician/SKILL.md +48 -0
- package/skills/sdd-semantic-context-pruner/SKILL.md +45 -0
- package/tools/gas_clasp_tools.ts +106 -0
- package/tools/index.ts +2 -1
- package/tools/sdd_checkpoint.ts +7 -2
- package/tools/sdd_regression_detector.ts +13 -5
- package/tools/sdd_requirement_tracker.ts +38 -26
- package/tools/sdd_spec_compliance_linter.ts +17 -3
- package/tools/sdd_spec_validator.ts +6 -6
- package/tools/sdd_transition.ts +12 -6
- package/tools/sdd_ui_auditor.ts +35 -21
- package/docs_opencode/acp.md +0 -165
- package/docs_opencode/acp.pdf +0 -0
- package/docs_opencode/agents.md +0 -803
- package/docs_opencode/agents.pdf +0 -0
- package/docs_opencode/commands.md +0 -354
- package/docs_opencode/commands.pdf +0 -0
- package/docs_opencode/custom-tools.md +0 -209
- package/docs_opencode/custom-tools.pdf +0 -0
- package/docs_opencode/ecosystem.md +0 -81
- package/docs_opencode/ecosystem.pdf +0 -0
- package/docs_opencode/formatters.md +0 -142
- package/docs_opencode/formatters.pdf +0 -0
- package/docs_opencode/keybinds.md +0 -205
- package/docs_opencode/keybinds.pdf +0 -0
- package/docs_opencode/lsp.md +0 -202
- package/docs_opencode/lsp.pdf +0 -0
- package/docs_opencode/mcp-servers.md +0 -565
- package/docs_opencode/mcp-servers.pdf +0 -0
- package/docs_opencode/models.md +0 -234
- package/docs_opencode/models.pdf +0 -0
- package/docs_opencode/permissions.md +0 -248
- package/docs_opencode/permissions.pdf +0 -0
- package/docs_opencode/plugins.md +0 -409
- package/docs_opencode/plugins.pdf +0 -0
- package/docs_opencode/rules.md +0 -189
- package/docs_opencode/rules.pdf +0 -0
- package/docs_opencode/sdk.md +0 -522
- package/docs_opencode/sdk.pdf +0 -0
- package/docs_opencode/server.md +0 -324
- package/docs_opencode/server.pdf +0 -0
- package/docs_opencode/skills.md +0 -235
- package/docs_opencode/skills.pdf +0 -0
- package/docs_opencode/themes.md +0 -378
- package/docs_opencode/themes.pdf +0 -0
- package/docs_opencode/tools.md +0 -364
- 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
|
+
});
|
package/.opencode/tools/index.js
CHANGED
|
@@ -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
|
-
|
|
13
|
+
let lockfilePath = path.join(projectRoot, ".openspec/sdd-lock.json");
|
|
14
14
|
if (!fs.existsSync(lockfilePath)) {
|
|
15
|
-
|
|
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").
|
|
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
|
|
80
|
-
const fileMatch = line.match(/^([a-zA-Z0-9_\-./]
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
//
|
|
154
|
-
const
|
|
155
|
-
.toLowerCase()
|
|
156
|
-
.
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
44
|
-
{ name: "1. Diagnóstico y Archivos Afectados", regex:
|
|
45
|
-
{ name: "3. Propuesta de Solución y Arquitectura", regex:
|
|
46
|
-
{ name: "5. Criterios de Aceptación y Calidad", regex:
|
|
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:
|
|
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.
|
|
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.
|
|
168
|
+
let qaSectionIndex = reportContent.search(/##\s*QA/i);
|
|
169
169
|
if (qaSectionIndex === -1) {
|
|
170
|
-
qaSectionIndex = reportContent.
|
|
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
|
-
|
|
374
|
-
execSync(
|
|
375
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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({
|
package/AGENTS.md
CHANGED
|
@@ -204,7 +204,13 @@ Feature: [Breve descripción de la funcionalidad]
|
|
|
204
204
|
2. **🛡️ Cooldown de Dependencias**:
|
|
205
205
|
- Cualquier dependencia agregada debe tener al menos **3 días de publicada** en el registro oficial. Carga la habilidad `sdd-dependency-cooldown` para verificar su antigüedad antes de cualquier importación.
|
|
206
206
|
|
|
207
|
-
3.
|
|
207
|
+
3. **⚡ Directivas de Activación y Uso de Skills Premium**:
|
|
208
|
+
El enjambre cuenta con habilidades premium cargadas dinámicamente en `.opencode/skills/`. Todos los agentes deben invocar y apegarse estrictamente a estas guías bajo las siguientes condiciones:
|
|
209
|
+
- **`sdd-auto-api-mocker` (Triggers: Fase 2 / Fase 3)**: Si el proyecto host contiene APIs inaccesibles localmente (como objetos globales de Google Apps Script `SpreadsheetApp`, clasp, APIs REST de terceros, bases de datos remotas), el `@sdd-builder` y `@sdd-tester` deben activar esta habilidad para simular mocks de red y sandbox locales no invasivos.
|
|
210
|
+
- **`sdd-semantic-context-pruner` (Triggers: Fase 0 / Fase 1 / Fase 2)**: Al indexar archivos o delegar tareas, los agentes deben usar poda semántica para eliminar cuerpos de métodos no afectados y enviar únicamente declaraciones de firmas/tipos para optimizar el consumo de tokens en un 70%.
|
|
211
|
+
- **`sdd-root-cause-diagnostician` (Trigger: Fallo de validación en F3 / F2)**: Ante cualquier error de linter o compilación, el `@sdd-tester` debe ejecutar esta habilidad para redactar un diagnóstico de causa raíz accionable en `.openspec/diagnostics/root_cause.md`, eliminando bucles infinitos de fallas.
|
|
212
|
+
|
|
213
|
+
4. **🔬 Estructura Estándar de Testing Agnóstico**:
|
|
208
214
|
- Todo proyecto gestionado por el arnés debe estructurar su carpeta de pruebas `tests/` en tres subdirectorios específicos:
|
|
209
215
|
* `tests/unit/`: Para pruebas unitarias de funciones aisladas.
|
|
210
216
|
* `tests/static/`: Para validadores estáticos universales y agnósticos (ej: balance de etiquetas HTML en `tag_balance.js`, detección de IDs duplicados en `dom_structure.js`).
|