zugzbot-sdd 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/AGENTS.md +212 -0
  2. package/README.md +112 -0
  3. package/ZUGZ.md +91 -0
  4. package/agents/aux-handyman.md +36 -0
  5. package/agents/aux-oracle.md +39 -0
  6. package/agents/sdd-archiver.md +33 -0
  7. package/agents/sdd-builder.md +29 -0
  8. package/agents/sdd-deployer.md +43 -0
  9. package/agents/sdd-explorer.md +49 -0
  10. package/agents/sdd-planner.md +59 -0
  11. package/agents/sdd-tester.md +51 -0
  12. package/agents/zugzbot.md +84 -0
  13. package/bin/zugzbot.js +249 -0
  14. package/bun.lock +259 -0
  15. package/commands/sdd-archiver.md +11 -0
  16. package/commands/sdd-builder.md +11 -0
  17. package/commands/sdd-deployer.md +12 -0
  18. package/commands/sdd-explorer.md +11 -0
  19. package/commands/sdd-planner.md +11 -0
  20. package/commands/sdd-tester.md +12 -0
  21. package/commands/sdd.md +11 -0
  22. package/eslint.config.js +51 -0
  23. package/opencode.json +121 -0
  24. package/package.json +46 -0
  25. package/plugin.json +10 -0
  26. package/plugins/plugin_sdd_core.ts +54 -0
  27. package/plugins/plugin_tui.tsx +318 -0
  28. package/sdd +1228 -0
  29. package/skills/sdd-dependency-cooldown/SKILL.md +40 -0
  30. package/skills/sdd-tree-generator/SKILL.md +40 -0
  31. package/skills-lock.json +35 -0
  32. package/tests/static/dom_structure.test.js +57 -0
  33. package/tests/static/tag_balance.test.js +74 -0
  34. package/tests/unit/harness_structure.test.js +65 -0
  35. package/tools/brain-utils.ts +122 -0
  36. package/tools/check_dependency_cooldown.ts +134 -0
  37. package/tools/index.ts +14 -0
  38. package/tools/sdd_archive_and_commit.ts +207 -0
  39. package/tools/sdd_bdd_tester.ts +163 -0
  40. package/tools/sdd_brain_sync.ts +160 -0
  41. package/tools/sdd_checkpoint.ts +142 -0
  42. package/tools/sdd_compact_context.ts +122 -0
  43. package/tools/sdd_generate_tree.ts +64 -0
  44. package/tools/sdd_install_autoskills.ts +100 -0
  45. package/tools/sdd_regression_detector.ts +241 -0
  46. package/tools/sdd_requirement_tracker.ts +236 -0
  47. package/tools/sdd_secret_scanner.ts +205 -0
  48. package/tools/sdd_spec_validator.ts +139 -0
  49. package/tools/sdd_transition.ts +375 -0
  50. package/tools/sdd_ui_auditor.ts +310 -0
  51. package/tsconfig.json +28 -0
  52. package/zugz-models.json +23 -0
@@ -0,0 +1,40 @@
1
+ # Skill: Dependency Cooldown Checker
2
+
3
+ Verifica que las dependencias tengan al menos **3 días** de publicadas antes de ser importadas.
4
+
5
+ ## Trigger
6
+
7
+ Cuando se requiera agregar una nueva dependencia npm, antes de cualquier `import` o `require`.
8
+
9
+ ## Uso
10
+
11
+ ```bash
12
+ npx -y npm-check-updates <package-name> --dep prod
13
+ # o verificar en https://www.npmjs.com/package/<package-name>
14
+ ```
15
+
16
+ ## Regla SDD Cooldown
17
+
18
+ | Tipo | Tiempo mínimo |
19
+ |:---|:---|
20
+ | Dependencias nuevas | 3 días desde publicación |
21
+ | Updates mayores (major) | 7 días |
22
+ | Dependencias критични (security) | Sin cooldown |
23
+
24
+ ## Verificación Manual
25
+
26
+ 1. Ir a https://www.npmjs.com/package/<nombre-paquete>
27
+ 2. Buscar "Published" en la sección de metadata
28
+ 3. Calcular días desde publicación
29
+ 4. Si >= 3 días, APPROVED
30
+ 5. Si < 3 días, WAIT con fecha de aprobación
31
+
32
+ ## Criterios de Bloqueo
33
+
34
+ - ❌ Paquetes con < 3 días
35
+ - ❌ Paquetes sin downloads recientes (posible abandonware)
36
+ - ❌ Paquetes con vulnerabilities conocidas sin fix
37
+
38
+ ## Tags
39
+
40
+ #sdd #dependency #npm #cooldown
@@ -0,0 +1,40 @@
1
+ # Skill: SDD Tree Generator
2
+
3
+ Genera un árbol de estructura del proyecto en milisegundos sin costo de tokens.
4
+
5
+ ## Trigger
6
+
7
+ Cuando se requiera visualizar la estructura de archivos del proyecto.
8
+
9
+ ## Uso
10
+
11
+ ```bash
12
+ node .opencode/tools/sdd_generate_tree.js
13
+ ```
14
+
15
+ O desde OpenCode usando la herramienta `sdd_generate_tree`.
16
+
17
+ ## Output
18
+
19
+ ```
20
+ proyecto/
21
+ ├── src/
22
+ │ ├── components/
23
+ │ │ └── Button.tsx
24
+ │ └── index.ts
25
+ ├── tests/
26
+ │ └── unit/
27
+ ├── package.json
28
+ └── README.md
29
+ ```
30
+
31
+ ## Características
32
+
33
+ - Profundidad máxima: 3 niveles
34
+ - Ignora: node_modules, .git, archivos ocultos
35
+ - Muestra tamaño de archivos
36
+ - Ejecución en < 100ms
37
+
38
+ ## Tags
39
+
40
+ #sdd #tooling #tree #structure
@@ -0,0 +1,35 @@
1
+ {
2
+ "version": 1,
3
+ "skills": {
4
+ "accessibility": {
5
+ "source": "addyosmani/web-quality-skills",
6
+ "sourceType": "autoskills-registry",
7
+ "computedHash": "bffe3d08cfe92ebad63699f74ce29e35c19850ebfbf474c1463183cfe34d6a09"
8
+ },
9
+ "bash-defensive-patterns": {
10
+ "source": "wshobson/agents",
11
+ "sourceType": "autoskills-registry",
12
+ "computedHash": "1f566c2d9c3b50a784b05d08f6b08ebfeb31f038e4d4173f2d488484edb058a6"
13
+ },
14
+ "bun": {
15
+ "source": "midudev/autoskills",
16
+ "sourceType": "autoskills-registry",
17
+ "computedHash": "1386ed5490ed278e38312a416c9aa074dbe3849f32ae7f841e4142fef5d4df94"
18
+ },
19
+ "frontend-design": {
20
+ "source": "anthropics/skills",
21
+ "sourceType": "autoskills-registry",
22
+ "computedHash": "82fb11a63fb1e35ee2469516ed02d54695f783115b1540c0e783197af4240a3a"
23
+ },
24
+ "seo": {
25
+ "source": "addyosmani/web-quality-skills",
26
+ "sourceType": "autoskills-registry",
27
+ "computedHash": "c184da724d1c61ad077f27418ea8e7e88fd54bcdf98165e18be7e4681cbd5e20"
28
+ },
29
+ "typescript-advanced-types": {
30
+ "source": "wshobson/agents",
31
+ "sourceType": "autoskills-registry",
32
+ "computedHash": "5ca0e177c6aaaba1889255691224daafdb7d71f317cc70bede1590d3907ded42"
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,57 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ function getFilesRecursively(dir, extensions) {
6
+ let results = [];
7
+ if (!fs.existsSync(dir)) return results;
8
+ const list = fs.readdirSync(dir);
9
+ list.forEach(file => {
10
+ const fullPath = path.join(dir, file);
11
+ const stat = fs.statSync(fullPath);
12
+ if (stat && stat.isDirectory()) {
13
+ results = results.concat(getFilesRecursively(fullPath, extensions));
14
+ } else {
15
+ if (extensions.some(ext => file.endsWith(ext))) {
16
+ results.push(fullPath);
17
+ }
18
+ }
19
+ });
20
+ return results;
21
+ }
22
+
23
+ function findDuplicateIds(content) {
24
+ const cleaned = content.replace(/<!--[\s\S]*?-->/g, '');
25
+ const idRegex = /id=["']([^"']+)["']/g;
26
+ const ids = [];
27
+ let match;
28
+ while ((match = idRegex.exec(cleaned)) !== null) {
29
+ ids.push(match[1]);
30
+ }
31
+
32
+ const seen = new Set();
33
+ const duplicates = [];
34
+ ids.forEach(id => {
35
+ if (seen.has(id)) {
36
+ duplicates.push(id);
37
+ } else {
38
+ seen.add(id);
39
+ }
40
+ });
41
+
42
+ return duplicates;
43
+ }
44
+
45
+ describe('DOM Structure Validator', () => {
46
+ test('Plugin UI files should not contain duplicate IDs', () => {
47
+ const pluginDir = path.resolve(process.cwd(), '.opencode', 'plugins');
48
+ if (!fs.existsSync(pluginDir)) return;
49
+
50
+ const files = getFilesRecursively(pluginDir, ['.tsx', '.jsx', '.html']);
51
+ files.forEach(file => {
52
+ const content = fs.readFileSync(file, 'utf-8');
53
+ const duplicates = findDuplicateIds(content);
54
+ expect(duplicates.length, `${path.basename(file)} has duplicate IDs: ${duplicates.join(', ')}`).toBe(0);
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ function getFilesRecursively(dir, extensions) {
6
+ let results = [];
7
+ if (!fs.existsSync(dir)) return results;
8
+ const list = fs.readdirSync(dir);
9
+ list.forEach(file => {
10
+ const fullPath = path.join(dir, file);
11
+ const stat = fs.statSync(fullPath);
12
+ if (stat && stat.isDirectory()) {
13
+ results = results.concat(getFilesRecursively(fullPath, extensions));
14
+ } else {
15
+ if (extensions.some(ext => file.endsWith(ext))) {
16
+ results.push(fullPath);
17
+ }
18
+ }
19
+ });
20
+ return results;
21
+ }
22
+
23
+ function checkTagBalance(content) {
24
+ let cleaned = content.replace(/<!--[\s\S]*?-->/g, '');
25
+ cleaned = cleaned.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '');
26
+ cleaned = cleaned.replace(/<style[\s\S]*?>[\s\S]*?<\/style>/gi, '');
27
+
28
+ const tagRegex = /<(\/?[a-zA-Z0-9:-]+)(?:\s+[^>]*?)?>/g;
29
+ const stack = [];
30
+ const selfClosingTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
31
+
32
+ let match;
33
+ while ((match = tagRegex.exec(cleaned)) !== null) {
34
+ const fullTag = match[0];
35
+ const tagName = match[1].toLowerCase();
36
+
37
+ if (fullTag.endsWith('/>')) continue;
38
+ if (selfClosingTags.has(tagName)) continue;
39
+ if (fullTag.startsWith('<?') || fullTag.endsWith('?>')) continue;
40
+ if (fullTag.startsWith('<!')) continue;
41
+
42
+ if (tagName.startsWith('/')) {
43
+ const closingName = tagName.substring(1);
44
+ if (stack.length === 0) {
45
+ return { balanced: false, error: `Closing tag without opener: </${closingName}>` };
46
+ }
47
+ const lastOpen = stack.pop();
48
+ if (lastOpen !== closingName) {
49
+ return { balanced: false, error: `Mismatched: expected </${lastOpen}> but found </${closingName}>` };
50
+ }
51
+ } else {
52
+ stack.push(tagName);
53
+ }
54
+ }
55
+
56
+ if (stack.length > 0) {
57
+ return { balanced: false, error: `Unclosed tags: <${stack.join('>, <')}> `};
58
+ }
59
+ return { balanced: true };
60
+ }
61
+
62
+ describe('Tag Balance Validator', () => {
63
+ test('Plugin UI files should have balanced tags', () => {
64
+ const pluginDir = path.resolve(process.cwd(), '.opencode', 'plugins');
65
+ if (!fs.existsSync(pluginDir)) return;
66
+
67
+ const files = getFilesRecursively(pluginDir, ['.tsx', '.jsx', '.ts', '.js']);
68
+ files.forEach(file => {
69
+ const content = fs.readFileSync(file, 'utf-8');
70
+ const result = checkTagBalance(content);
71
+ expect(result.balanced, `${path.basename(file)}: ${result.error}`).toBe(true);
72
+ });
73
+ });
74
+ });
@@ -0,0 +1,65 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { describe, test, expect } from 'vitest';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const rootDir = path.resolve(__dirname, '..', '..');
9
+
10
+ describe('Tool Scripts Existence', () => {
11
+ const tools = [
12
+ 'sdd_generate_tree.js',
13
+ 'sdd_install_autoskills.js',
14
+ 'sdd_transition.js',
15
+ 'sdd_checkpoint.js',
16
+ 'sdd_ui_auditor.js',
17
+ 'sdd_archive_and_commit.js',
18
+ 'sdd_brain_sync.js'
19
+ ];
20
+
21
+ tools.forEach(tool => {
22
+ test(`${tool} should exist`, () => {
23
+ const toolPath = path.join(rootDir, '.opencode', 'tools', tool);
24
+ expect(fs.existsSync(toolPath), `${tool} should exist at ${toolPath}`).toBe(true);
25
+ });
26
+ });
27
+ });
28
+
29
+ describe('Agent Files Existence', () => {
30
+ const agents = [
31
+ 'zugzbot.md',
32
+ 'sdd-explorer.md',
33
+ 'sdd-planner.md',
34
+ 'sdd-builder.md',
35
+ 'sdd-tester.md',
36
+ 'sdd-archiver.md',
37
+ 'aux-handyman.md',
38
+ 'aux-oracle.md'
39
+ ];
40
+
41
+ agents.forEach(agent => {
42
+ test(`${agent} should exist`, () => {
43
+ const agentPath = path.join(rootDir, 'agents', agent);
44
+ expect(fs.existsSync(agentPath), `${agent} should exist`).toBe(true);
45
+ });
46
+ });
47
+ });
48
+
49
+ describe('Config Files', () => {
50
+ test('package.json should exist', () => {
51
+ expect(fs.existsSync(path.join(rootDir, 'package.json'))).toBe(true);
52
+ });
53
+
54
+ test('plugin.json should exist', () => {
55
+ expect(fs.existsSync(path.join(rootDir, 'plugin.json'))).toBe(true);
56
+ });
57
+
58
+ test('eslint.config.js should exist', () => {
59
+ expect(fs.existsSync(path.join(rootDir, 'eslint.config.js'))).toBe(true);
60
+ });
61
+
62
+ test('zugz-models.json should exist', () => {
63
+ expect(fs.existsSync(path.join(rootDir, 'zugz-models.json'))).toBe(true);
64
+ });
65
+ });
@@ -0,0 +1,122 @@
1
+ export interface BrainEntry {
2
+ id: string
3
+ category: string
4
+ tag: string
5
+ problem: string
6
+ solution: string
7
+ date: string
8
+ }
9
+
10
+ import fs from "fs"
11
+ import path from "path"
12
+
13
+ export function today(): string {
14
+ return new Date().toISOString().split("T")[0]
15
+ }
16
+
17
+ export function nextId(entries: BrainEntry[]): string {
18
+ let max = 0
19
+ for (const e of entries) {
20
+ const num = parseInt(e.id.substring(1), 10)
21
+ if (!isNaN(num) && num > max) max = num
22
+ }
23
+ return `L${String(max + 1).padStart(3, "0")}`
24
+ }
25
+
26
+ export function parseEntries(content: string): BrainEntry[] {
27
+ const entries: BrainEntry[] = []
28
+ const blocks = content.split("\n### ")
29
+ for (const block of blocks) {
30
+ if (!block.trim()) continue
31
+ const lines = block.split("\n")
32
+ const header = lines[0].trim()
33
+ const colonIdx = header.indexOf(": ")
34
+ if (colonIdx === -1) continue
35
+ const id = header.substring(0, colonIdx).trim()
36
+ if (!id || !/^L\d{3}$/.test(id)) continue
37
+ const tag = header.substring(colonIdx + 2).trim()
38
+ if (!tag) continue
39
+
40
+ let category = ""
41
+ let problem = ""
42
+ let solution = ""
43
+ let date = ""
44
+
45
+ for (const line of lines) {
46
+ const t = line.trim()
47
+ if (t.startsWith("- **Tags**:")) {
48
+ const m = t.match(/#(\w+)/)
49
+ if (m) category = m[1]
50
+ } else if (t.startsWith("- **Problema**:")) {
51
+ problem = t.substring("- **Problema**: ".length).trim()
52
+ } else if (t.startsWith("- **Solución**:")) {
53
+ solution = t.substring("- **Solución**: ".length).trim()
54
+ } else if (t.startsWith("- **Fecha**:")) {
55
+ date = t.substring("- **Fecha**: ".length).trim()
56
+ }
57
+ }
58
+
59
+ if (id && problem) {
60
+ entries.push({ id, category, tag, problem, solution, date: date || today() })
61
+ }
62
+ }
63
+ return entries
64
+ }
65
+
66
+ export function buildIndex(entries: BrainEntry[]): string {
67
+ if (entries.length === 0) return "_No hay lecciones registradas todavía._"
68
+ const header = "| ID | Categoría | Tag | Problema |\n| :--- | :--- | :--- | :--- |\n"
69
+ const rows = entries.map(e => {
70
+ const problemTrunc = e.problem.length > 55 ? e.problem.slice(0, 52) + "..." : e.problem
71
+ return `| ${e.id} | ${e.category || "-"} | ${e.tag} | ${problemTrunc} |`
72
+ }).join("\n")
73
+ return header + rows
74
+ }
75
+
76
+ export function buildEntryBlock(e: BrainEntry): string {
77
+ const tags = `#${e.category || "general"} #${e.tag.replace(/[-\s]/g, "_")}`
78
+ return [
79
+ `### ${e.id}: ${e.tag}`,
80
+ `- **Tags**: ${tags}`,
81
+ `- **Problema**: ${e.problem}`,
82
+ `- **Solución**: ${e.solution}`,
83
+ `- **Fecha**: ${e.date}`,
84
+ ].join("\n") + "\n"
85
+ }
86
+
87
+ export function buildFullBrain(entries: BrainEntry[]): string {
88
+ const lines: string[] = [
89
+ "# 🧠 Cerebro del Proyecto",
90
+ "",
91
+ "> Base de conocimiento técnico a largo plazo. Solo registra aprendizajes de alto valor y no triviales.",
92
+ "",
93
+ "## Índice",
94
+ "",
95
+ buildIndex(entries),
96
+ "",
97
+ "## Lecciones",
98
+ "",
99
+ ]
100
+ for (const e of entries) {
101
+ lines.push(buildEntryBlock(e))
102
+ }
103
+ return lines.join("\n")
104
+ }
105
+
106
+ export function readBrainFile(brainPath: string): { entries: BrainEntry[] } {
107
+ if (!fs.existsSync(brainPath)) {
108
+ return { entries: [] }
109
+ }
110
+ const content = fs.readFileSync(brainPath, "utf-8")
111
+ const leccionesIdx = content.indexOf("## Lecciones")
112
+ const leccionesContent = leccionesIdx >= 0
113
+ ? content.substring(leccionesIdx)
114
+ : content
115
+ const entries = parseEntries(leccionesContent)
116
+ return { entries }
117
+ }
118
+
119
+ export function writeBrainFile(brainPath: string, entries: BrainEntry[]) {
120
+ fs.mkdirSync(path.dirname(brainPath), { recursive: true })
121
+ fs.writeFileSync(brainPath, buildFullBrain(entries), "utf-8")
122
+ }
@@ -0,0 +1,134 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+
3
+ export default tool({
4
+ description: "Audita programáticamente si una dependencia de terceros cumple con la regla de estabilidad y seguridad de cooldown (mínimo 3 días / 4320 minutos de antigüedad). Consulta APIs reales de NPM y PyPI.",
5
+ args: {
6
+ package: tool.schema.string().describe("Nombre del paquete o dependencia (ej: 'lodash', 'fastapi')"),
7
+ version: tool.schema.string().optional().describe("Versión del paquete a auditar. Si no se especifica, audita la última versión."),
8
+ ecosystem: tool.schema.enum(["npm", "pypi"]).optional().describe("El ecosistema del paquete. Por defecto se infiere del stack.")
9
+ },
10
+ async execute(args) {
11
+ const pkg = args.package;
12
+ let targetVersion = args.version;
13
+ let eco = args.ecosystem;
14
+
15
+ // Inferir ecosistema si no está presente
16
+ if (!eco) {
17
+ if (pkg.startsWith("@") || pkg.includes("/") || pkg === "lodash" || pkg === "axios" || pkg === "express") {
18
+ eco = "npm";
19
+ } else {
20
+ // Enfoque por defecto: intentar NPM primero, luego PyPI
21
+ eco = "npm";
22
+ }
23
+ }
24
+
25
+ try {
26
+ if (eco === "npm") {
27
+ const url = `https://registry.npmjs.org/${encodeURIComponent(pkg)}`;
28
+ const res = await fetch(url);
29
+ if (!res.ok) {
30
+ // Si falla NPM, chequear si es PyPI
31
+ if (!args.ecosystem) {
32
+ return await checkPyPI(pkg, targetVersion);
33
+ }
34
+ return `[Cooldown Blocked] Error: No se encontró el paquete '${pkg}' en NPM.`;
35
+ }
36
+
37
+ const data: any = await res.json();
38
+
39
+ // Obtener última versión si no se provee
40
+ if (!targetVersion) {
41
+ targetVersion = data["dist-tags"]?.latest;
42
+ }
43
+
44
+ if (!targetVersion) {
45
+ return `[Cooldown Blocked] Error: No se pudo resolver la versión para el paquete '${pkg}' en NPM.`;
46
+ }
47
+
48
+ const publishTimeString = data.time?.[targetVersion];
49
+ if (!publishTimeString) {
50
+ return `[Cooldown Blocked] Error: No se encontró la versión '${targetVersion}' para '${pkg}' en NPM.`;
51
+ }
52
+
53
+ return evaluateCooldown(pkg, targetVersion, publishTimeString, "NPM");
54
+ } else {
55
+ return await checkPyPI(pkg, targetVersion);
56
+ }
57
+ } catch (e: any) {
58
+ return `[Cooldown Warning] Error al consultar la API de dependencias: ${e.message || e}. Por seguridad, verifica manualmente.`;
59
+ }
60
+ }
61
+ })
62
+
63
+ async function checkPyPI(pkg: string, targetVersion?: string) {
64
+ try {
65
+ const url = `https://pypi.org/pypi/${encodeURIComponent(pkg)}/json`;
66
+ const res = await fetch(url);
67
+ if (!res.ok) {
68
+ return `[Cooldown Blocked] Error: No se encontró el paquete '${pkg}' en NPM ni PyPI.`;
69
+ }
70
+
71
+ const data: any = await res.json();
72
+
73
+ // Resolver versión
74
+ const version = targetVersion || data.info?.version;
75
+ if (!version) {
76
+ return `[Cooldown Blocked] Error: No se pudo resolver la versión para '${pkg}' en PyPI.`;
77
+ }
78
+
79
+ const releases = data.releases?.[version];
80
+ if (!releases || releases.length === 0) {
81
+ return `[Cooldown Blocked] Error: No se encontró la versión '${version}' para '${pkg}' en PyPI.`;
82
+ }
83
+
84
+ // PyPI expone fecha de subida del primer archivo
85
+ const uploadTime = releases[0]?.upload_time_iso_8601 || releases[0]?.upload_time;
86
+ if (!uploadTime) {
87
+ return `[Cooldown Blocked] Error: No se encontró fecha de publicación para '${pkg}@${version}' en PyPI.`;
88
+ }
89
+
90
+ return evaluateCooldown(pkg, version, uploadTime, "PyPI");
91
+ } catch (e: any) {
92
+ return `[Cooldown Blocked] Error de conexión con PyPI: ${e.message || e}`;
93
+ }
94
+ }
95
+
96
+ function evaluateCooldown(pkg: string, version: string, publishTimeStr: string, registryName: string) {
97
+ const publishTime = new Date(publishTimeStr);
98
+ const now = new Date();
99
+ const diffMs = now.getTime() - publishTime.getTime();
100
+ const diffMinutes = Math.floor(diffMs / (1000 * 60));
101
+ const cooldownRequired = 4320; // 3 días en minutos
102
+
103
+ const publishDateStr = publishTime.toISOString().split('T')[0];
104
+
105
+ if (diffMinutes < cooldownRequired) {
106
+ const remainingMinutes = cooldownRequired - diffMinutes;
107
+ const remainingHours = Math.ceil(remainingMinutes / 60);
108
+ const remainingDays = (remainingMinutes / 1440).toFixed(1);
109
+
110
+ return JSON.stringify({
111
+ status: "BLOCKED",
112
+ package: pkg,
113
+ version: version,
114
+ registry: registryName,
115
+ publishDate: publishDateStr,
116
+ ageMinutes: diffMinutes,
117
+ ageDays: (diffMinutes / 1440).toFixed(1),
118
+ cooldownMinutesRequired: cooldownRequired,
119
+ message: `❌ COOLDOWN BLOQUEADO: El paquete '${pkg}@${version}' fue publicado hace solo ${(diffMinutes / 1440).toFixed(1)} días (${diffMinutes} minutos) el ${publishDateStr}. Requiere superar los 3 días de estabilidad. Faltan aproximadamente ${remainingDays} días (${remainingHours} horas) para que expire el cooldown.`
120
+ }, null, 2);
121
+ } else {
122
+ return JSON.stringify({
123
+ status: "APPROVED",
124
+ package: pkg,
125
+ version: version,
126
+ registry: registryName,
127
+ publishDate: publishDateStr,
128
+ ageMinutes: diffMinutes,
129
+ ageDays: (diffMinutes / 1440).toFixed(1),
130
+ cooldownMinutesRequired: cooldownRequired,
131
+ message: `✅ COOLDOWN APROBADO: El paquete '${pkg}@${version}' fue publicado el ${publishDateStr} (hace ${(diffMinutes / 1440).toFixed(1)} días). Cumple plenamente con la regla de estabilidad mínima de 3 días.`
132
+ }, null, 2);
133
+ }
134
+ }
package/tools/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { default as sdd_transition } from './sdd_transition.js';
2
+ export { default as sdd_archive_and_commit } from './sdd_archive_and_commit.js';
3
+ export { default as sdd_brain_sync } from './sdd_brain_sync.js';
4
+ export { default as sdd_checkpoint } from './sdd_checkpoint.js';
5
+ export { default as sdd_ui_auditor } from './sdd_ui_auditor.js';
6
+ export { default as sdd_install_autoskills } from './sdd_install_autoskills.js';
7
+ export { default as sdd_generate_tree } from './sdd_generate_tree.js';
8
+ export { default as sdd_spec_validator } from './sdd_spec_validator.js';
9
+ export { default as sdd_regression_detector } from './sdd_regression_detector.js';
10
+ export { default as sdd_secret_scanner } from './sdd_secret_scanner.js';
11
+ export { default as sdd_requirement_tracker } from './sdd_requirement_tracker.js';
12
+ export { default as sdd_bdd_tester } from './sdd_bdd_tester.js';
13
+ export { default as sdd_compact_context } from './sdd_compact_context.js';
14
+ export { default as check_dependency_cooldown } from './check_dependency_cooldown.js';