@justmpm/ai-tool 0.9.2 → 1.0.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.
@@ -143,8 +143,235 @@ function isCodeFile(filePath) {
143
143
  return CODE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
144
144
  }
145
145
 
146
+ // src/utils/hints.ts
147
+ var COMMAND_MAP = {
148
+ map: {
149
+ cli: "ai-tool map",
150
+ mcp: "analyze__aitool_project_map"
151
+ },
152
+ dead: {
153
+ cli: "ai-tool dead",
154
+ mcp: "analyze__aitool_dead_code"
155
+ },
156
+ impact: {
157
+ cli: "ai-tool impact <arquivo>",
158
+ mcp: "analyze__aitool_impact_analysis { target: '<arquivo>' }"
159
+ },
160
+ suggest: {
161
+ cli: "ai-tool suggest <arquivo>",
162
+ mcp: "analyze__aitool_suggest_reads { target: '<arquivo>' }"
163
+ },
164
+ context: {
165
+ cli: "ai-tool context <arquivo>",
166
+ mcp: "analyze__aitool_file_context { target: '<arquivo>' }"
167
+ },
168
+ area_context: {
169
+ cli: "ai-tool context --area=<nome>",
170
+ mcp: "analyze__aitool_area_context { area: '<nome>' }"
171
+ },
172
+ areas: {
173
+ cli: "ai-tool areas",
174
+ mcp: "analyze__aitool_list_areas"
175
+ },
176
+ area: {
177
+ cli: "ai-tool area <nome>",
178
+ mcp: "analyze__aitool_area_detail { target: '<nome>' }"
179
+ },
180
+ areas_init: {
181
+ cli: "ai-tool areas init",
182
+ mcp: "analyze__aitool_areas_init"
183
+ },
184
+ find: {
185
+ cli: "ai-tool find <termo>",
186
+ mcp: "analyze__aitool_find { query: '<termo>' }"
187
+ },
188
+ describe: {
189
+ cli: "ai-tool describe <termo>",
190
+ mcp: "analyze__aitool_describe { query: '<termo>' }"
191
+ },
192
+ functions: {
193
+ cli: "ai-tool functions",
194
+ mcp: "analyze__aitool_list_functions"
195
+ }
196
+ };
197
+ function hint(command, ctx, params) {
198
+ const mapping = COMMAND_MAP[command];
199
+ if (!mapping) return command;
200
+ let instruction = mapping[ctx];
201
+ if (params) {
202
+ for (const [placeholder, value] of Object.entries(params)) {
203
+ instruction = instruction.replaceAll(placeholder, value);
204
+ }
205
+ }
206
+ return instruction;
207
+ }
208
+ var NEXT_STEPS = {
209
+ map: [
210
+ { command: "area", description: "ver arquivos de uma area" },
211
+ { command: "suggest", description: "o que ler antes de editar" },
212
+ { command: "context", description: "ver API de um arquivo" }
213
+ ],
214
+ impact: [
215
+ { command: "suggest", description: "o que ler antes de editar este arquivo" },
216
+ { command: "context", description: "ver assinaturas dos arquivos upstream" },
217
+ { command: "find", description: "localizar usos de exports especificos" }
218
+ ],
219
+ suggest: [
220
+ { command: "context", description: "ver assinaturas de cada arquivo sugerido" },
221
+ { command: "impact", description: "ver impacto completo da mudanca" }
222
+ ],
223
+ context: [
224
+ { command: "impact", description: "ver quem sera afetado por mudancas" },
225
+ { command: "find", description: "localizar usos de uma funcao ou tipo" },
226
+ { command: "suggest", description: "o que ler antes de editar" }
227
+ ],
228
+ area_context: [
229
+ { command: "find", description: "buscar usos de simbolos desta area" },
230
+ { command: "area", description: "ver lista de arquivos desta area" },
231
+ { command: "impact", description: "ver impacto de modificar um arquivo" }
232
+ ],
233
+ find: [
234
+ { command: "context", description: "ver assinaturas completas do arquivo" },
235
+ { command: "impact", description: "ver impacto de modificar o arquivo" },
236
+ { command: "describe", description: "buscar areas por descricao" }
237
+ ],
238
+ describe: [
239
+ { command: "area", description: "ver detalhes de uma area" },
240
+ { command: "area_context", description: "contexto completo de uma area" }
241
+ ],
242
+ areas: [
243
+ { command: "area", description: "ver arquivos de uma area especifica" },
244
+ { command: "describe", description: "buscar areas por descricao" }
245
+ ],
246
+ area: [
247
+ { command: "area_context", description: "contexto consolidado (tipos, hooks, funcoes)" },
248
+ { command: "context", description: "ver assinaturas de um arquivo especifico" },
249
+ { command: "find", description: "buscar simbolos dentro da area" }
250
+ ],
251
+ dead: [
252
+ { command: "impact", description: "verificar se um arquivo morto e realmente nao usado" },
253
+ { command: "map", description: "ver estrutura atualizada do projeto" }
254
+ ],
255
+ functions: [
256
+ { command: "find", description: "buscar uma Cloud Function especifica" },
257
+ { command: "impact", description: "ver impacto de modificar uma function" },
258
+ { command: "context", description: "ver assinaturas de uma function" }
259
+ ]
260
+ };
261
+ function nextSteps(command, ctx) {
262
+ const steps = NEXT_STEPS[command];
263
+ if (!steps || steps.length === 0) return "";
264
+ let out = "\n\u{1F4D6} Proximos passos:\n";
265
+ for (const step of steps) {
266
+ const instruction = hint(step.command, ctx);
267
+ out += ` \u2192 ${instruction} - ${step.description}
268
+ `;
269
+ }
270
+ return out;
271
+ }
272
+ function recoveryHint(errorType, ctx, _extra) {
273
+ switch (errorType) {
274
+ case "file_not_found":
275
+ return `
276
+ \u{1F4A1} Dicas:
277
+ \u2192 ${hint("map", ctx)} - ver arquivos disponiveis
278
+ \u2192 ${hint("find", ctx)} - buscar por nome de simbolo
279
+ `;
280
+ case "area_not_found":
281
+ return `
282
+ \u{1F4A1} Dicas:
283
+ \u2192 ${hint("areas", ctx)} - listar areas disponiveis
284
+ \u2192 ${hint("describe", ctx)} - buscar areas por descricao
285
+ \u2192 ${hint("areas_init", ctx)} - gerar configuracao de areas
286
+ `;
287
+ case "no_results":
288
+ return `
289
+ \u{1F4A1} Dicas:
290
+ \u2192 ${hint("find", ctx)} - buscar com outro termo
291
+ \u2192 ${hint("describe", ctx)} - buscar areas por descricao
292
+ \u2192 ${hint("map", ctx)} - ver estrutura do projeto
293
+ `;
294
+ case "no_firebase":
295
+ return `
296
+ \u{1F4A1} Este projeto nao usa Firebase. Comandos disponiveis:
297
+ \u2192 ${hint("map", ctx)} - ver estrutura do projeto
298
+ \u2192 ${hint("find", ctx)} - buscar simbolos no codigo
299
+ \u2192 ${hint("areas", ctx)} - listar areas funcionais
300
+ `;
301
+ case "generic":
302
+ return `
303
+ \u{1F4A1} Tente:
304
+ \u2192 ${hint("map", ctx)} - verificar estrutura do projeto
305
+ \u2192 Verifique se o caminho (cwd) esta correto
306
+ `;
307
+ }
308
+ }
309
+
310
+ // src/utils/similarity.ts
311
+ function levenshteinDistance(a, b) {
312
+ const matrix = [];
313
+ for (let i = 0; i <= b.length; i++) {
314
+ matrix[i] = [i];
315
+ }
316
+ for (let j = 0; j <= a.length; j++) {
317
+ matrix[0][j] = j;
318
+ }
319
+ for (let i = 1; i <= b.length; i++) {
320
+ for (let j = 1; j <= a.length; j++) {
321
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
322
+ matrix[i][j] = matrix[i - 1][j - 1];
323
+ } else {
324
+ matrix[i][j] = Math.min(
325
+ matrix[i - 1][j - 1] + 1,
326
+ // substituição
327
+ matrix[i][j - 1] + 1,
328
+ // inserção
329
+ matrix[i - 1][j] + 1
330
+ // deleção
331
+ );
332
+ }
333
+ }
334
+ }
335
+ return matrix[b.length][a.length];
336
+ }
337
+ function findSimilar(target, candidates, options = {}) {
338
+ const {
339
+ maxDistance = 3,
340
+ limit = 5,
341
+ normalize: normalize2 = true,
342
+ extractKey = (item) => String(item)
343
+ } = options;
344
+ const normalizedTarget = normalize2 ? target.toLowerCase() : target;
345
+ const scored = candidates.map((item) => {
346
+ const key = extractKey(item);
347
+ const normalizedKey = normalize2 ? key.toLowerCase() : key;
348
+ const keyNoExt = normalizedKey.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
349
+ let score = Infinity;
350
+ if (normalizedKey.includes(normalizedTarget) || keyNoExt.includes(normalizedTarget)) {
351
+ score = 0;
352
+ } else {
353
+ score = levenshteinDistance(keyNoExt, normalizedTarget);
354
+ }
355
+ return { item, score };
356
+ }).filter(({ score }) => score <= maxDistance).sort((a, b) => a.score - b.score).slice(0, limit);
357
+ return scored.map(({ item }) => item);
358
+ }
359
+ function findBestMatch(target, candidates, extractKey = (item) => String(item)) {
360
+ const similar = findSimilar(target, candidates, {
361
+ maxDistance: 2,
362
+ // Mais restritivo para "você quis dizer"
363
+ limit: 1,
364
+ extractKey
365
+ });
366
+ return similar.length > 0 ? similar[0] : null;
367
+ }
368
+ function extractFileName(filePath) {
369
+ const fileName = filePath.split("/").pop() || filePath;
370
+ return fileName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
371
+ }
372
+
146
373
  // src/formatters/text.ts
147
- function formatMapSummary(result, areasInfo) {
374
+ function formatMapSummary(result, areasInfo, ctx = "cli") {
148
375
  let out = "";
149
376
  out += `\u{1F4CA} ${result.summary.totalFiles} arquivos | ${result.summary.totalFolders} pastas
150
377
  `;
@@ -172,9 +399,15 @@ function formatMapSummary(result, areasInfo) {
172
399
  }
173
400
  out += ` ${catParts.join(", ")}
174
401
  `;
402
+ const topFolders = result.folders.sort((a, b) => b.fileCount - a.fileCount).slice(0, 5);
403
+ if (topFolders.length > 0) {
404
+ const folderParts = topFolders.map((f) => `${f.path}/ (${f.fileCount})`);
405
+ out += `\u{1F4C1} ${folderParts.join(", ")}
406
+ `;
407
+ }
175
408
  if (areasInfo && areasInfo.total > 0) {
176
409
  out += `
177
- \u{1F5C2}\uFE0F \xC1reas: ${areasInfo.names.join(", ")}
410
+ \u{1F5C2}\uFE0F Areas: ${areasInfo.names.join(", ")}
178
411
  `;
179
412
  }
180
413
  out += `
@@ -185,43 +418,30 @@ function formatMapSummary(result, areasInfo) {
185
418
  `;
186
419
  out += ` ${cloudFunctionCount} function(s) em functions/src/
187
420
  `;
188
- out += ` \u2192 Use 'ai-tool functions' para listar triggers
421
+ out += ` \u2192 ${hint("functions", ctx)}
189
422
 
190
423
  `;
191
424
  }
192
425
  if (result.circularDependencies.length > 0) {
193
- out += `\u26A0\uFE0F ${result.circularDependencies.length} depend\xEAncia(s) circular(es) detectada(s)
426
+ out += `\u26A0\uFE0F ${result.circularDependencies.length} dependencia(s) circular(es) detectada(s)
194
427
  `;
195
- out += ` \u2192 Use impact <arquivo> para investigar
428
+ out += ` \u2192 ${hint("impact", ctx)} para investigar
196
429
 
197
430
  `;
198
431
  }
199
432
  if (areasInfo && areasInfo.unmappedCount > 0) {
200
- out += `\u26A0\uFE0F ${areasInfo.unmappedCount} arquivo(s) sem \xE1rea definida
433
+ out += `\u26A0\uFE0F ${areasInfo.unmappedCount} arquivo(s) sem area definida
201
434
  `;
202
- out += ` \u2192 Use areas init para configurar
435
+ out += ` \u2192 ${hint("areas_init", ctx)} para configurar
203
436
 
204
437
  `;
205
438
  }
206
- out += `\u{1F4D6} Pr\xF3ximos passos:
207
- `;
208
- out += ` \u2192 area <nome> - ver arquivos de uma \xE1rea
209
- `;
210
- out += ` \u2192 suggest <arquivo> - o que ler antes de editar
211
- `;
212
- out += ` \u2192 context <arquivo> - ver API de um arquivo
213
- `;
439
+ out += nextSteps("map", ctx);
214
440
  return out;
215
441
  }
216
- function formatMapText(result) {
442
+ function formatMapText(result, ctx = "cli") {
217
443
  let out = "";
218
- out += `
219
- `;
220
- out += `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
221
- `;
222
- out += `\u2551 \u{1F4C1} PROJECT MAP \u2551
223
- `;
224
- out += `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
444
+ out += `## \u{1F4C1} PROJECT MAP
225
445
 
226
446
  `;
227
447
  out += `\u{1F4CA} RESUMO
@@ -230,7 +450,7 @@ function formatMapText(result) {
230
450
  `;
231
451
  out += ` Pastas: ${result.summary.totalFolders}
232
452
  `;
233
- out += ` Diret\xF3rio: ${result.cwd}
453
+ out += ` Diretorio: ${result.cwd}
234
454
 
235
455
  `;
236
456
  out += `\u{1F4C2} CATEGORIAS
@@ -272,7 +492,7 @@ function formatMapText(result) {
272
492
  }
273
493
  if (result.circularDependencies.length > 0) {
274
494
  out += `
275
- \u26A0\uFE0F DEPEND\xCANCIAS CIRCULARES (${result.circularDependencies.length})
495
+ \u26A0\uFE0F DEPENDENCIAS CIRCULARES (${result.circularDependencies.length})
276
496
  `;
277
497
  for (const cycle of result.circularDependencies.slice(0, 5)) {
278
498
  out += ` ${cycle.join(" \u2192 ")}
@@ -283,41 +503,37 @@ function formatMapText(result) {
283
503
  `;
284
504
  }
285
505
  }
506
+ out += nextSteps("map", ctx);
286
507
  return out;
287
508
  }
288
- function formatDeadText(result) {
509
+ function formatDeadText(result, ctx = "cli") {
289
510
  let out = "";
290
- out += `
291
- `;
292
- out += `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
293
- `;
294
- out += `\u2551 \u{1F480} DEAD CODE \u2551
295
- `;
296
- out += `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
511
+ out += `## \u{1F480} DEAD CODE
297
512
 
298
513
  `;
299
514
  if (result.summary.totalDead === 0) {
300
- out += `\u2705 Nenhum c\xF3digo morto encontrado!
515
+ out += `\u2705 Nenhum codigo morto encontrado!
301
516
  `;
302
- out += ` Todos os arquivos e exports est\xE3o sendo utilizados.
517
+ out += ` Todos os arquivos e exports estao sendo utilizados.
303
518
  `;
519
+ out += nextSteps("dead", ctx);
304
520
  return out;
305
521
  }
306
522
  out += `\u{1F4CA} RESUMO
307
523
  `;
308
- out += ` Total: ${result.summary.totalDead} itens n\xE3o utilizados
524
+ out += ` Total: ${result.summary.totalDead} itens nao utilizados
309
525
  `;
310
- out += ` Arquivos \xF3rf\xE3os: ${result.summary.byType.files}
526
+ out += ` Arquivos orfaos: ${result.summary.byType.files}
311
527
  `;
312
- out += ` Exports n\xE3o usados: ${result.summary.byType.exports}
528
+ out += ` Exports nao usados: ${result.summary.byType.exports}
313
529
  `;
314
- out += ` Depend\xEAncias n\xE3o usadas: ${result.summary.byType.dependencies}
530
+ out += ` Dependencias nao usadas: ${result.summary.byType.dependencies}
315
531
 
316
532
  `;
317
533
  if (result.files.length > 0) {
318
- out += `\u{1F5D1}\uFE0F ARQUIVOS \xD3RF\xC3OS (${result.files.length})
534
+ out += `\u{1F5D1}\uFE0F ARQUIVOS ORFAOS (${result.files.length})
319
535
  `;
320
- out += ` Arquivos que ningu\xE9m importa:
536
+ out += ` Arquivos que ninguem importa:
321
537
 
322
538
  `;
323
539
  const byCategory = /* @__PURE__ */ new Map();
@@ -344,7 +560,7 @@ function formatDeadText(result) {
344
560
  }
345
561
  }
346
562
  if (result.exports.length > 0) {
347
- out += `\u{1F4E4} EXPORTS N\xC3O USADOS (${result.exports.length})
563
+ out += `\u{1F4E4} EXPORTS NAO USADOS (${result.exports.length})
348
564
  `;
349
565
  for (const exp of result.exports.slice(0, 10)) {
350
566
  out += ` ${exp.file}: ${exp.export}
@@ -358,7 +574,7 @@ function formatDeadText(result) {
358
574
  `;
359
575
  }
360
576
  if (result.dependencies.length > 0) {
361
- out += `\u{1F4E6} DEPEND\xCANCIAS N\xC3O USADAS (${result.dependencies.length})
577
+ out += `\u{1F4E6} DEPENDENCIAS NAO USADAS (${result.dependencies.length})
362
578
  `;
363
579
  for (const dep of result.dependencies) {
364
580
  out += ` ${dep}
@@ -394,12 +610,12 @@ function formatDeadText(result) {
394
610
  `;
395
611
  out += ` 3. Ver detalhes em JSON:
396
612
  `;
397
- out += ` ai-tool dead --format=json
613
+ out += ` ${hint("dead", ctx)} --format=json
398
614
  `;
399
615
  const suggestions = generateIgnoreSuggestions(result);
400
616
  if (suggestions.length > 0) {
401
617
  out += `
402
- \u{1F3AF} SUGEST\xD5ES INTELIGENTES
618
+ \u{1F3AF} SUGESTOES INTELIGENTES
403
619
 
404
620
  `;
405
621
  for (const suggestion of suggestions) {
@@ -412,6 +628,7 @@ function formatDeadText(result) {
412
628
  `;
413
629
  }
414
630
  }
631
+ out += nextSteps("dead", ctx);
415
632
  return out;
416
633
  }
417
634
  function generateIgnoreSuggestions(result) {
@@ -431,7 +648,7 @@ function generateIgnoreSuggestions(result) {
431
648
  suggestions.push({
432
649
  icon: "\u{1F9EA}",
433
650
  pattern: "**/*.(test|spec).(ts|tsx|js|jsx)",
434
- reason: "Arquivos de teste geralmente s\xE3o entry points pr\xF3prios",
651
+ reason: "Arquivos de teste geralmente sao entry points proprios",
435
652
  count: testFiles.length
436
653
  });
437
654
  }
@@ -442,7 +659,7 @@ function generateIgnoreSuggestions(result) {
442
659
  suggestions.push({
443
660
  icon: "\u2699\uFE0F",
444
661
  pattern: "**/*.config.(ts|js|mjs|cjs)",
445
- reason: "Arquivos de configura\xE7\xE3o s\xE3o entry points",
662
+ reason: "Arquivos de configuracao sao entry points",
446
663
  count: configFiles.length
447
664
  });
448
665
  }
@@ -451,7 +668,7 @@ function generateIgnoreSuggestions(result) {
451
668
  suggestions.push({
452
669
  icon: "\u{1F4D8}",
453
670
  pattern: "**/*.d.ts",
454
- reason: "Arquivos de defini\xE7\xE3o TypeScript",
671
+ reason: "Arquivos de definicao TypeScript",
455
672
  count: dtsFiles.length
456
673
  });
457
674
  }
@@ -460,21 +677,15 @@ function generateIgnoreSuggestions(result) {
460
677
  suggestions.push({
461
678
  icon: "\u{1F4DC}",
462
679
  pattern: "scripts/**",
463
- reason: "Scripts de automa\xE7\xE3o s\xE3o entry points",
680
+ reason: "Scripts de automacao sao entry points",
464
681
  count: scriptFiles.length
465
682
  });
466
683
  }
467
684
  return suggestions;
468
685
  }
469
- function formatImpactText(result) {
686
+ function formatImpactText(result, ctx = "cli") {
470
687
  let out = "";
471
- out += `
472
- `;
473
- out += `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
474
- `;
475
- out += `\u2551 \u{1F3AF} IMPACT ANALYSIS \u2551
476
- `;
477
- out += `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
688
+ out += `## \u{1F3AF} IMPACT ANALYSIS
478
689
 
479
690
  `;
480
691
  const icon = categoryIcons[result.category];
@@ -483,10 +694,7 @@ function formatImpactText(result) {
483
694
  out += ` ${icon} ${result.category}
484
695
 
485
696
  `;
486
- out += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
487
-
488
- `;
489
- out += `\u2B06\uFE0F USADO POR (${result.upstream.total} arquivo${result.upstream.total !== 1 ? "s" : ""} \xFAnico${result.upstream.total !== 1 ? "s" : ""})
697
+ out += `\u2B06\uFE0F USADO POR (${result.upstream.total} arquivo${result.upstream.total !== 1 ? "s" : ""} unico${result.upstream.total !== 1 ? "s" : ""})
490
698
  `;
491
699
  if (result.upstream.direct.length > 0 || result.upstream.indirect.length > 0) {
492
700
  out += ` \u{1F4CD} ${result.upstream.direct.length} direto${result.upstream.direct.length !== 1 ? "s" : ""} + ${result.upstream.indirect.length} indireto${result.upstream.indirect.length !== 1 ? "s" : ""}
@@ -496,7 +704,7 @@ function formatImpactText(result) {
496
704
 
497
705
  `;
498
706
  if (result.upstream.total === 0) {
499
- out += ` Ningu\xE9m importa este arquivo diretamente.
707
+ out += ` Ninguem importa este arquivo diretamente.
500
708
  `;
501
709
  } else {
502
710
  for (const file of result.upstream.direct.slice(0, 10)) {
@@ -519,10 +727,8 @@ function formatImpactText(result) {
519
727
  }
520
728
  }
521
729
  out += `
522
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
523
-
524
730
  `;
525
- out += `\u2B07\uFE0F DEPEND\xCANCIAS (${result.downstream.total} arquivo${result.downstream.total !== 1 ? "s" : ""} \xFAnico${result.downstream.total !== 1 ? "s" : ""})
731
+ out += `\u2B07\uFE0F DEPENDENCIAS (${result.downstream.total} arquivo${result.downstream.total !== 1 ? "s" : ""} unico${result.downstream.total !== 1 ? "s" : ""})
526
732
  `;
527
733
  if (result.downstream.direct.length > 0 || result.downstream.indirect.length > 0) {
528
734
  out += ` \u{1F4CD} ${result.downstream.direct.length} direto${result.downstream.direct.length !== 1 ? "s" : ""} + ${result.downstream.indirect.length} indireto${result.downstream.indirect.length !== 1 ? "s" : ""}
@@ -532,7 +738,7 @@ function formatImpactText(result) {
532
738
 
533
739
  `;
534
740
  if (result.downstream.total === 0) {
535
- out += ` Este arquivo n\xE3o importa nenhum arquivo local.
741
+ out += ` Este arquivo nao importa nenhum arquivo local.
536
742
  `;
537
743
  } else {
538
744
  for (const file of result.downstream.direct.slice(0, 10)) {
@@ -546,15 +752,13 @@ function formatImpactText(result) {
546
752
  }
547
753
  }
548
754
  out += `
549
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
550
-
551
755
  `;
552
- out += `\u{1F4CA} M\xC9TRICAS DE IMPACTO
756
+ out += `\u{1F4CA} METRICAS DE IMPACTO
553
757
 
554
758
  `;
555
- out += ` Arquivos que importam este (upstream): ${result.upstream.total} \xFAnico${result.upstream.total !== 1 ? "s" : ""}
759
+ out += ` Arquivos que importam este (upstream): ${result.upstream.total} unico${result.upstream.total !== 1 ? "s" : ""}
556
760
  `;
557
- out += ` Arquivos que este importa (downstream): ${result.downstream.total} \xFAnico${result.downstream.total !== 1 ? "s" : ""}
761
+ out += ` Arquivos que este importa (downstream): ${result.downstream.total} unico${result.downstream.total !== 1 ? "s" : ""}
558
762
  `;
559
763
  if (result.risks.length > 0) {
560
764
  out += `
@@ -569,7 +773,7 @@ function formatImpactText(result) {
569
773
  }
570
774
  if (result.suggestions.length > 0) {
571
775
  out += `
572
- \u{1F4A1} SUGEST\xD5ES
776
+ \u{1F4A1} SUGESTOES
573
777
 
574
778
  `;
575
779
  for (const suggestion of result.suggestions) {
@@ -579,11 +783,11 @@ function formatImpactText(result) {
579
783
  }
580
784
  if (result.gitHistory && result.gitHistory.hasGitRepo) {
581
785
  out += `
582
- \u{1F4DC} HIST\xD3RICO GIT (\xFAltimos ${result.gitHistory.recentCommits.length} commits)
786
+ \u{1F4DC} HISTORICO GIT (ultimos ${result.gitHistory.recentCommits.length} commits)
583
787
 
584
788
  `;
585
789
  if (result.gitHistory.recentCommits.length === 0) {
586
- out += ` Arquivo n\xE3o est\xE1 no reposit\xF3rio Git ou sem hist\xF3rico.
790
+ out += ` Arquivo nao esta no repositorio Git ou sem historico.
587
791
  `;
588
792
  } else {
589
793
  for (const commit of result.gitHistory.recentCommits) {
@@ -599,17 +803,12 @@ function formatImpactText(result) {
599
803
  }
600
804
  }
601
805
  }
806
+ out += nextSteps("impact", ctx);
602
807
  return out;
603
808
  }
604
- function formatSuggestText(result) {
809
+ function formatSuggestText(result, ctx = "cli") {
605
810
  let out = "";
606
- out += `
607
- `;
608
- out += `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
609
- `;
610
- out += `\u2551 \u{1F4DA} SUGGEST \u2551
611
- `;
612
- out += `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
811
+ out += `## \u{1F4DA} SUGGEST
613
812
 
614
813
  `;
615
814
  const icon = categoryIcons[result.category];
@@ -623,11 +822,9 @@ function formatSuggestText(result) {
623
822
  `;
624
823
  out += ` Este arquivo nao tem dependencias ou arquivos relacionados.
625
824
  `;
825
+ out += nextSteps("suggest", ctx);
626
826
  return out;
627
827
  }
628
- out += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
629
-
630
- `;
631
828
  const byPriority = {
632
829
  critical: result.suggestions.filter((s) => s.priority === "critical"),
633
830
  high: result.suggestions.filter((s) => s.priority === "high"),
@@ -698,9 +895,6 @@ function formatSuggestText(result) {
698
895
  out += `
699
896
  `;
700
897
  }
701
- out += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
702
-
703
- `;
704
898
  out += `\u{1F4CA} RESUMO
705
899
  `;
706
900
  out += ` Total de arquivos sugeridos: ${result.suggestions.length}
@@ -723,10 +917,7 @@ function formatSuggestText(result) {
723
917
  }
724
918
  if (result.testSuggestions.length > 0) {
725
919
  out += `
726
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
727
-
728
- `;
729
- out += `\u{1F9EA} TESTES E VERIFICA\xC7\xD5ES
920
+ \u{1F9EA} TESTES E VERIFICACOES
730
921
 
731
922
  `;
732
923
  for (const testSuggestion of result.testSuggestions) {
@@ -734,17 +925,12 @@ function formatSuggestText(result) {
734
925
  `;
735
926
  }
736
927
  }
928
+ out += nextSteps("suggest", ctx);
737
929
  return out;
738
930
  }
739
- function formatContextText(result) {
931
+ function formatContextText(result, ctx = "cli") {
740
932
  let out = "";
741
- out += `
742
- `;
743
- out += `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
744
- `;
745
- out += `\u2551 \u{1F4C4} CONTEXT \u2551
746
- `;
747
- out += `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
933
+ out += `## \u{1F4C4} CONTEXT
748
934
 
749
935
  `;
750
936
  const icon = categoryIcons[result.category];
@@ -752,9 +938,6 @@ function formatContextText(result) {
752
938
  `;
753
939
  out += ` ${icon} ${result.category}
754
940
 
755
- `;
756
- out += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
757
-
758
941
  `;
759
942
  if (result.imports.length > 0) {
760
943
  out += `\u{1F4E5} IMPORTS (${result.imports.length})
@@ -798,10 +981,10 @@ function formatContextText(result) {
798
981
  `;
799
982
  for (const fn of result.functions) {
800
983
  const exported = fn.isExported ? "export " : "";
801
- const async = fn.isAsync ? "async " : "";
984
+ const asyncLabel = fn.isAsync ? "async " : "";
802
985
  const arrow = fn.isArrowFunction ? " =>" : "";
803
986
  const params = fn.params.map((p) => `${p.name}: ${p.type}`).join(", ");
804
- out += ` ${exported}${async}${fn.name}(${params})${arrow}: ${fn.returnType}
987
+ out += ` ${exported}${asyncLabel}${fn.name}(${params})${arrow}: ${fn.returnType}
805
988
  `;
806
989
  if (fn.jsdoc) {
807
990
  out += ` /** ${fn.jsdoc} */
@@ -811,9 +994,18 @@ function formatContextText(result) {
811
994
  `;
812
995
  }
813
996
  }
814
- out += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
997
+ if (result.constants && result.constants.length > 0) {
998
+ out += `\u{1F4CC} CONSTANTS (${result.constants.length})
815
999
 
816
1000
  `;
1001
+ for (const c of result.constants) {
1002
+ const exported = c.isExported ? "export " : "";
1003
+ out += ` ${exported}${c.name}: ${c.type}
1004
+ `;
1005
+ }
1006
+ out += `
1007
+ `;
1008
+ }
817
1009
  out += `\u{1F4CA} RESUMO
818
1010
  `;
819
1011
  out += ` Imports: ${result.imports.length}
@@ -824,35 +1016,31 @@ function formatContextText(result) {
824
1016
  `;
825
1017
  out += ` Functions: ${result.functions.length}
826
1018
  `;
1019
+ if (result.constants && result.constants.length > 0) {
1020
+ out += ` Constants: ${result.constants.length}
1021
+ `;
1022
+ }
1023
+ out += nextSteps("context", ctx);
827
1024
  return out;
828
1025
  }
829
- function formatAreasText(result) {
1026
+ function formatAreasText(result, ctx = "cli") {
830
1027
  let out = "";
831
- out += `
832
- `;
833
- out += `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
834
- `;
835
- out += `\u2551 \u{1F4E6} PROJECT AREAS \u2551
836
- `;
837
- out += `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1028
+ out += `## \u{1F4E6} PROJECT AREAS
838
1029
 
839
1030
  `;
840
1031
  out += `\u{1F4CA} RESUMO
841
1032
  `;
842
- out += ` \xC1reas: ${result.summary.totalAreas}
1033
+ out += ` Areas: ${result.summary.totalAreas}
843
1034
  `;
844
1035
  out += ` Arquivos: ${result.summary.totalFiles}
845
1036
  `;
846
1037
  if (result.summary.unmappedCount > 0) {
847
- out += ` \u26A0\uFE0F Sem \xE1rea: ${result.summary.unmappedCount}
1038
+ out += ` \u26A0\uFE0F Sem area: ${result.summary.unmappedCount}
848
1039
  `;
849
1040
  }
850
1041
  out += `
851
1042
  `;
852
- out += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
853
-
854
- `;
855
- out += `\u{1F4E6} \xC1REAS DETECTADAS
1043
+ out += `\u{1F4E6} AREAS DETECTADAS
856
1044
 
857
1045
  `;
858
1046
  for (const area2 of result.areas) {
@@ -872,15 +1060,12 @@ function formatAreasText(result) {
872
1060
  `;
873
1061
  }
874
1062
  if (result.unmapped.length > 0) {
875
- out += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
876
-
877
- `;
878
- out += `\u26A0\uFE0F ARQUIVOS SEM \xC1REA (${result.unmapped.length})
1063
+ out += `\u26A0\uFE0F ARQUIVOS SEM AREA (${result.unmapped.length})
879
1064
 
880
1065
  `;
881
1066
  for (const file of result.unmapped.slice(0, 10)) {
882
- const icon = categoryIcons[file.category];
883
- out += ` ${icon} ${file.path}
1067
+ const fileIcon = categoryIcons[file.category];
1068
+ out += ` ${fileIcon} ${file.path}
884
1069
  `;
885
1070
  }
886
1071
  if (result.unmapped.length > 10) {
@@ -888,32 +1073,19 @@ function formatAreasText(result) {
888
1073
  `;
889
1074
  }
890
1075
  out += `
891
- \u{1F4A1} Adicione padr\xF5es em .analyze/areas.config.json
1076
+ \u{1F4A1} Adicione padroes em .analyze/areas.config.json
892
1077
  `;
893
- out += ` ou execute 'ai-tool areas init' para gerar configura\xE7\xE3o
1078
+ out += ` ou execute ${hint("areas_init", ctx)} para gerar configuracao
894
1079
  `;
895
1080
  }
896
- out += `
897
- \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
898
-
899
- `;
900
- out += `\u{1F4A1} Use 'ai-tool area <nome>' para ver detalhes de uma \xE1rea
901
- `;
902
- out += ` Exemplo: ai-tool area auth
903
- `;
1081
+ out += nextSteps("areas", ctx);
904
1082
  return out;
905
1083
  }
906
- function formatAreaDetailText(result, options = {}) {
1084
+ function formatAreaDetailText(result, options = {}, ctx = "cli") {
907
1085
  const { full = false, filterType } = options;
908
1086
  const { area: area2, byCategory } = result;
909
1087
  let out = "";
910
- out += `
911
- `;
912
- out += `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
913
- `;
914
- out += `\u2551 \u{1F4E6} AREA DETAIL \u2551
915
- `;
916
- out += `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1088
+ out += `## \u{1F4E6} AREA DETAIL
917
1089
 
918
1090
  `;
919
1091
  out += `\u{1F4E6} ${area2.name}
@@ -951,9 +1123,6 @@ function formatAreaDetailText(result, options = {}) {
951
1123
  out += catParts.join(" ");
952
1124
  out += `
953
1125
 
954
- `;
955
- out += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
956
-
957
1126
  `;
958
1127
  if (filterType) {
959
1128
  const files = byCategory[filterType] || [];
@@ -965,6 +1134,7 @@ function formatAreaDetailText(result, options = {}) {
965
1134
  out += ` ${file.path}${desc}
966
1135
  `;
967
1136
  }
1137
+ out += nextSteps("area", ctx);
968
1138
  return out;
969
1139
  }
970
1140
  const maxFilesPerCategory = full ? 100 : 8;
@@ -996,17 +1166,12 @@ function formatAreaDetailText(result, options = {}) {
996
1166
  out += `\u{1F4A1} Use --full para ver todos os arquivos
997
1167
  `;
998
1168
  }
1169
+ out += nextSteps("area", ctx);
999
1170
  return out;
1000
1171
  }
1001
- function formatFindText(result) {
1172
+ function formatFindText(result, ctx = "cli", symbolNames) {
1002
1173
  let out = "";
1003
- out += `
1004
- `;
1005
- out += `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
1006
- `;
1007
- out += `\u2551 \u{1F50D} FIND \u2551
1008
- `;
1009
- out += `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1174
+ out += `## \u{1F50D} FIND
1010
1175
 
1011
1176
  `;
1012
1177
  if (result.query) {
@@ -1021,7 +1186,7 @@ function formatFindText(result) {
1021
1186
 
1022
1187
  `;
1023
1188
  } else {
1024
- out += `\u{1F4CB} Listando todos os s\xEDmbolos do tipo: ${result.filters.type || "all"}
1189
+ out += `\u{1F4CB} Listando todos os simbolos do tipo: ${result.filters.type || "all"}
1025
1190
 
1026
1191
  `;
1027
1192
  }
@@ -1031,33 +1196,45 @@ function formatFindText(result) {
1031
1196
 
1032
1197
  `;
1033
1198
  } else {
1034
- out += `\u274C Nenhum s\xEDmbolo do tipo "${result.filters.type}" encontrado
1199
+ out += `\u274C Nenhum simbolo do tipo "${result.filters.type}" encontrado
1035
1200
 
1036
1201
  `;
1037
1202
  }
1203
+ if (symbolNames && symbolNames.length > 0 && result.query) {
1204
+ const similar = findSimilar(result.query, symbolNames, { maxDistance: 3, limit: 5 });
1205
+ if (similar.length > 0) {
1206
+ out += `\u{1F4A1} Voce quis dizer?
1207
+ `;
1208
+ for (const name of similar) {
1209
+ out += ` \u2192 ${hint("find", ctx, { "<termo>": name })}
1210
+ `;
1211
+ }
1212
+ out += `
1213
+ `;
1214
+ }
1215
+ }
1038
1216
  out += `\u{1F4A1} Dicas:
1039
1217
  `;
1040
- out += ` \u2022 Verifique a ortografia
1218
+ out += ` \u2192 Verifique a ortografia
1041
1219
  `;
1042
- out += ` \u2022 Tente buscar parte do nome
1220
+ out += ` \u2192 Tente buscar parte do nome
1043
1221
  `;
1044
- out += ` \u2022 Remova filtros de tipo ou \xE1rea
1222
+ out += ` \u2192 Remova filtros de tipo ou area
1223
+ `;
1224
+ out += ` \u2192 ${hint("describe", ctx)} - buscar areas por descricao
1045
1225
  `;
1046
1226
  return out;
1047
1227
  }
1048
- out += `\u{1F4CA} ${result.summary.definitions} defini\xE7\xE3o, ${result.summary.references} refer\xEAncias em ${result.summary.files} arquivos
1049
-
1050
- `;
1051
- out += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
1228
+ out += `\u{1F4CA} ${result.summary.definitions} definicao, ${result.summary.references} referencias em ${result.summary.files} arquivos
1052
1229
 
1053
1230
  `;
1054
1231
  if (result.definition) {
1055
- out += `\u{1F4CD} DEFINI\xC7\xC3O
1232
+ out += `\u{1F4CD} DEFINICAO
1056
1233
 
1057
1234
  `;
1058
1235
  const def = result.definition;
1059
- const icon = categoryIcons[def.category];
1060
- out += ` ${icon} ${def.file}:${def.line}
1236
+ const defIcon = categoryIcons[def.category];
1237
+ out += ` ${defIcon} ${def.file}:${def.line}
1061
1238
  `;
1062
1239
  out += ` ${def.code}
1063
1240
  `;
@@ -1072,8 +1249,8 @@ function formatFindText(result) {
1072
1249
 
1073
1250
  `;
1074
1251
  for (const ref of imports.slice(0, 10)) {
1075
- const icon = categoryIcons[ref.category];
1076
- out += ` ${icon} ${ref.file}:${ref.line}
1252
+ const refIcon = categoryIcons[ref.category];
1253
+ out += ` ${refIcon} ${ref.file}:${ref.line}
1077
1254
  `;
1078
1255
  out += ` ${ref.code}
1079
1256
  `;
@@ -1090,8 +1267,8 @@ function formatFindText(result) {
1090
1267
 
1091
1268
  `;
1092
1269
  for (const ref of usages.slice(0, 15)) {
1093
- const icon = categoryIcons[ref.category];
1094
- out += ` ${icon} ${ref.file}:${ref.line}
1270
+ const refIcon = categoryIcons[ref.category];
1271
+ out += ` ${refIcon} ${ref.file}:${ref.line}
1095
1272
  `;
1096
1273
  out += ` ${ref.code}
1097
1274
  `;
@@ -1106,15 +1283,9 @@ function formatFindText(result) {
1106
1283
  }
1107
1284
  return out;
1108
1285
  }
1109
- function formatAreaContextText(result) {
1286
+ function formatAreaContextText(result, ctx = "cli") {
1110
1287
  let out = "";
1111
- out += `
1112
- `;
1113
- out += `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
1114
- `;
1115
- out += `\u2551 \u{1F4E6} AREA CONTEXT \u2551
1116
- `;
1117
- out += `\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1288
+ out += `## \u{1F4E6} AREA CONTEXT
1118
1289
 
1119
1290
  `;
1120
1291
  out += `\u{1F4E6} ${result.area.name} - Contexto Consolidado (${result.area.fileCount} arquivos)
@@ -1124,9 +1295,6 @@ function formatAreaContextText(result) {
1124
1295
  `;
1125
1296
  }
1126
1297
  out += `
1127
- `;
1128
- out += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
1129
-
1130
1298
  `;
1131
1299
  if (result.types.length > 0) {
1132
1300
  out += `\u{1F3F7}\uFE0F TYPES (${result.types.length})
@@ -1232,9 +1400,6 @@ function formatAreaContextText(result) {
1232
1400
  `;
1233
1401
  }
1234
1402
  }
1235
- out += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
1236
-
1237
- `;
1238
1403
  out += `\u{1F4CA} RESUMO
1239
1404
  `;
1240
1405
  out += ` Types: ${result.types.length}
@@ -1253,6 +1418,7 @@ function formatAreaContextText(result) {
1253
1418
  out += ` Triggers: ${result.triggers.length}
1254
1419
  `;
1255
1420
  }
1421
+ out += nextSteps("area_context", ctx);
1256
1422
  return out;
1257
1423
  }
1258
1424
 
@@ -2091,69 +2257,6 @@ ${output}`;
2091
2257
  // src/commands/impact.ts
2092
2258
  import skott2 from "skott";
2093
2259
 
2094
- // src/utils/similarity.ts
2095
- function levenshteinDistance(a, b) {
2096
- const matrix = [];
2097
- for (let i = 0; i <= b.length; i++) {
2098
- matrix[i] = [i];
2099
- }
2100
- for (let j = 0; j <= a.length; j++) {
2101
- matrix[0][j] = j;
2102
- }
2103
- for (let i = 1; i <= b.length; i++) {
2104
- for (let j = 1; j <= a.length; j++) {
2105
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
2106
- matrix[i][j] = matrix[i - 1][j - 1];
2107
- } else {
2108
- matrix[i][j] = Math.min(
2109
- matrix[i - 1][j - 1] + 1,
2110
- // substituição
2111
- matrix[i][j - 1] + 1,
2112
- // inserção
2113
- matrix[i - 1][j] + 1
2114
- // deleção
2115
- );
2116
- }
2117
- }
2118
- }
2119
- return matrix[b.length][a.length];
2120
- }
2121
- function findSimilar(target, candidates, options = {}) {
2122
- const {
2123
- maxDistance = 3,
2124
- limit = 5,
2125
- normalize: normalize2 = true,
2126
- extractKey = (item) => String(item)
2127
- } = options;
2128
- const normalizedTarget = normalize2 ? target.toLowerCase() : target;
2129
- const scored = candidates.map((item) => {
2130
- const key = extractKey(item);
2131
- const normalizedKey = normalize2 ? key.toLowerCase() : key;
2132
- const keyNoExt = normalizedKey.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2133
- let score = Infinity;
2134
- if (normalizedKey.includes(normalizedTarget) || keyNoExt.includes(normalizedTarget)) {
2135
- score = 0;
2136
- } else {
2137
- score = levenshteinDistance(keyNoExt, normalizedTarget);
2138
- }
2139
- return { item, score };
2140
- }).filter(({ score }) => score <= maxDistance).sort((a, b) => a.score - b.score).slice(0, limit);
2141
- return scored.map(({ item }) => item);
2142
- }
2143
- function findBestMatch(target, candidates, extractKey = (item) => String(item)) {
2144
- const similar = findSimilar(target, candidates, {
2145
- maxDistance: 2,
2146
- // Mais restritivo para "você quis dizer"
2147
- limit: 1,
2148
- extractKey
2149
- });
2150
- return similar.length > 0 ? similar[0] : null;
2151
- }
2152
- function extractFileName(filePath) {
2153
- const fileName = filePath.split("/").pop() || filePath;
2154
- return fileName.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2155
- }
2156
-
2157
2260
  // src/utils/errors.ts
2158
2261
  var COMMAND_REFERENCE = {
2159
2262
  map: "Resumo do projeto (sem target)",
@@ -2919,6 +3022,7 @@ function generateTestSuggestions(suggestions, allFiles) {
2919
3022
  // src/commands/context.ts
2920
3023
  import { existsSync as existsSync6, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
2921
3024
  import { join as join6, resolve as resolve2, basename, extname as extname3 } from "path";
3025
+ import { SyntaxKind as SyntaxKind3 } from "ts-morph";
2922
3026
 
2923
3027
  // src/ts/extractor.ts
2924
3028
  import { Project, SyntaxKind } from "ts-morph";
@@ -3800,6 +3904,24 @@ async function context(target, options = {}) {
3800
3904
  const functions2 = extractFunctions(sourceFile);
3801
3905
  const types = extractTypes(sourceFile);
3802
3906
  const exports = extractExports(sourceFile);
3907
+ const constants = [];
3908
+ for (const varStatement of sourceFile.getVariableStatements()) {
3909
+ if (!varStatement.isExported()) continue;
3910
+ for (const decl of varStatement.getDeclarations()) {
3911
+ const init = decl.getInitializer();
3912
+ if (init) {
3913
+ const kind = init.getKind();
3914
+ if (kind === SyntaxKind3.ArrowFunction || kind === SyntaxKind3.FunctionExpression) {
3915
+ continue;
3916
+ }
3917
+ }
3918
+ constants.push({
3919
+ name: decl.getName(),
3920
+ type: simplifyType(decl.getType().getText()),
3921
+ isExported: true
3922
+ });
3923
+ }
3924
+ }
3803
3925
  const result = {
3804
3926
  version: "1.0.0",
3805
3927
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3808,7 +3930,8 @@ async function context(target, options = {}) {
3808
3930
  imports,
3809
3931
  exports,
3810
3932
  functions: functions2,
3811
- types
3933
+ types,
3934
+ constants: constants.length > 0 ? constants : void 0
3812
3935
  };
3813
3936
  return formatOutput(result, format, formatContextText);
3814
3937
  } catch (error) {
@@ -3927,9 +4050,19 @@ async function areaContext(areaName, options = {}) {
3927
4050
  }
3928
4051
  }
3929
4052
  if (areaFiles.length === 0) {
3930
- return format === "json" ? JSON.stringify({ error: `\xC1rea n\xE3o encontrada: "${areaName}"` }) : `\u274C \xC1rea n\xE3o encontrada: "${areaName}"
3931
-
3932
- \u{1F4A1} Use 'ai-tool areas' para listar \xE1reas dispon\xEDveis`;
4053
+ const availableAreas = /* @__PURE__ */ new Map();
4054
+ for (const filePath of Object.keys(index.files)) {
4055
+ if (isFileIgnored(filePath, config)) continue;
4056
+ const fileAreas = detectFileAreas(filePath, config);
4057
+ for (const areaId of fileAreas) {
4058
+ availableAreas.set(areaId, (availableAreas.get(areaId) || 0) + 1);
4059
+ }
4060
+ }
4061
+ const areaList = [...availableAreas.entries()].map(([id, count]) => ({ id, count })).sort((a, b) => b.count - a.count);
4062
+ if (format === "json") {
4063
+ return JSON.stringify({ error: `Area nao encontrada: "${areaName}"`, availableAreas: areaList });
4064
+ }
4065
+ return formatAreaNotFound({ target: areaName, availableAreas: areaList });
3933
4066
  }
3934
4067
  const types = [];
3935
4068
  const hooks = [];
@@ -4682,12 +4815,40 @@ async function functions(options = {}) {
4682
4815
  const useCache = options.cache !== false;
4683
4816
  const filterTrigger = options.trigger;
4684
4817
  if (!isFirebaseProject(cwd)) {
4685
- const errorMsg = "Este n\xE3o \xE9 um projeto Firebase.\nN\xE3o foi encontrado .firebaserc ou firebase.json";
4686
- return format === "json" ? JSON.stringify({ error: errorMsg }) : `\u274C ${errorMsg}`;
4818
+ const errorMsg = "Este nao e um projeto Firebase (nao encontrou .firebaserc ou firebase.json)";
4819
+ if (format === "json") {
4820
+ return JSON.stringify({ error: errorMsg });
4821
+ }
4822
+ let out = `\u274C ${errorMsg}
4823
+ `;
4824
+ out += `
4825
+ \u{1F4A1} Comandos disponiveis para este projeto:
4826
+ `;
4827
+ out += ` \u2192 ai-tool map - ver estrutura do projeto
4828
+ `;
4829
+ out += ` \u2192 ai-tool find <termo> - buscar simbolos no codigo
4830
+ `;
4831
+ out += ` \u2192 ai-tool areas - listar areas funcionais
4832
+ `;
4833
+ return out;
4687
4834
  }
4688
4835
  if (!hasFirebaseFunctions(cwd)) {
4689
- const errorMsg = "Projeto Firebase sem Cloud Functions.\nN\xE3o foi encontrado functions/src/index.ts";
4690
- return format === "json" ? JSON.stringify({ error: errorMsg }) : `\u274C ${errorMsg}`;
4836
+ const errorMsg = "Projeto Firebase detectado, mas sem Cloud Functions";
4837
+ if (format === "json") {
4838
+ return JSON.stringify({ error: errorMsg });
4839
+ }
4840
+ let out = `\u274C ${errorMsg}
4841
+ `;
4842
+ out += ` Nao foi encontrado functions/src/index.ts
4843
+
4844
+ `;
4845
+ out += `\u{1F4A1} Para adicionar Cloud Functions:
4846
+ `;
4847
+ out += ` \u2192 firebase init functions
4848
+ `;
4849
+ out += ` \u2192 Documentacao: https://firebase.google.com/docs/functions
4850
+ `;
4851
+ return out;
4691
4852
  }
4692
4853
  try {
4693
4854
  let index;
@@ -4946,7 +5107,19 @@ async function find(query, options = {}) {
4946
5107
  }
4947
5108
  }
4948
5109
  if (allowedFiles.size === 0) {
4949
- return format === "json" ? JSON.stringify({ error: `Nenhum arquivo encontrado na \xE1rea "${filterArea}"` }) : `\u274C Nenhum arquivo encontrado na \xE1rea "${filterArea}"`;
5110
+ const availableAreas = /* @__PURE__ */ new Map();
5111
+ for (const filePath of Object.keys(index.files)) {
5112
+ if (isFileIgnored(filePath, config)) continue;
5113
+ const fileAreas = detectFileAreas(filePath, config);
5114
+ for (const areaId of fileAreas) {
5115
+ availableAreas.set(areaId, (availableAreas.get(areaId) || 0) + 1);
5116
+ }
5117
+ }
5118
+ const areaList = [...availableAreas.entries()].map(([id, count]) => ({ id, count })).sort((a, b) => b.count - a.count);
5119
+ if (format === "json") {
5120
+ return JSON.stringify({ error: `Nenhum arquivo encontrado na area "${filterArea}"`, availableAreas: areaList });
5121
+ }
5122
+ return formatAreaNotFound({ target: filterArea, availableAreas: areaList });
4950
5123
  }
4951
5124
  }
4952
5125
  const matches = searchInIndex(index, query, filterType, allowedFiles);
@@ -4991,7 +5164,8 @@ async function find(query, options = {}) {
4991
5164
  },
4992
5165
  fromCache
4993
5166
  };
4994
- return formatOutput(result, format, formatFindText, fromCache);
5167
+ const allSymbolNames = Object.keys(index.symbolsByName);
5168
+ return formatOutput(result, format, (r) => formatFindText(r, "cli", allSymbolNames), fromCache);
4995
5169
  } catch (error) {
4996
5170
  const message = error instanceof Error ? error.message : String(error);
4997
5171
  throw new Error(`Erro ao executar find: ${message}`);
@@ -5118,6 +5292,13 @@ export {
5118
5292
  categoryIcons,
5119
5293
  isEntryPoint,
5120
5294
  isCodeFile,
5295
+ hint,
5296
+ nextSteps,
5297
+ recoveryHint,
5298
+ levenshteinDistance,
5299
+ findSimilar,
5300
+ findBestMatch,
5301
+ extractFileName,
5121
5302
  isFirebaseProject,
5122
5303
  hasFirebaseFunctions,
5123
5304
  isExportedCloudFunction,
@@ -5125,7 +5306,10 @@ export {
5125
5306
  clearFirebaseCache,
5126
5307
  getCacheDir,
5127
5308
  isCacheValid,
5309
+ updateCacheMeta,
5128
5310
  invalidateCache,
5311
+ cacheSymbolsIndex,
5312
+ getCachedSymbolsIndex,
5129
5313
  configExists,
5130
5314
  readConfig,
5131
5315
  writeConfig,
@@ -5143,10 +5327,6 @@ export {
5143
5327
  map,
5144
5328
  dead,
5145
5329
  deadFix,
5146
- levenshteinDistance,
5147
- findSimilar,
5148
- findBestMatch,
5149
- extractFileName,
5150
5330
  COMMAND_REFERENCE,
5151
5331
  formatFileNotFound,
5152
5332
  formatAreaNotFound,
@@ -5154,6 +5334,8 @@ export {
5154
5334
  formatInvalidCommand,
5155
5335
  impact,
5156
5336
  suggest,
5337
+ getAllCodeFiles,
5338
+ indexProject,
5157
5339
  context,
5158
5340
  areaContext,
5159
5341
  areas,