openprompt-lang 0.3.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.
- package/LICENSE +21 -0
- package/README.md +663 -0
- package/bin/cli.js +110 -0
- package/bin/lint.js +50 -0
- package/docs/COMMANDS.md +229 -0
- package/docs/COMMITS/INDEX.md +11 -0
- package/docs/COMMITS/v0.1.0-existing.md +31 -0
- package/docs/COMMITS/v0.1.0-inicial.md +50 -0
- package/docs/COMMITS/v0.1.0-readme.md +24 -0
- package/docs/COMMITS/v0.2.0-strict-db-templates.md +50 -0
- package/docs/COMMITS/v0.3.0-parser-fixes-vscode.md +67 -0
- package/docs/COMMITS/v0.3.0-versioning-component.md +44 -0
- package/docs/DEPENDENCIES.md +45 -0
- package/docs/FRAMEWORK.md +1741 -0
- package/docs/SYNTAX.md +359 -0
- package/docs/VERSIONING.md +150 -0
- package/docs/referencia-metodologia/Anexos Finales Documentos de Respaldo y Estandarizaci/303/263n.md" +90 -0
- package/docs/referencia-metodologia/Cotizaciones.md +84 -0
- package/docs/referencia-metodologia/Example.md +1 -0
- package/docs/referencia-metodologia/ExtractorInformacion.py +78 -0
- package/docs/referencia-metodologia/Fase - 1 .- Desarrollo de la Metodolog/303/255a.md" +67 -0
- package/docs/referencia-metodologia/Fase - 2 .- Levantamiento de requisitos generales y traduccion a la IA.md +64 -0
- package/docs/referencia-metodologia/Fase - 3 .- Prototipado visual con IA (Figma Maker o equivalentes).md +64 -0
- package/docs/referencia-metodologia/Fase - 4 .- Especificacion de requisitos e iteracion con el cliente.md +58 -0
- package/docs/referencia-metodologia/Fase - 5 .- Estructuracion y maquetado de funciones (Scaffolding).md +118 -0
- package/docs/referencia-metodologia/Fase - 6 .- Estructuracion del backlog y division de tareas.md +48 -0
- package/docs/referencia-metodologia/Fase - 7 .- Desarrollo activo, pruebas y control de versiones.md +98 -0
- package/docs/referencia-metodologia/Fase - 8 .- Entrega, capacitaci/303/263n y mantenimiento.md" +55 -0
- package/docs/referencia-metodologia/Figma prompt template.md +130 -0
- package/docs/referencia-metodologia/Framework de Desarrollo Asistido por IA.md +1741 -0
- package/docs/referencia-metodologia/Indice General.md +83 -0
- package/docs/referencia-metodologia/Prompt refactorizar o creacion desde cero.md +50 -0
- package/docs/referencia-metodologia/docs/CONVENCIONES_DB.md +410 -0
- package/docs/referencia-metodologia/docs/CONVENCIONES_DOCUMENTACION.md +209 -0
- package/docs/referencia-metodologia/docs/PROMPTS/INDEX.md +73 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/01-hook-supabase.md +79 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/02-componente-ui.md +82 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/03-pagina-feature.md +70 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/04-comando-tauri.md +56 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/05-store-zustand.md +74 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/06-servicio-supabase.md +74 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/07-formulario-validacion.md +63 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/08-hook-capacitor.md +65 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/09-refactor-division.md +51 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/10-scaffolding-inicial.md +79 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/11-supabase-crud-service.md +114 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/12-supabase-hook-usetable.md +143 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/13-tauri-command-rust.md +84 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/14-tauri-wrapper-typescript.md +92 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/15-documentar-tabla-db.md +50 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/16-diagrama-arquitectura.md +60 -0
- package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/17-documentar-api-rpc.md +56 -0
- package/docs/referencia-metodologia/docs/PROMPTS/STACK/ionic-capacitor.md +52 -0
- package/docs/referencia-metodologia/docs/PROMPTS/STACK/react-web-puro.md +46 -0
- package/docs/referencia-metodologia/docs/PROMPTS/STACK/tauri-desktop.md +53 -0
- package/package.json +56 -0
- package/schemas/prompt-lang.json +98 -0
- package/src/commands/component.js +326 -0
- package/src/commands/context.js +206 -0
- package/src/commands/figma.js +63 -0
- package/src/commands/init.js +373 -0
- package/src/commands/suggest.js +31 -0
- package/src/commands/validate.js +183 -0
- package/src/generators/figma-prompt.js +56 -0
- package/src/utils/ai.js +143 -0
- package/src/utils/annotations.js +510 -0
- package/src/utils/config.js +60 -0
- package/vscode-extension/README.md +31 -0
- package/vscode-extension/language-configuration.json +7 -0
- package/vscode-extension/package.json +62 -0
- package/vscode-extension/snippets/promptlang.json +105 -0
- package/vscode-extension/syntaxes/annotations.tmGrammar.json +39 -0
- package/vscode-extension/syntaxes/promptlang.tmGrammar.json +14 -0
package/src/utils/ai.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openPrompt-Lang AI module
|
|
3
|
+
*
|
|
4
|
+
* Proporciona integración con servicios de IA (OpenAI, opencode, etc.)
|
|
5
|
+
* para sugerencias automáticas de stack, estructura y scaffolding.
|
|
6
|
+
*
|
|
7
|
+
* Si no hay API key ni opencode, genera un plan detallado para que
|
|
8
|
+
* la IA externa lo ejecute.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
|
|
13
|
+
export async function suggestStack(description) {
|
|
14
|
+
try {
|
|
15
|
+
// Intenta usar opencode si está disponible
|
|
16
|
+
const result = await tryOpencode(description);
|
|
17
|
+
if (result) return result;
|
|
18
|
+
} catch {
|
|
19
|
+
// fallback
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Intenta usar OpenAI si hay API key
|
|
24
|
+
const result = await tryOpenAI(description);
|
|
25
|
+
if (result) return result;
|
|
26
|
+
} catch {
|
|
27
|
+
// fallback
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Fallback: generar plan para IA externa
|
|
31
|
+
return generateFallbackPlan(description);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function tryOpencode(description) {
|
|
35
|
+
try {
|
|
36
|
+
const { execSync } = await import("child_process");
|
|
37
|
+
const prompt = `Eres un arquitecto de software. Analiza esta descripción de proyecto y sugiere:
|
|
38
|
+
1. Stack tecnológico (React/Vue/Angular, base de datos, hosting)
|
|
39
|
+
2. Estructura de carpetas inicial
|
|
40
|
+
3. Módulos principales
|
|
41
|
+
4. Prioridades de desarrollo
|
|
42
|
+
|
|
43
|
+
Descripción: ${description}
|
|
44
|
+
|
|
45
|
+
Formato de respuesta: JSON con keys: stack, folders, modules, priorities`;
|
|
46
|
+
|
|
47
|
+
const result = execSync(`opencode prompt "${prompt}"`, {
|
|
48
|
+
encoding: "utf-8",
|
|
49
|
+
timeout: 30000,
|
|
50
|
+
}).trim();
|
|
51
|
+
|
|
52
|
+
return JSON.parse(result);
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function tryOpenAI(description) {
|
|
59
|
+
const apiKey = process.env.OPENAI_API_KEY || process.env.OPENPROMPT_API_KEY;
|
|
60
|
+
if (!apiKey) return null;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
Authorization: `Bearer ${apiKey}`,
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
model: "gpt-4",
|
|
71
|
+
messages: [
|
|
72
|
+
{
|
|
73
|
+
role: "system",
|
|
74
|
+
content:
|
|
75
|
+
"Eres un arquitecto de software. Responde siempre en JSON con keys: stack, folders, modules, priorities",
|
|
76
|
+
},
|
|
77
|
+
{ role: "user", content: `Sugiere stack y estructura para: ${description}` },
|
|
78
|
+
],
|
|
79
|
+
temperature: 0.3,
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const data = await response.json();
|
|
84
|
+
const content = data.choices?.[0]?.message?.content;
|
|
85
|
+
if (content) {
|
|
86
|
+
return JSON.parse(content.replace(/```json/g, "").replace(/```/g, "").trim());
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// fallback
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function generateFallbackPlan(description) {
|
|
96
|
+
const plan = `# INIT_PLAN.md — Plan de Inicialización para IA
|
|
97
|
+
|
|
98
|
+
## Proyecto
|
|
99
|
+
${description}
|
|
100
|
+
|
|
101
|
+
## Instrucciones para la IA
|
|
102
|
+
|
|
103
|
+
Eres un arquitecto de software. Basado en la descripción anterior, genera:
|
|
104
|
+
|
|
105
|
+
### 1. Stack Tecnológico Sugerido
|
|
106
|
+
- Frontend: React 18 + TypeScript + Vite + Tailwind CSS
|
|
107
|
+
- Backend/DB: Supabase (recomendado) / Firebase / PostgreSQL
|
|
108
|
+
- Estado: Zustand
|
|
109
|
+
- Ruteo: React Router v6
|
|
110
|
+
- Testing: Vitest + Testing Library
|
|
111
|
+
|
|
112
|
+
### 2. Estructura de Carpetas Inicial
|
|
113
|
+
Sigue el estándar definido en \`docs/patrones de trabajo.md\`:
|
|
114
|
+
- \`src/components/ui/\` — Componentes atómicos
|
|
115
|
+
- \`src/features/\` — Módulos por dominio
|
|
116
|
+
- \`src/hooks/\` — Custom hooks globales
|
|
117
|
+
- \`src/services/supabase/\` — Clientes de API
|
|
118
|
+
- \`src/store/\` — Estado global
|
|
119
|
+
- \`docs/COMMITS/\` — Logs de commits
|
|
120
|
+
- \`docs/LOGS/\` — Logs de errores/actividad
|
|
121
|
+
- \`docs/BACKLOG/\` — Backlog por fases
|
|
122
|
+
|
|
123
|
+
### 3. Módulos Principales
|
|
124
|
+
Identifica 3-5 módulos principales basados en la descripción.
|
|
125
|
+
Para cada uno: nombre, funcionalidades core, dependencias.
|
|
126
|
+
|
|
127
|
+
### 4. Prioridades de Desarrollo (PHASE-1)
|
|
128
|
+
Lista las tareas P0 en orden:
|
|
129
|
+
1. Setup del proyecto
|
|
130
|
+
2. Autenticación
|
|
131
|
+
3. Módulo core #1
|
|
132
|
+
4. Módulo core #2
|
|
133
|
+
5. Despliegue inicial
|
|
134
|
+
|
|
135
|
+
## Notas
|
|
136
|
+
- Usa anotaciones PromptLang (@kind, @contract, @limit) en cada archivo
|
|
137
|
+
- Sigue los límites: hook ≤80 líneas, componente ≤120, página ≤200, servicio ≤150
|
|
138
|
+
- 100% Tailwind utility classes
|
|
139
|
+
- Cada componente debe manejar estados: loading, empty, error, success
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
return { plan, source: "fallback" };
|
|
143
|
+
}
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
// ─── Parser ────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export function parseAnnotations(code) {
|
|
6
|
+
const annotations = [];
|
|
7
|
+
const lines = code.split("\n");
|
|
8
|
+
const errors = [];
|
|
9
|
+
|
|
10
|
+
let currentBlock = null;
|
|
11
|
+
let inBlock = false;
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < lines.length; i++) {
|
|
14
|
+
const line = lines[i];
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
|
|
17
|
+
// Detecta bloque de anotaciones /* ... */
|
|
18
|
+
if (trimmed.startsWith("/*")) {
|
|
19
|
+
inBlock = true;
|
|
20
|
+
currentBlock = "";
|
|
21
|
+
const cleaned = trimmed.replace(/^\/\*+\s*/, "").replace(/\s*\*+\/$/, "").trim();
|
|
22
|
+
// Solo procesamos si hay contenido (si la línea era solo /*, esperamos a la siguiente)
|
|
23
|
+
if (cleaned) {
|
|
24
|
+
currentBlock += cleaned + " ";
|
|
25
|
+
}
|
|
26
|
+
if (trimmed.includes("*/")) {
|
|
27
|
+
inBlock = false;
|
|
28
|
+
if (currentBlock.trim()) {
|
|
29
|
+
annotations.push(...parseTags(currentBlock));
|
|
30
|
+
}
|
|
31
|
+
currentBlock = null;
|
|
32
|
+
}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (inBlock) {
|
|
37
|
+
if (trimmed.includes("*/")) {
|
|
38
|
+
const content = trimmed.replace(/\s*\*+\/$/, "").trim();
|
|
39
|
+
currentBlock += content + " ";
|
|
40
|
+
inBlock = false;
|
|
41
|
+
annotations.push(...parseTags(currentBlock));
|
|
42
|
+
currentBlock = null;
|
|
43
|
+
} else {
|
|
44
|
+
const content = trimmed.replace(/^\s*\*\s*/, "").trim();
|
|
45
|
+
currentBlock += content + " ";
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Detecta línea de comentario con @
|
|
51
|
+
if (trimmed.startsWith("//") && trimmed.includes("@")) {
|
|
52
|
+
const tagLine = trimmed.replace(/^\/\/\s*/, "");
|
|
53
|
+
annotations.push(...parseTags(tagLine));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Si quedó un bloque abierto sin cerrar, lo forzamos
|
|
58
|
+
if (inBlock && currentBlock) {
|
|
59
|
+
annotations.push(...parseTags(currentBlock));
|
|
60
|
+
errors.push("Comentario multinea sin cerrar. Las anotaciones se procesaron igual.");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Extraer @use primero (determina qué tags son válidos)
|
|
64
|
+
const useTag = annotations.find((a) => a.name === "use");
|
|
65
|
+
|
|
66
|
+
// Edge case: @use(*) → todos los tags son válidos
|
|
67
|
+
let validTags = [];
|
|
68
|
+
if (useTag) {
|
|
69
|
+
const starArg = useTag.args.find((a) => a.value === "*");
|
|
70
|
+
if (starArg) {
|
|
71
|
+
validTags = []; // empty = todos permitidos
|
|
72
|
+
} else {
|
|
73
|
+
validTags = useTag.args.map((a) => a.value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Edge case: @use presente pero sin tags reales
|
|
78
|
+
if (useTag && useTag.args.length === 0) {
|
|
79
|
+
errors.push("@use() está vacío — no se importaron tags");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Edge case: @use sin anotaciones de tags abajo
|
|
83
|
+
if (useTag && annotations.filter((a) => a.name !== "use").length === 0) {
|
|
84
|
+
errors.push("@use presente pero no hay anotaciones de tags en el archivo");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { annotations, validTags, parseErrors: errors };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseTags(text) {
|
|
91
|
+
const tags = [];
|
|
92
|
+
|
|
93
|
+
if (!text || text.trim().length === 0) return tags;
|
|
94
|
+
|
|
95
|
+
// Parseador manual con conteo de profundidad de paréntesis
|
|
96
|
+
let i = 0;
|
|
97
|
+
while (i < text.length) {
|
|
98
|
+
// Buscar @ seguido de nombre de tag
|
|
99
|
+
const atIndex = text.indexOf("@", i);
|
|
100
|
+
if (atIndex === -1) break;
|
|
101
|
+
|
|
102
|
+
// Extraer nombre del tag (letras, números, guiones)
|
|
103
|
+
const nameMatch = text.slice(atIndex + 1).match(/^\w+/);
|
|
104
|
+
if (!nameMatch) { i = atIndex + 1; continue; }
|
|
105
|
+
|
|
106
|
+
const name = nameMatch[0];
|
|
107
|
+
let argsRaw = "";
|
|
108
|
+
let parenDepth = 0;
|
|
109
|
+
let pos = atIndex + 1 + name.length;
|
|
110
|
+
|
|
111
|
+
// Si hay paréntesis, extraemos con depth tracking
|
|
112
|
+
if (pos < text.length && text[pos] === "(") {
|
|
113
|
+
parenDepth = 1;
|
|
114
|
+
pos++;
|
|
115
|
+
const start = pos;
|
|
116
|
+
while (pos < text.length && parenDepth > 0) {
|
|
117
|
+
if (text[pos] === "(") parenDepth++;
|
|
118
|
+
else if (text[pos] === ")") parenDepth--;
|
|
119
|
+
pos++;
|
|
120
|
+
}
|
|
121
|
+
argsRaw = text.slice(start, pos - 1).trim();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
tags.push({ name, args: parseArgs(argsRaw), raw: `@${name}(${argsRaw})` });
|
|
125
|
+
i = pos;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return tags;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseArgs(raw) {
|
|
132
|
+
const args = [];
|
|
133
|
+
|
|
134
|
+
if (!raw || raw.trim().length === 0) return args;
|
|
135
|
+
|
|
136
|
+
const trimmed = raw.trim();
|
|
137
|
+
const lenient = trimmed.replace(/\)$/, "");
|
|
138
|
+
|
|
139
|
+
// Intenta parsear como JSON object primero: { key: value, ... }
|
|
140
|
+
if (trimmed.startsWith("{")) {
|
|
141
|
+
try {
|
|
142
|
+
const obj = JSON.parse(trimmed);
|
|
143
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
144
|
+
args.push({ key, value: String(value), type: "kv" });
|
|
145
|
+
}
|
|
146
|
+
return args;
|
|
147
|
+
} catch {
|
|
148
|
+
// Fallback: parsear como TypeScript object { key: type, key?: type }
|
|
149
|
+
const tsProps = parseTsObject(trimmed);
|
|
150
|
+
if (tsProps.length > 0) {
|
|
151
|
+
args.push(...tsProps);
|
|
152
|
+
return args;
|
|
153
|
+
}
|
|
154
|
+
// Edge case: JSON inválido → error reportado en validación
|
|
155
|
+
args.push({ key: null, value: trimmed, type: "invalid-json" });
|
|
156
|
+
return args;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Detectar mezcla de subtags, flags y key:values
|
|
161
|
+
// Dividir por coma respetando corchetes
|
|
162
|
+
const parts = splitRespectingBrackets(lenient);
|
|
163
|
+
|
|
164
|
+
for (const part of parts) {
|
|
165
|
+
const p = part.trim();
|
|
166
|
+
if (!p) continue;
|
|
167
|
+
|
|
168
|
+
// Subtag: @algo: [lista]
|
|
169
|
+
const subMatch = p.match(/^@(\w+)\s*:\s*\[([^\]]*)\]/);
|
|
170
|
+
if (subMatch) {
|
|
171
|
+
args.push({
|
|
172
|
+
key: subMatch[1],
|
|
173
|
+
value: subMatch[2].split(",").map((s) => s.trim().replace(/["']/g, "")),
|
|
174
|
+
type: "subtag",
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Flag: @algo
|
|
180
|
+
const flagMatch = p.match(/^@(\w+)/);
|
|
181
|
+
if (flagMatch) {
|
|
182
|
+
args.push({ key: flagMatch[1], value: true, type: "flag" });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Key: value
|
|
187
|
+
const kvMatch = p.match(/^(\w+)\s*:\s*(.+)/);
|
|
188
|
+
if (kvMatch) {
|
|
189
|
+
args.push({ key: kvMatch[1].trim(), value: kvMatch[2].trim().replace(/["']/g, ""), type: "kv" });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Valor plano
|
|
194
|
+
args.push({ key: null, value: p.replace(/["']/g, ""), type: "plain" });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return args;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseTsObject(raw) {
|
|
201
|
+
// Parsear { key: type, key?: type, "key": "type" }
|
|
202
|
+
const inner = raw.trim().replace(/^{/, "").replace(/}$/, "").trim();
|
|
203
|
+
if (!inner) return [];
|
|
204
|
+
const parts = splitRespectingBrackets(inner);
|
|
205
|
+
const result = [];
|
|
206
|
+
for (const part of parts) {
|
|
207
|
+
const p = part.trim();
|
|
208
|
+
if (!p) continue;
|
|
209
|
+
// key: type o key?: type o "key": "type"
|
|
210
|
+
const m = p.match(/^"([^"]+)"\s*:\s*(.+)/) || p.match(/^(\w+\??)\s*:\s*(.+)/);
|
|
211
|
+
if (m) {
|
|
212
|
+
const key = m[1].replace(/\?$/, "?");
|
|
213
|
+
let value = m[2].trim().replace(/["']/g, "");
|
|
214
|
+
// Quitar default values: = "Default" → ignorar
|
|
215
|
+
value = value.replace(/\s*=\s*.+$/, "").trim();
|
|
216
|
+
result.push({ key, value, type: "kv" });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function splitRespectingBrackets(str) {
|
|
223
|
+
const result = [];
|
|
224
|
+
let current = "";
|
|
225
|
+
let depth = 0;
|
|
226
|
+
|
|
227
|
+
for (const ch of str) {
|
|
228
|
+
if (ch === "[" || ch === "{" || ch === "(") {
|
|
229
|
+
depth++;
|
|
230
|
+
current += ch;
|
|
231
|
+
} else if (ch === "]" || ch === "}" || ch === ")") {
|
|
232
|
+
depth--;
|
|
233
|
+
current += ch;
|
|
234
|
+
} else if (ch === "," && depth === 0) {
|
|
235
|
+
result.push(current.trim());
|
|
236
|
+
current = "";
|
|
237
|
+
} else {
|
|
238
|
+
current += ch;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (current.trim()) result.push(current.trim());
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Validador ──────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
export function validateAnnotations(annotations, validTags, options = {}) {
|
|
248
|
+
const errors = [];
|
|
249
|
+
const warnings = [];
|
|
250
|
+
const tags = annotations.filter((a) => a.name !== "use");
|
|
251
|
+
|
|
252
|
+
// Validar tags contra @use
|
|
253
|
+
const useAll = validTags.length === 0 && annotations.some(
|
|
254
|
+
(a) => a.name === "use" && a.args.some((x) => x.value === "*")
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (validTags.length > 0 && !useAll) {
|
|
258
|
+
for (const tag of tags) {
|
|
259
|
+
if (!validTags.includes(tag.name)) {
|
|
260
|
+
errors.push(`Tag @${tag.name} no está importado. Agrega @use(${tag.name})`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Validar @kind
|
|
266
|
+
const kindTag = tags.find((t) => t.name === "kind");
|
|
267
|
+
const validKinds = [
|
|
268
|
+
"hook", "component", "page", "service", "store",
|
|
269
|
+
"util", "type", "layout", "feature",
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
if (kindTag) {
|
|
273
|
+
const kindValue = kindTag.args[0]?.value;
|
|
274
|
+
|
|
275
|
+
// Edge case: @kind sin valor
|
|
276
|
+
if (!kindValue || kindValue.trim() === "") {
|
|
277
|
+
errors.push("@kind() necesita un valor. Válidos: " + validKinds.join(", "));
|
|
278
|
+
} else if (!validKinds.includes(kindValue)) {
|
|
279
|
+
errors.push(
|
|
280
|
+
`@kind("${kindValue}") inválido. Valores válidos: ${validKinds.join(", ")}`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (kindValue && validKinds.includes(kindValue)) {
|
|
285
|
+
const hasContract = tags.some((t) => t.name === "contract");
|
|
286
|
+
const hasProps = tags.some((t) => t.name === "props");
|
|
287
|
+
const hasLimit = tags.some((t) => t.name === "limit");
|
|
288
|
+
const hasCompose = tags.some((t) => t.name === "compose");
|
|
289
|
+
|
|
290
|
+
if (kindValue === "hook" && !hasContract) {
|
|
291
|
+
warnings.push("@kind(hook) debería tener @contract");
|
|
292
|
+
}
|
|
293
|
+
if (kindValue === "component" && hasContract) {
|
|
294
|
+
errors.push("@kind(component) no puede tener @contract. Usa @props en su lugar.");
|
|
295
|
+
}
|
|
296
|
+
if (kindValue === "component" && !hasProps) {
|
|
297
|
+
warnings.push("@kind(component) debería tener @props");
|
|
298
|
+
}
|
|
299
|
+
if (kindValue === "page" && !hasCompose) {
|
|
300
|
+
warnings.push("@kind(page) debería tener @compose (debe componer subcomponentes)");
|
|
301
|
+
}
|
|
302
|
+
if (kindValue === "service" && !hasContract) {
|
|
303
|
+
errors.push("@kind(service) requiere @contract");
|
|
304
|
+
}
|
|
305
|
+
if (kindValue === "hook" && hasProps) {
|
|
306
|
+
errors.push("@kind(hook) no puede tener @props");
|
|
307
|
+
}
|
|
308
|
+
if (kindValue === "type" && hasLimit) {
|
|
309
|
+
warnings.push("@kind(type) no necesita @limit");
|
|
310
|
+
}
|
|
311
|
+
if (kindValue === "store") {
|
|
312
|
+
const hasDeps = tags.some((t) => t.name === "deps");
|
|
313
|
+
if (!hasDeps) {
|
|
314
|
+
warnings.push("@kind(store) debería tener @deps (zustand, context, etc.)");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Límites por defecto según kind
|
|
319
|
+
const limitTag = tags.find((t) => t.name === "limit");
|
|
320
|
+
if (limitTag) {
|
|
321
|
+
const linesArg = limitTag.args.find((a) => a.key === "lines");
|
|
322
|
+
if (linesArg) {
|
|
323
|
+
const maxLines = {
|
|
324
|
+
hook: 80, component: 120, page: 200,
|
|
325
|
+
service: 150, store: 100, layout: 150,
|
|
326
|
+
};
|
|
327
|
+
const expected = maxLines[kindValue];
|
|
328
|
+
const numVal = parseInt(linesArg.value);
|
|
329
|
+
if (expected && !isNaN(numVal) && numVal > expected) {
|
|
330
|
+
warnings.push(
|
|
331
|
+
`@kind(${kindValue}) @limit(lines) debería ser ≤ ${expected} (actual: ${linesArg.value})`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
// Edge case: límite 0 o negativo
|
|
335
|
+
if (!isNaN(numVal) && numVal <= 0) {
|
|
336
|
+
errors.push(`@limit(lines: ${numVal}) debe ser mayor a 0`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Validar @contract — edge cases
|
|
344
|
+
const contractTag = tags.find((t) => t.name === "contract");
|
|
345
|
+
if (contractTag) {
|
|
346
|
+
const args = contractTag.args;
|
|
347
|
+
const hasInput = args.some((a) => a.key === "in" || a.type === "plain");
|
|
348
|
+
const hasOutput = args.find((a) => a.key === "out" || a.value === "->");
|
|
349
|
+
const contractText = contractTag.raw;
|
|
350
|
+
|
|
351
|
+
// Edge case: contrato vacío
|
|
352
|
+
if (args.length === 0 && contractTag.raw === "@contract()") {
|
|
353
|
+
errors.push("@contract() está vacío. Formato: @contract(in: tipo -> out: tipo @error: tipo)");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Edge case: sin input claro
|
|
357
|
+
if (args.length > 0 && !hasInput && !contractText.includes("in:")) {
|
|
358
|
+
// Puede ser formato @contract(in: -> out:) sin inputs
|
|
359
|
+
if (contractText.includes("->")) {
|
|
360
|
+
// tiene flecha pero sin "in:" → intentamos extraer
|
|
361
|
+
const parts = contractText.split("->");
|
|
362
|
+
const inputPart = parts[0].replace("@contract(", "").replace("@contract", "").trim();
|
|
363
|
+
if (!inputPart || inputPart === "in: " || inputPart === "in:") {
|
|
364
|
+
errors.push("@contract: falta definir tipos de entrada (in:)");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Edge case: @contract(in: any -> out: any)
|
|
370
|
+
if (contractTag.raw.includes("any")) {
|
|
371
|
+
warnings.push("@contract usa 'any' — especifica tipos concretos");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Validar @props — edge case JSON inválido
|
|
376
|
+
const propsTag = tags.find((t) => t.name === "props");
|
|
377
|
+
if (propsTag) {
|
|
378
|
+
const hasInvalidJson = propsTag.args.some((a) => a.type === "invalid-json");
|
|
379
|
+
if (hasInvalidJson) {
|
|
380
|
+
errors.push(`@props(${propsTag.args[0]?.value}) no es un JSON válido. Formato: @props({ key: type })`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Validar @platform — edge case web + capacitor
|
|
385
|
+
const platformTag = tags.find((t) => t.name === "platform");
|
|
386
|
+
const depsTag = tags.find((t) => t.name === "deps");
|
|
387
|
+
if (platformTag && depsTag) {
|
|
388
|
+
const platforms = platformTag.args.map((a) => a.value);
|
|
389
|
+
const deps = depsTag.args;
|
|
390
|
+
|
|
391
|
+
if (platforms.includes("web")) {
|
|
392
|
+
const hasCapacitor = deps.some(
|
|
393
|
+
(d) => d.value && (String(d.value).includes("@capacitor"))
|
|
394
|
+
);
|
|
395
|
+
if (hasCapacitor) {
|
|
396
|
+
errors.push("@platform(web) no es compatible con @capacitor/*");
|
|
397
|
+
}
|
|
398
|
+
const hasTauri = deps.some(
|
|
399
|
+
(d) => d.value && String(d.value).includes("@tauri-apps")
|
|
400
|
+
);
|
|
401
|
+
if (hasTauri) {
|
|
402
|
+
errors.push("@platform(web) no es compatible con @tauri-apps/*");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (platforms.includes("mobile")) {
|
|
407
|
+
const hasTauri = deps.some(
|
|
408
|
+
(d) => d.value && String(d.value).includes("@tauri-apps")
|
|
409
|
+
);
|
|
410
|
+
if (hasTauri) {
|
|
411
|
+
errors.push("@platform(mobile) no es compatible con @tauri-apps/*");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (platforms.includes("desktop")) {
|
|
415
|
+
const hasCapacitor = deps.some(
|
|
416
|
+
(d) => d.value && String(d.value).includes("@capacitor")
|
|
417
|
+
);
|
|
418
|
+
if (hasCapacitor) {
|
|
419
|
+
errors.push("@platform(desktop) no es compatible con @capacitor/*");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Validar @limit — edge cases de valores
|
|
425
|
+
const limitTag = tags.find((t) => t.name === "limit");
|
|
426
|
+
if (limitTag) {
|
|
427
|
+
for (const arg of limitTag.args) {
|
|
428
|
+
if (arg.type === "kv" && arg.value !== undefined) {
|
|
429
|
+
const num = parseInt(arg.value);
|
|
430
|
+
if (isNaN(num) || (arg.value.includes(".") && isNaN(parseFloat(arg.value)))) {
|
|
431
|
+
errors.push(`@limit(${arg.key}: "${arg.value}") esperaba un número, recibió texto`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Edge case: @limit() vacío
|
|
436
|
+
if (limitTag.args.length === 0) {
|
|
437
|
+
errors.push("@limit() está vacío. Especifica al menos un límite: @limit(lines: 120)");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Validar @forbidden
|
|
442
|
+
const forbiddenTag = tags.find((t) => t.name === "forbidden");
|
|
443
|
+
if (forbiddenTag) {
|
|
444
|
+
if (forbiddenTag.args.length === 0) {
|
|
445
|
+
warnings.push("@forbidden() está vacío — no prohíbe nada");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Validar @scope breaking
|
|
450
|
+
const scopeTag = tags.find((t) => t.name === "scope");
|
|
451
|
+
if (scopeTag) {
|
|
452
|
+
const breaking = scopeTag.args.find((a) => a.key === "breaking");
|
|
453
|
+
if (breaking && breaking.value === "true") {
|
|
454
|
+
warnings.push("@scope(breaking: true) — el commit debe indicar breaking change (!)");
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Validar @test
|
|
459
|
+
const testTag = tags.find((t) => t.name === "test");
|
|
460
|
+
if (testTag) {
|
|
461
|
+
const coverage = testTag.args.find((a) => a.key === "coverage");
|
|
462
|
+
if (coverage) {
|
|
463
|
+
const val = parseInt(coverage.value);
|
|
464
|
+
if (!isNaN(val) && val > 100) {
|
|
465
|
+
errors.push(`@test(@coverage: ${val}) no puede exceder 100`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Validar @state error → debe tener @contract (solo en kinds que requieren contract)
|
|
471
|
+
const stateTag = tags.find((t) => t.name === "state");
|
|
472
|
+
if (stateTag) {
|
|
473
|
+
const hasError = stateTag.args.some((a) => a.value === "error");
|
|
474
|
+
const hasContract = tags.some((t) => t.name === "contract");
|
|
475
|
+
const kindValue = kindTag?.args[0]?.value;
|
|
476
|
+
const contractKinds = ["hook", "service"];
|
|
477
|
+
if (hasError && !hasContract && contractKinds.includes(kindValue)) {
|
|
478
|
+
warnings.push("@state(error) presente pero no hay @contract — define el tipo de error");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return { errors, warnings };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ─── API pública ────────────────────────────────────────────────────────────
|
|
486
|
+
|
|
487
|
+
export function lintFile(code, strict = false) {
|
|
488
|
+
const { annotations, validTags, parseErrors } = parseAnnotations(code);
|
|
489
|
+
|
|
490
|
+
if (annotations.length === 0) {
|
|
491
|
+
return { annotations: [], errors: parseErrors, warnings: [] };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const { errors, warnings } = validateAnnotations(annotations, validTags);
|
|
495
|
+
const allErrors = [...parseErrors, ...errors];
|
|
496
|
+
const allWarnings = [...warnings];
|
|
497
|
+
|
|
498
|
+
if (strict && allWarnings.length > 0) {
|
|
499
|
+
allErrors.push(...allWarnings.map(w => `[strict] ${w}`));
|
|
500
|
+
return { annotations, errors: allErrors, warnings: [] };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
annotations,
|
|
505
|
+
errors: allErrors,
|
|
506
|
+
warnings: allWarnings,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export { parseArgs };
|