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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +663 -0
  3. package/bin/cli.js +110 -0
  4. package/bin/lint.js +50 -0
  5. package/docs/COMMANDS.md +229 -0
  6. package/docs/COMMITS/INDEX.md +11 -0
  7. package/docs/COMMITS/v0.1.0-existing.md +31 -0
  8. package/docs/COMMITS/v0.1.0-inicial.md +50 -0
  9. package/docs/COMMITS/v0.1.0-readme.md +24 -0
  10. package/docs/COMMITS/v0.2.0-strict-db-templates.md +50 -0
  11. package/docs/COMMITS/v0.3.0-parser-fixes-vscode.md +67 -0
  12. package/docs/COMMITS/v0.3.0-versioning-component.md +44 -0
  13. package/docs/DEPENDENCIES.md +45 -0
  14. package/docs/FRAMEWORK.md +1741 -0
  15. package/docs/SYNTAX.md +359 -0
  16. package/docs/VERSIONING.md +150 -0
  17. package/docs/referencia-metodologia/Anexos Finales Documentos de Respaldo y Estandarizaci/303/263n.md" +90 -0
  18. package/docs/referencia-metodologia/Cotizaciones.md +84 -0
  19. package/docs/referencia-metodologia/Example.md +1 -0
  20. package/docs/referencia-metodologia/ExtractorInformacion.py +78 -0
  21. package/docs/referencia-metodologia/Fase - 1 .- Desarrollo de la Metodolog/303/255a.md" +67 -0
  22. package/docs/referencia-metodologia/Fase - 2 .- Levantamiento de requisitos generales y traduccion a la IA.md +64 -0
  23. package/docs/referencia-metodologia/Fase - 3 .- Prototipado visual con IA (Figma Maker o equivalentes).md +64 -0
  24. package/docs/referencia-metodologia/Fase - 4 .- Especificacion de requisitos e iteracion con el cliente.md +58 -0
  25. package/docs/referencia-metodologia/Fase - 5 .- Estructuracion y maquetado de funciones (Scaffolding).md +118 -0
  26. package/docs/referencia-metodologia/Fase - 6 .- Estructuracion del backlog y division de tareas.md +48 -0
  27. package/docs/referencia-metodologia/Fase - 7 .- Desarrollo activo, pruebas y control de versiones.md +98 -0
  28. package/docs/referencia-metodologia/Fase - 8 .- Entrega, capacitaci/303/263n y mantenimiento.md" +55 -0
  29. package/docs/referencia-metodologia/Figma prompt template.md +130 -0
  30. package/docs/referencia-metodologia/Framework de Desarrollo Asistido por IA.md +1741 -0
  31. package/docs/referencia-metodologia/Indice General.md +83 -0
  32. package/docs/referencia-metodologia/Prompt refactorizar o creacion desde cero.md +50 -0
  33. package/docs/referencia-metodologia/docs/CONVENCIONES_DB.md +410 -0
  34. package/docs/referencia-metodologia/docs/CONVENCIONES_DOCUMENTACION.md +209 -0
  35. package/docs/referencia-metodologia/docs/PROMPTS/INDEX.md +73 -0
  36. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/01-hook-supabase.md +79 -0
  37. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/02-componente-ui.md +82 -0
  38. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/03-pagina-feature.md +70 -0
  39. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/04-comando-tauri.md +56 -0
  40. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/05-store-zustand.md +74 -0
  41. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/06-servicio-supabase.md +74 -0
  42. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/07-formulario-validacion.md +63 -0
  43. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/08-hook-capacitor.md +65 -0
  44. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/09-refactor-division.md +51 -0
  45. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/10-scaffolding-inicial.md +79 -0
  46. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/11-supabase-crud-service.md +114 -0
  47. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/12-supabase-hook-usetable.md +143 -0
  48. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/13-tauri-command-rust.md +84 -0
  49. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/14-tauri-wrapper-typescript.md +92 -0
  50. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/15-documentar-tabla-db.md +50 -0
  51. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/16-diagrama-arquitectura.md +60 -0
  52. package/docs/referencia-metodologia/docs/PROMPTS/PLANTILLAS/17-documentar-api-rpc.md +56 -0
  53. package/docs/referencia-metodologia/docs/PROMPTS/STACK/ionic-capacitor.md +52 -0
  54. package/docs/referencia-metodologia/docs/PROMPTS/STACK/react-web-puro.md +46 -0
  55. package/docs/referencia-metodologia/docs/PROMPTS/STACK/tauri-desktop.md +53 -0
  56. package/package.json +56 -0
  57. package/schemas/prompt-lang.json +98 -0
  58. package/src/commands/component.js +326 -0
  59. package/src/commands/context.js +206 -0
  60. package/src/commands/figma.js +63 -0
  61. package/src/commands/init.js +373 -0
  62. package/src/commands/suggest.js +31 -0
  63. package/src/commands/validate.js +183 -0
  64. package/src/generators/figma-prompt.js +56 -0
  65. package/src/utils/ai.js +143 -0
  66. package/src/utils/annotations.js +510 -0
  67. package/src/utils/config.js +60 -0
  68. package/vscode-extension/README.md +31 -0
  69. package/vscode-extension/language-configuration.json +7 -0
  70. package/vscode-extension/package.json +62 -0
  71. package/vscode-extension/snippets/promptlang.json +105 -0
  72. package/vscode-extension/syntaxes/annotations.tmGrammar.json +39 -0
  73. package/vscode-extension/syntaxes/promptlang.tmGrammar.json +14 -0
@@ -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 };