openprompt-lang 0.3.0 → 0.4.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 (90) hide show
  1. package/README.md +483 -32
  2. package/bin/cli.js +232 -41
  3. package/bin/create.js +135 -0
  4. package/bin/lint.js +20 -21
  5. package/docs/COMMANDS.md +200 -127
  6. package/docs/COMMITS/INDEX.md +1 -0
  7. package/docs/PROMPT_AI_CONTEXT.md +99 -0
  8. package/docs/langs/dotnet.md +36 -0
  9. package/docs/langs/java-spring.md +45 -0
  10. package/docs/langs/python-fastapi.md +35 -0
  11. package/docs/langs/unity.md +30 -0
  12. package/docs/langs/vue-nuxt.md +36 -0
  13. package/package.json +31 -3
  14. package/scaffolds/.cursorrules +6 -0
  15. package/scaffolds/AGENTS.md +27 -0
  16. package/scaffolds/Dockerfile +11 -0
  17. package/scaffolds/capacitor.config.ts +17 -0
  18. package/scaffolds/netlify.toml +8 -0
  19. package/scaffolds/prompt-lang.json +15 -0
  20. package/scaffolds/railway.json +12 -0
  21. package/scaffolds/tailwind.config.js +8 -0
  22. package/scaffolds/tauri.conf.json +26 -0
  23. package/schemas/language-module.json +116 -0
  24. package/schemas/prompt-lang.json +38 -3
  25. package/schemas/structures.json +145 -0
  26. package/src/ai/prompt-builder.js +184 -0
  27. package/src/ai/providers.js +247 -0
  28. package/src/annotations/registry.js +39 -0
  29. package/src/annotations/tags.json +24 -0
  30. package/src/commands/ai-gen.js +161 -0
  31. package/src/commands/component.js +242 -212
  32. package/src/commands/context.js +184 -109
  33. package/src/commands/extract.js +242 -0
  34. package/src/commands/figma.js +15 -15
  35. package/src/commands/init.js +197 -93
  36. package/src/commands/integrate.js +406 -0
  37. package/src/commands/lang.js +148 -0
  38. package/src/commands/qa-gen.js +139 -0
  39. package/src/commands/scaffold.js +127 -0
  40. package/src/commands/suggest.js +24 -14
  41. package/src/commands/teach.js +110 -0
  42. package/src/commands/validate.js +143 -83
  43. package/src/commands/wizard.js +456 -0
  44. package/src/generators/figma-prompt.js +20 -12
  45. package/src/language-service/plugin.cjs +94 -0
  46. package/src/language-service/plugin.d.ts +6 -0
  47. package/src/mcp-server.js +605 -0
  48. package/src/templates/langs/react/INDEX.json +262 -0
  49. package/src/templates/langs/react/MODULE.json +166 -0
  50. package/src/templates/langs/react/templates/hooks/useAuth.template.ts +134 -0
  51. package/src/templates/langs/react/templates/hooks/useDebounce.template.ts +45 -0
  52. package/src/templates/langs/react/templates/hooks/useForm.template.ts +146 -0
  53. package/src/templates/langs/react/templates/hooks/usePagination.template.ts +108 -0
  54. package/src/templates/langs/react/templates/services/apiService.template.ts +123 -0
  55. package/src/templates/langs/react/templates/ui/Button.template.tsx +87 -0
  56. package/src/templates/langs/react/templates/ui/Card.template.tsx +85 -0
  57. package/src/templates/langs/react/templates/ui/DataTable.template.tsx +163 -0
  58. package/src/templates/langs/react/templates/ui/Input.template.tsx +96 -0
  59. package/src/templates/langs/react/templates/ui/Modal.template.tsx +133 -0
  60. package/src/templates/langs/react/templates/ui/Select.template.tsx +99 -0
  61. package/src/templates/langs/vue/INDEX.json +246 -0
  62. package/src/templates/langs/vue/MODULE.json +105 -0
  63. package/src/templates/langs/vue/templates/composables/useAuth.template.ts +106 -0
  64. package/src/templates/langs/vue/templates/composables/useDebounce.template.ts +47 -0
  65. package/src/templates/langs/vue/templates/composables/useFetch.template.ts +54 -0
  66. package/src/templates/langs/vue/templates/composables/useForm.template.ts +127 -0
  67. package/src/templates/langs/vue/templates/composables/usePagination.template.ts +98 -0
  68. package/src/templates/langs/vue/templates/services/apiService.template.ts +116 -0
  69. package/src/templates/langs/vue/templates/ui/Button.template.vue +79 -0
  70. package/src/templates/langs/vue/templates/ui/Card.template.vue +73 -0
  71. package/src/templates/langs/vue/templates/ui/DataTable.template.vue +115 -0
  72. package/src/templates/langs/vue/templates/ui/Input.template.vue +70 -0
  73. package/src/templates/langs/vue/templates/ui/Modal.template.vue +112 -0
  74. package/src/templates/langs/vue/templates/ui/Select.template.vue +77 -0
  75. package/src/templates/scripts/log-actividad.sh +32 -0
  76. package/src/templates/scripts/log-commit.sh +35 -0
  77. package/src/templates/scripts/log-error.sh +45 -0
  78. package/src/templates/scripts/validate.sh +23 -0
  79. package/src/ts-transformer/index.cjs +86 -0
  80. package/src/utils/ai.js +35 -53
  81. package/src/utils/annotations.js +260 -214
  82. package/src/utils/config.js +61 -13
  83. package/src/utils/error-learner.js +203 -0
  84. package/src/utils/file-utils.js +119 -0
  85. package/src/utils/language-loader.js +167 -0
  86. package/src/utils/template-utils.js +45 -0
  87. package/src/vite-plugin/index.js +54 -0
  88. package/vscode-extension/package.json +23 -2
  89. package/vscode-extension/snippets/promptlang.json +1 -3
  90. package/vscode-extension/syntaxes/annotations-code.tmGrammar.json +15 -0
@@ -1,339 +1,370 @@
1
- import chalk from "chalk";
1
+ import chalk from "chalk"
2
+ import { getBuiltinTags, getAnnotationTags } from "../annotations/registry.js"
2
3
 
3
4
  // ─── Parser ────────────────────────────────────────────────────────────────
4
5
 
5
- export function parseAnnotations(code) {
6
- const annotations = [];
7
- const lines = code.split("\n");
8
- const errors = [];
6
+ export function parseAnnotations(code, options = {}) {
7
+ const annotations = []
8
+ const lines = code.split("\n")
9
+ const errors = []
10
+ const { langId, projectConfig } = options
11
+ const knownTags = projectConfig ? getAnnotationTags(langId, projectConfig) : getBuiltinTags()
9
12
 
10
- let currentBlock = null;
11
- let inBlock = false;
13
+ let currentBlock = null
14
+ let inBlock = false
12
15
 
13
16
  for (let i = 0; i < lines.length; i++) {
14
- const line = lines[i];
15
- const trimmed = line.trim();
17
+ const line = lines[i]
18
+ const trimmed = line.trim()
16
19
 
17
20
  // Detecta bloque de anotaciones /* ... */
18
21
  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)
22
+ inBlock = true
23
+ currentBlock = ""
24
+ const cleaned = trimmed
25
+ .replace(/^\/\*+\s*/, "")
26
+ .replace(/\s*\*+\/$/, "")
27
+ .trim()
23
28
  if (cleaned) {
24
- currentBlock += cleaned + " ";
29
+ currentBlock += cleaned + " "
25
30
  }
26
31
  if (trimmed.includes("*/")) {
27
- inBlock = false;
32
+ inBlock = false
28
33
  if (currentBlock.trim()) {
29
- annotations.push(...parseTags(currentBlock));
34
+ annotations.push(...parseTags(currentBlock))
30
35
  }
31
- currentBlock = null;
36
+ currentBlock = null
32
37
  }
33
- continue;
38
+ continue
34
39
  }
35
40
 
36
41
  if (inBlock) {
37
42
  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
+ const content = trimmed.replace(/\s*\*+\/$/, "").trim()
44
+ currentBlock += content + " "
45
+ inBlock = false
46
+ annotations.push(...parseTags(currentBlock))
47
+ currentBlock = null
43
48
  } else {
44
- const content = trimmed.replace(/^\s*\*\s*/, "").trim();
45
- currentBlock += content + " ";
49
+ const content = trimmed.replace(/^\s*\*\s*/, "").trim()
50
+ currentBlock += content + " "
46
51
  }
47
- continue;
52
+ continue
48
53
  }
49
54
 
50
- // Detecta línea de comentario con @
51
- if (trimmed.startsWith("//") && trimmed.includes("@")) {
52
- const tagLine = trimmed.replace(/^\/\/\s*/, "");
53
- annotations.push(...parseTags(tagLine));
55
+ // Detecta // comment inline (incluso después de código)
56
+ const commentIdx = trimmed.lastIndexOf("//")
57
+ const hasInlineComment = commentIdx !== -1 && trimmed.includes("@", commentIdx)
58
+ if (hasInlineComment) {
59
+ const afterComment = trimmed.slice(commentIdx + 2).trim()
60
+ if (afterComment.startsWith("@") || afterComment.includes("@")) {
61
+ annotations.push(...parseTags(afterComment))
62
+ continue
63
+ }
64
+ }
65
+
66
+ // Detecta anotación como código (sin //)
67
+ const codeMatch = trimmed.match(/^@(\w[\w-]*)\(/)
68
+ if (codeMatch && knownTags.includes(codeMatch[1])) {
69
+ annotations.push(...parseTags(trimmed))
54
70
  }
55
71
  }
56
72
 
57
73
  // Si quedó un bloque abierto sin cerrar, lo forzamos
58
74
  if (inBlock && currentBlock) {
59
- annotations.push(...parseTags(currentBlock));
60
- errors.push("Comentario multinea sin cerrar. Las anotaciones se procesaron igual.");
75
+ annotations.push(...parseTags(currentBlock))
76
+ errors.push("Comentario multinea sin cerrar. Las anotaciones se procesaron igual.")
61
77
  }
62
78
 
63
79
  // Extraer @use primero (determina qué tags son válidos)
64
- const useTag = annotations.find((a) => a.name === "use");
80
+ const useTag = annotations.find((a) => a.name === "use")
65
81
 
66
82
  // Edge case: @use(*) → todos los tags son válidos
67
- let validTags = [];
83
+ let validTags = []
68
84
  if (useTag) {
69
- const starArg = useTag.args.find((a) => a.value === "*");
85
+ const starArg = useTag.args.find((a) => a.value === "*")
70
86
  if (starArg) {
71
- validTags = []; // empty = todos permitidos
87
+ validTags = [] // empty = todos permitidos
72
88
  } else {
73
- validTags = useTag.args.map((a) => a.value);
89
+ validTags = useTag.args.map((a) => a.value)
74
90
  }
75
91
  }
76
92
 
77
93
  // Edge case: @use presente pero sin tags reales
78
94
  if (useTag && useTag.args.length === 0) {
79
- errors.push("@use() está vacío — no se importaron tags");
95
+ errors.push("@use() está vacío — no se importaron tags")
80
96
  }
81
97
 
82
98
  // Edge case: @use sin anotaciones de tags abajo
83
99
  if (useTag && annotations.filter((a) => a.name !== "use").length === 0) {
84
- errors.push("@use presente pero no hay anotaciones de tags en el archivo");
100
+ errors.push("@use presente pero no hay anotaciones de tags en el archivo")
85
101
  }
86
102
 
87
- return { annotations, validTags, parseErrors: errors };
103
+ return { annotations, validTags, parseErrors: errors }
88
104
  }
89
105
 
90
106
  function parseTags(text) {
91
- const tags = [];
107
+ const tags = []
92
108
 
93
- if (!text || text.trim().length === 0) return tags;
109
+ if (!text || text.trim().length === 0) return tags
94
110
 
95
111
  // Parseador manual con conteo de profundidad de paréntesis
96
- let i = 0;
112
+ let i = 0
97
113
  while (i < text.length) {
98
114
  // Buscar @ seguido de nombre de tag
99
- const atIndex = text.indexOf("@", i);
100
- if (atIndex === -1) break;
115
+ const atIndex = text.indexOf("@", i)
116
+ if (atIndex === -1) break
101
117
 
102
118
  // Extraer nombre del tag (letras, números, guiones)
103
- const nameMatch = text.slice(atIndex + 1).match(/^\w+/);
104
- if (!nameMatch) { i = atIndex + 1; continue; }
119
+ const nameMatch = text.slice(atIndex + 1).match(/^[\w-]+/)
120
+ if (!nameMatch) {
121
+ i = atIndex + 1
122
+ continue
123
+ }
105
124
 
106
- const name = nameMatch[0];
107
- let argsRaw = "";
108
- let parenDepth = 0;
109
- let pos = atIndex + 1 + name.length;
125
+ const name = nameMatch[0]
126
+ let argsRaw = ""
127
+ let parenDepth = 0
128
+ let pos = atIndex + 1 + name.length
110
129
 
111
130
  // Si hay paréntesis, extraemos con depth tracking
112
131
  if (pos < text.length && text[pos] === "(") {
113
- parenDepth = 1;
114
- pos++;
115
- const start = pos;
132
+ parenDepth = 1
133
+ pos++
134
+ const start = pos
116
135
  while (pos < text.length && parenDepth > 0) {
117
- if (text[pos] === "(") parenDepth++;
118
- else if (text[pos] === ")") parenDepth--;
119
- pos++;
136
+ if (text[pos] === "(") parenDepth++
137
+ else if (text[pos] === ")") parenDepth--
138
+ pos++
120
139
  }
121
- argsRaw = text.slice(start, pos - 1).trim();
140
+ argsRaw = text.slice(start, pos - 1).trim()
122
141
  }
123
142
 
124
- tags.push({ name, args: parseArgs(argsRaw), raw: `@${name}(${argsRaw})` });
125
- i = pos;
143
+ tags.push({ name, args: parseArgs(argsRaw), raw: `@${name}(${argsRaw})` })
144
+ i = pos
126
145
  }
127
146
 
128
- return tags;
147
+ return tags
129
148
  }
130
149
 
131
150
  function parseArgs(raw) {
132
- const args = [];
151
+ const args = []
133
152
 
134
- if (!raw || raw.trim().length === 0) return args;
153
+ if (!raw || raw.trim().length === 0) return args
135
154
 
136
- const trimmed = raw.trim();
137
- const lenient = trimmed.replace(/\)$/, "");
155
+ const trimmed = raw.trim()
138
156
 
139
157
  // Intenta parsear como JSON object primero: { key: value, ... }
140
158
  if (trimmed.startsWith("{")) {
141
159
  try {
142
- const obj = JSON.parse(trimmed);
160
+ const obj = JSON.parse(trimmed)
143
161
  for (const [key, value] of Object.entries(obj)) {
144
- args.push({ key, value: String(value), type: "kv" });
162
+ args.push({ key, value: String(value), type: "kv" })
145
163
  }
146
- return args;
164
+ return args
147
165
  } catch {
148
166
  // Fallback: parsear como TypeScript object { key: type, key?: type }
149
- const tsProps = parseTsObject(trimmed);
167
+ const tsProps = parseTsObject(trimmed)
150
168
  if (tsProps.length > 0) {
151
- args.push(...tsProps);
152
- return args;
169
+ args.push(...tsProps)
170
+ return args
153
171
  }
154
172
  // Edge case: JSON inválido → error reportado en validación
155
- args.push({ key: null, value: trimmed, type: "invalid-json" });
156
- return args;
173
+ args.push({ key: null, value: trimmed, type: "invalid-json" })
174
+ return args
157
175
  }
158
176
  }
159
177
 
160
178
  // Detectar mezcla de subtags, flags y key:values
161
179
  // Dividir por coma respetando corchetes
162
- const parts = splitRespectingBrackets(lenient);
180
+ const parts = splitRespectingBrackets(trimmed)
163
181
 
164
182
  for (const part of parts) {
165
- const p = part.trim();
166
- if (!p) continue;
183
+ const p = part.trim()
184
+ if (!p) continue
167
185
 
168
186
  // Subtag: @algo: [lista]
169
- const subMatch = p.match(/^@(\w+)\s*:\s*\[([^\]]*)\]/);
187
+ const subMatch = p.match(/^@(\w+)\s*:\s*\[([^\]]*)\]/)
170
188
  if (subMatch) {
171
189
  args.push({
172
190
  key: subMatch[1],
173
191
  value: subMatch[2].split(",").map((s) => s.trim().replace(/["']/g, "")),
174
192
  type: "subtag",
175
- });
176
- continue;
193
+ })
194
+ continue
177
195
  }
178
196
 
179
197
  // Flag: @algo
180
- const flagMatch = p.match(/^@(\w+)/);
198
+ const flagMatch = p.match(/^@(\w+)/)
181
199
  if (flagMatch) {
182
- args.push({ key: flagMatch[1], value: true, type: "flag" });
183
- continue;
200
+ args.push({ key: flagMatch[1], value: true, type: "flag" })
201
+ continue
184
202
  }
185
203
 
186
204
  // Key: value
187
- const kvMatch = p.match(/^(\w+)\s*:\s*(.+)/);
205
+ const kvMatch = p.match(/^(\w+)\s*:\s*(.+)/)
188
206
  if (kvMatch) {
189
- args.push({ key: kvMatch[1].trim(), value: kvMatch[2].trim().replace(/["']/g, ""), type: "kv" });
190
- continue;
207
+ args.push({
208
+ key: kvMatch[1].trim(),
209
+ value: kvMatch[2].trim().replace(/["']/g, ""),
210
+ type: "kv",
211
+ })
212
+ continue
191
213
  }
192
214
 
193
215
  // Valor plano
194
- args.push({ key: null, value: p.replace(/["']/g, ""), type: "plain" });
216
+ args.push({ key: null, value: p.replace(/["']/g, ""), type: "plain" })
195
217
  }
196
218
 
197
- return args;
219
+ return args
198
220
  }
199
221
 
200
222
  function parseTsObject(raw) {
201
223
  // 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 = [];
224
+ const inner = raw.trim().replace(/^{/, "").replace(/}$/, "").trim()
225
+ if (!inner) return []
226
+ const parts = splitRespectingBrackets(inner)
227
+ const result = []
206
228
  for (const part of parts) {
207
- const p = part.trim();
208
- if (!p) continue;
229
+ const p = part.trim()
230
+ if (!p) continue
209
231
  // key: type o key?: type o "key": "type"
210
- const m = p.match(/^"([^"]+)"\s*:\s*(.+)/) || p.match(/^(\w+\??)\s*:\s*(.+)/);
232
+ const m = p.match(/^"([^"]+)"\s*:\s*(.+)/) || p.match(/^(\w+\??)\s*:\s*(.+)/)
211
233
  if (m) {
212
- const key = m[1].replace(/\?$/, "?");
213
- let value = m[2].trim().replace(/["']/g, "");
234
+ const key = m[1].replace(/\?$/, "?")
235
+ let value = m[2].trim().replace(/["']/g, "")
214
236
  // Quitar default values: = "Default" → ignorar
215
- value = value.replace(/\s*=\s*.+$/, "").trim();
216
- result.push({ key, value, type: "kv" });
237
+ value = value.replace(/\s*=\s*.+$/, "").trim()
238
+ result.push({ key, value, type: "kv" })
217
239
  }
218
240
  }
219
- return result;
241
+ return result
220
242
  }
221
243
 
222
244
  function splitRespectingBrackets(str) {
223
- const result = [];
224
- let current = "";
225
- let depth = 0;
245
+ const result = []
246
+ let current = ""
247
+ let depth = 0
226
248
 
227
249
  for (const ch of str) {
228
250
  if (ch === "[" || ch === "{" || ch === "(") {
229
- depth++;
230
- current += ch;
251
+ depth++
252
+ current += ch
231
253
  } else if (ch === "]" || ch === "}" || ch === ")") {
232
- depth--;
233
- current += ch;
254
+ depth--
255
+ current += ch
234
256
  } else if (ch === "," && depth === 0) {
235
- result.push(current.trim());
236
- current = "";
257
+ result.push(current.trim())
258
+ current = ""
237
259
  } else {
238
- current += ch;
260
+ current += ch
239
261
  }
240
262
  }
241
- if (current.trim()) result.push(current.trim());
242
- return result;
263
+ if (current.trim()) result.push(current.trim())
264
+ return result
243
265
  }
244
266
 
245
267
  // ─── Validador ──────────────────────────────────────────────────────────────
246
268
 
247
269
  export function validateAnnotations(annotations, validTags, options = {}) {
248
- const errors = [];
249
- const warnings = [];
250
- const tags = annotations.filter((a) => a.name !== "use");
270
+ const errors = []
271
+ const warnings = []
272
+ const tags = annotations.filter((a) => a.name !== "use")
251
273
 
252
274
  // Validar tags contra @use
253
- const useAll = validTags.length === 0 && annotations.some(
254
- (a) => a.name === "use" && a.args.some((x) => x.value === "*")
255
- );
275
+ const useAll =
276
+ validTags.length === 0 &&
277
+ annotations.some((a) => a.name === "use" && a.args.some((x) => x.value === "*"))
256
278
 
257
279
  if (validTags.length > 0 && !useAll) {
258
280
  for (const tag of tags) {
259
281
  if (!validTags.includes(tag.name)) {
260
- errors.push(`Tag @${tag.name} no está importado. Agrega @use(${tag.name})`);
282
+ errors.push(`Tag @${tag.name} no está importado. Agrega @use(${tag.name})`)
261
283
  }
262
284
  }
263
285
  }
264
286
 
265
287
  // Validar @kind
266
- const kindTag = tags.find((t) => t.name === "kind");
288
+ const kindTag = tags.find((t) => t.name === "kind")
267
289
  const validKinds = [
268
- "hook", "component", "page", "service", "store",
269
- "util", "type", "layout", "feature",
270
- ];
290
+ "hook",
291
+ "component",
292
+ "page",
293
+ "service",
294
+ "store",
295
+ "util",
296
+ "type",
297
+ "layout",
298
+ "feature",
299
+ ]
271
300
 
272
301
  if (kindTag) {
273
- const kindValue = kindTag.args[0]?.value;
302
+ const kindValue = kindTag.args[0]?.value
274
303
 
275
304
  // Edge case: @kind sin valor
276
305
  if (!kindValue || kindValue.trim() === "") {
277
- errors.push("@kind() necesita un valor. Válidos: " + validKinds.join(", "));
306
+ errors.push("@kind() necesita un valor. Válidos: " + validKinds.join(", "))
278
307
  } else if (!validKinds.includes(kindValue)) {
279
- errors.push(
280
- `@kind("${kindValue}") inválido. Valores válidos: ${validKinds.join(", ")}`
281
- );
308
+ errors.push(`@kind("${kindValue}") inválido. Valores válidos: ${validKinds.join(", ")}`)
282
309
  }
283
310
 
284
311
  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");
312
+ const hasContract = tags.some((t) => t.name === "contract")
313
+ const hasProps = tags.some((t) => t.name === "props")
314
+ const hasLimit = tags.some((t) => t.name === "limit")
315
+ const hasCompose = tags.some((t) => t.name === "compose")
289
316
 
290
317
  if (kindValue === "hook" && !hasContract) {
291
- warnings.push("@kind(hook) debería tener @contract");
318
+ warnings.push("@kind(hook) debería tener @contract")
292
319
  }
293
320
  if (kindValue === "component" && hasContract) {
294
- errors.push("@kind(component) no puede tener @contract. Usa @props en su lugar.");
321
+ errors.push("@kind(component) no puede tener @contract. Usa @props en su lugar.")
295
322
  }
296
323
  if (kindValue === "component" && !hasProps) {
297
- warnings.push("@kind(component) debería tener @props");
324
+ warnings.push("@kind(component) debería tener @props")
298
325
  }
299
326
  if (kindValue === "page" && !hasCompose) {
300
- warnings.push("@kind(page) debería tener @compose (debe componer subcomponentes)");
327
+ warnings.push("@kind(page) debería tener @compose (debe componer subcomponentes)")
301
328
  }
302
329
  if (kindValue === "service" && !hasContract) {
303
- errors.push("@kind(service) requiere @contract");
330
+ errors.push("@kind(service) requiere @contract")
304
331
  }
305
332
  if (kindValue === "hook" && hasProps) {
306
- errors.push("@kind(hook) no puede tener @props");
333
+ errors.push("@kind(hook) no puede tener @props")
307
334
  }
308
335
  if (kindValue === "type" && hasLimit) {
309
- warnings.push("@kind(type) no necesita @limit");
336
+ warnings.push("@kind(type) no necesita @limit")
310
337
  }
311
338
  if (kindValue === "store") {
312
- const hasDeps = tags.some((t) => t.name === "deps");
339
+ const hasDeps = tags.some((t) => t.name === "deps")
313
340
  if (!hasDeps) {
314
- warnings.push("@kind(store) debería tener @deps (zustand, context, etc.)");
341
+ warnings.push("@kind(store) debería tener @deps (zustand, context, etc.)")
315
342
  }
316
343
  }
317
344
 
318
345
  // Límites por defecto según kind
319
- const limitTag = tags.find((t) => t.name === "limit");
346
+ const limitTag = tags.find((t) => t.name === "limit")
320
347
  if (limitTag) {
321
- const linesArg = limitTag.args.find((a) => a.key === "lines");
348
+ const linesArg = limitTag.args.find((a) => a.key === "lines")
322
349
  if (linesArg) {
323
350
  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);
351
+ hook: 80,
352
+ component: 120,
353
+ page: 200,
354
+ service: 150,
355
+ store: 100,
356
+ layout: 150,
357
+ }
358
+ const expected = maxLines[kindValue]
359
+ const numVal = parseInt(linesArg.value)
329
360
  if (expected && !isNaN(numVal) && numVal > expected) {
330
361
  warnings.push(
331
362
  `@kind(${kindValue}) @limit(lines) debería ser ≤ ${expected} (actual: ${linesArg.value})`
332
- );
363
+ )
333
364
  }
334
365
  // Edge case: límite 0 o negativo
335
366
  if (!isNaN(numVal) && numVal <= 0) {
336
- errors.push(`@limit(lines: ${numVal}) debe ser mayor a 0`);
367
+ errors.push(`@limit(lines: ${numVal}) debe ser mayor a 0`)
337
368
  }
338
369
  }
339
370
  }
@@ -341,16 +372,14 @@ export function validateAnnotations(annotations, validTags, options = {}) {
341
372
  }
342
373
 
343
374
  // Validar @contract — edge cases
344
- const contractTag = tags.find((t) => t.name === "contract");
375
+ const contractTag = tags.find((t) => t.name === "contract")
345
376
  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;
377
+ const args = contractTag.args
378
+ const hasInput = args.some((a) => a.key === "in" || a.type === "plain")
379
+ const contractText = contractTag.raw
350
380
 
351
- // Edge case: contrato vacío
352
381
  if (args.length === 0 && contractTag.raw === "@contract()") {
353
- errors.push("@contract() está vacío. Formato: @contract(in: tipo -> out: tipo @error: tipo)");
382
+ errors.push("@contract() está vacío. Formato: @contract(in: tipo -> out: tipo @error: tipo)")
354
383
  }
355
384
 
356
385
  // Edge case: sin input claro
@@ -358,153 +387,170 @@ export function validateAnnotations(annotations, validTags, options = {}) {
358
387
  // Puede ser formato @contract(in: -> out:) sin inputs
359
388
  if (contractText.includes("->")) {
360
389
  // tiene flecha pero sin "in:" → intentamos extraer
361
- const parts = contractText.split("->");
362
- const inputPart = parts[0].replace("@contract(", "").replace("@contract", "").trim();
390
+ const parts = contractText.split("->")
391
+ const inputPart = parts[0].replace("@contract(", "").replace("@contract", "").trim()
363
392
  if (!inputPart || inputPart === "in: " || inputPart === "in:") {
364
- errors.push("@contract: falta definir tipos de entrada (in:)");
393
+ errors.push("@contract: falta definir tipos de entrada (in:)")
365
394
  }
366
395
  }
367
396
  }
368
397
 
369
398
  // Edge case: @contract(in: any -> out: any)
370
399
  if (contractTag.raw.includes("any")) {
371
- warnings.push("@contract usa 'any' — especifica tipos concretos");
400
+ warnings.push("@contract usa 'any' — especifica tipos concretos")
372
401
  }
373
402
  }
374
403
 
375
404
  // Validar @props — edge case JSON inválido
376
- const propsTag = tags.find((t) => t.name === "props");
405
+ const propsTag = tags.find((t) => t.name === "props")
377
406
  if (propsTag) {
378
- const hasInvalidJson = propsTag.args.some((a) => a.type === "invalid-json");
407
+ const hasInvalidJson = propsTag.args.some((a) => a.type === "invalid-json")
379
408
  if (hasInvalidJson) {
380
- errors.push(`@props(${propsTag.args[0]?.value}) no es un JSON válido. Formato: @props({ key: type })`);
409
+ errors.push(
410
+ `@props(${propsTag.args[0]?.value}) no es un JSON válido. Formato: @props({ key: type })`
411
+ )
381
412
  }
382
413
  }
383
414
 
384
415
  // Validar @platform — edge case web + capacitor
385
- const platformTag = tags.find((t) => t.name === "platform");
386
- const depsTag = tags.find((t) => t.name === "deps");
416
+ const platformTag = tags.find((t) => t.name === "platform")
417
+ const depsTag = tags.find((t) => t.name === "deps")
387
418
  if (platformTag && depsTag) {
388
- const platforms = platformTag.args.map((a) => a.value);
389
- const deps = depsTag.args;
419
+ const platforms = platformTag.args.map((a) => a.value)
420
+ const deps = depsTag.args
390
421
 
391
422
  if (platforms.includes("web")) {
392
- const hasCapacitor = deps.some(
393
- (d) => d.value && (String(d.value).includes("@capacitor"))
394
- );
423
+ const hasCapacitor = deps.some((d) => d.value && String(d.value).includes("@capacitor"))
395
424
  if (hasCapacitor) {
396
- errors.push("@platform(web) no es compatible con @capacitor/*");
425
+ errors.push("@platform(web) no es compatible con @capacitor/*")
397
426
  }
398
- const hasTauri = deps.some(
399
- (d) => d.value && String(d.value).includes("@tauri-apps")
400
- );
427
+ const hasTauri = deps.some((d) => d.value && String(d.value).includes("@tauri-apps"))
401
428
  if (hasTauri) {
402
- errors.push("@platform(web) no es compatible con @tauri-apps/*");
429
+ errors.push("@platform(web) no es compatible con @tauri-apps/*")
403
430
  }
404
431
  }
405
432
 
406
433
  if (platforms.includes("mobile")) {
407
- const hasTauri = deps.some(
408
- (d) => d.value && String(d.value).includes("@tauri-apps")
409
- );
434
+ const hasTauri = deps.some((d) => d.value && String(d.value).includes("@tauri-apps"))
410
435
  if (hasTauri) {
411
- errors.push("@platform(mobile) no es compatible con @tauri-apps/*");
436
+ errors.push("@platform(mobile) no es compatible con @tauri-apps/*")
412
437
  }
413
438
  }
414
439
  if (platforms.includes("desktop")) {
415
- const hasCapacitor = deps.some(
416
- (d) => d.value && String(d.value).includes("@capacitor")
417
- );
440
+ const hasCapacitor = deps.some((d) => d.value && String(d.value).includes("@capacitor"))
418
441
  if (hasCapacitor) {
419
- errors.push("@platform(desktop) no es compatible con @capacitor/*");
442
+ errors.push("@platform(desktop) no es compatible con @capacitor/*")
420
443
  }
421
444
  }
422
445
  }
423
446
 
424
447
  // Validar @limit — edge cases de valores
425
- const limitTag = tags.find((t) => t.name === "limit");
448
+ const limitTag = tags.find((t) => t.name === "limit")
426
449
  if (limitTag) {
427
450
  for (const arg of limitTag.args) {
428
451
  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`);
452
+ if (!/^\d+(\.\d+)?$/.test(arg.value)) {
453
+ errors.push(`@limit(${arg.key}: "${arg.value}") esperaba un número, recibió texto`)
432
454
  }
433
455
  }
434
456
  }
435
457
  // Edge case: @limit() vacío
436
458
  if (limitTag.args.length === 0) {
437
- errors.push("@limit() está vacío. Especifica al menos un límite: @limit(lines: 120)");
459
+ errors.push("@limit() está vacío. Especifica al menos un límite: @limit(lines: 120)")
438
460
  }
439
461
  }
440
462
 
441
463
  // Validar @forbidden
442
- const forbiddenTag = tags.find((t) => t.name === "forbidden");
464
+ const forbiddenTag = tags.find((t) => t.name === "forbidden")
443
465
  if (forbiddenTag) {
444
466
  if (forbiddenTag.args.length === 0) {
445
- warnings.push("@forbidden() está vacío — no prohíbe nada");
467
+ warnings.push("@forbidden() está vacío — no prohíbe nada")
446
468
  }
447
469
  }
448
470
 
449
471
  // Validar @scope breaking
450
- const scopeTag = tags.find((t) => t.name === "scope");
472
+ const scopeTag = tags.find((t) => t.name === "scope")
451
473
  if (scopeTag) {
452
- const breaking = scopeTag.args.find((a) => a.key === "breaking");
474
+ const breaking = scopeTag.args.find((a) => a.key === "breaking")
453
475
  if (breaking && breaking.value === "true") {
454
- warnings.push("@scope(breaking: true) — el commit debe indicar breaking change (!)");
476
+ warnings.push("@scope(breaking: true) — el commit debe indicar breaking change (!)")
455
477
  }
456
478
  }
457
479
 
458
480
  // Validar @test
459
- const testTag = tags.find((t) => t.name === "test");
481
+ const testTag = tags.find((t) => t.name === "test")
460
482
  if (testTag) {
461
- const coverage = testTag.args.find((a) => a.key === "coverage");
483
+ const coverage = testTag.args.find((a) => a.key === "coverage")
462
484
  if (coverage) {
463
- const val = parseInt(coverage.value);
485
+ const val = parseInt(coverage.value)
464
486
  if (!isNaN(val) && val > 100) {
465
- errors.push(`@test(@coverage: ${val}) no puede exceder 100`);
487
+ errors.push(`@test(@coverage: ${val}) no puede exceder 100`)
466
488
  }
467
489
  }
468
490
  }
469
491
 
492
+ // Validar @pattern
493
+ const patternTag = tags.find((t) => t.name === "pattern")
494
+ if (patternTag) {
495
+ const validPatterns = [
496
+ "compound",
497
+ "composition",
498
+ "generic",
499
+ "render-prop",
500
+ "provider",
501
+ "singleton",
502
+ ]
503
+ const patternValue = patternTag.args[0]?.value
504
+
505
+ if (!patternValue || patternValue.trim() === "") {
506
+ errors.push("@pattern() necesita un valor. Válidos: " + validPatterns.join(", "))
507
+ } else if (patternValue === "inheritance") {
508
+ warnings.push("@pattern(inheritance) — usa @pattern(composition) en su lugar")
509
+ } else if (!validPatterns.includes(patternValue)) {
510
+ warnings.push(
511
+ `@pattern("${patternValue}") no es un valor estándar. Válidos: ${validPatterns.join(", ")}`
512
+ )
513
+ }
514
+ }
515
+
470
516
  // Validar @state error → debe tener @contract (solo en kinds que requieren contract)
471
- const stateTag = tags.find((t) => t.name === "state");
517
+ const stateTag = tags.find((t) => t.name === "state")
472
518
  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"];
519
+ const hasError = stateTag.args.some((a) => a.value === "error")
520
+ const hasContract = tags.some((t) => t.name === "contract")
521
+ const kindValue = kindTag?.args[0]?.value
522
+ const contractKinds = ["hook", "service"]
477
523
  if (hasError && !hasContract && contractKinds.includes(kindValue)) {
478
- warnings.push("@state(error) presente pero no hay @contract — define el tipo de error");
524
+ warnings.push("@state(error) presente pero no hay @contract — define el tipo de error")
479
525
  }
480
526
  }
481
527
 
482
- return { errors, warnings };
528
+ return { errors, warnings }
483
529
  }
484
530
 
485
531
  // ─── API pública ────────────────────────────────────────────────────────────
486
532
 
487
- export function lintFile(code, strict = false) {
488
- const { annotations, validTags, parseErrors } = parseAnnotations(code);
533
+ export function lintFile(code, strict = false, options = {}) {
534
+ const { annotations, validTags, parseErrors } = parseAnnotations(code, options)
489
535
 
490
536
  if (annotations.length === 0) {
491
- return { annotations: [], errors: parseErrors, warnings: [] };
537
+ return { annotations: [], errors: parseErrors, warnings: [] }
492
538
  }
493
539
 
494
- const { errors, warnings } = validateAnnotations(annotations, validTags);
495
- const allErrors = [...parseErrors, ...errors];
496
- const allWarnings = [...warnings];
540
+ const { errors, warnings } = validateAnnotations(annotations, validTags)
541
+ const allErrors = [...parseErrors, ...errors]
542
+ const allWarnings = [...warnings]
497
543
 
498
544
  if (strict && allWarnings.length > 0) {
499
- allErrors.push(...allWarnings.map(w => `[strict] ${w}`));
500
- return { annotations, errors: allErrors, warnings: [] };
545
+ allErrors.push(...allWarnings.map((w) => `[strict] ${w}`))
546
+ return { annotations, errors: allErrors, warnings: allWarnings }
501
547
  }
502
548
 
503
549
  return {
504
550
  annotations,
505
551
  errors: allErrors,
506
552
  warnings: allWarnings,
507
- };
553
+ }
508
554
  }
509
555
 
510
- export { parseArgs };
556
+ export { parseArgs }