@mocoto/mahoraga 0.14.2 → 0.14.3

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 (105) hide show
  1. package/README.md +0 -34
  2. package/dist/analysts/architects/analyst-structure.js +19 -19
  3. package/dist/analysts/architects/archetypes-custom.js +8 -6
  4. package/dist/analysts/architects/detector-archetypes.js +30 -27
  5. package/dist/analysts/architects/signals-project-advanced.js +24 -26
  6. package/dist/analysts/architects/signals-project.js +1 -1
  7. package/dist/analysts/architects/strategist-structure.js +47 -66
  8. package/dist/analysts/azure-pipelines/analysts/analyst-azure.js +21 -14
  9. package/dist/analysts/azure-pipelines/corrections/correction-azure.js +22 -19
  10. package/dist/analysts/azure-pipelines/detectors/detector-azure-performance.js +21 -18
  11. package/dist/analysts/azure-pipelines/detectors/detector-azure-security.js +25 -20
  12. package/dist/analysts/azure-pipelines/detectors/detector-azure-structure.js +39 -37
  13. package/dist/analysts/azure-pipelines/scorers/scorer-azure.js +10 -7
  14. package/dist/analysts/circleci/analysts/analyst-circleci.js +23 -15
  15. package/dist/analysts/circleci/corrections/correction-circleci.js +22 -20
  16. package/dist/analysts/circleci/detectors/detector-circleci-performance.js +23 -19
  17. package/dist/analysts/circleci/detectors/detector-circleci-security.js +78 -82
  18. package/dist/analysts/circleci/detectors/detector-circleci-structure.js +20 -17
  19. package/dist/analysts/converters/engines/converter-gha-to-gitlab.js +17 -15
  20. package/dist/analysts/corrections/analyst-quick-fixes.js +25 -28
  21. package/dist/analysts/corrections/analyst-scoring.js +24 -21
  22. package/dist/analysts/corrections/corrector-structure.js +35 -35
  23. package/dist/analysts/corrections/fix-alias-imports.js +10 -6
  24. package/dist/analysts/corrections/fix-md-fences.js +23 -23
  25. package/dist/analysts/corrections/map-reversion.js +54 -51
  26. package/dist/analysts/corrections/pruning.js +19 -16
  27. package/dist/analysts/corrections/quick-fixes/fix-any-to-proper-type.js +57 -61
  28. package/dist/analysts/corrections/quick-fixes/fix-unknown-to-specific-type.js +30 -30
  29. package/dist/analysts/corrections/reorganize-barrels.js +10 -10
  30. package/dist/analysts/corrections/scoring-shared.js +19 -18
  31. package/dist/analysts/corrections/type-safety/context-analyzer.js +15 -10
  32. package/dist/analysts/corrections/type-safety/type-validator.js +2 -1
  33. package/dist/analysts/corrections/type-safety/usage-analyzer.js +36 -39
  34. package/dist/analysts/css/analysts/analyst-css.js +16 -15
  35. package/dist/analysts/css/corrections/correction-css.js +19 -11
  36. package/dist/analysts/css/detectors/detector-css-best-practices.js +129 -118
  37. package/dist/analysts/css/scorers/scorer-css.js +23 -18
  38. package/dist/analysts/css-in-js/analysts/analyst-css-in-js.js +24 -17
  39. package/dist/analysts/css-in-js/corrections/correction-css-in-js.js +31 -16
  40. package/dist/analysts/css-in-js/detectors/detector-css-in-js-best-practices.js +20 -15
  41. package/dist/analysts/css-in-js/scorers/scorer-css-in-js.js +79 -54
  42. package/dist/analysts/css-in-js/shared.js +7 -0
  43. package/dist/analysts/detectors/detector-architecture.js +89 -83
  44. package/dist/analysts/detectors/detector-bugs-ml.js +21 -19
  45. package/dist/analysts/detectors/detector-code-fragile.js +30 -26
  46. package/dist/analysts/detectors/detector-duplications.js +21 -18
  47. package/dist/analysts/detectors/detector-monorepo.js +41 -36
  48. package/dist/analysts/detectors/detector-performance.js +22 -16
  49. package/dist/analysts/detectors/detector-recommendation-architecture.js +39 -35
  50. package/dist/analysts/detectors/detector-security.js +43 -42
  51. package/dist/analysts/detectors/detector-structure.js +29 -26
  52. package/dist/analysts/js-ts/detectors/detector-anti-patterns-async.js +4 -1
  53. package/dist/analysts/js-ts/detectors/detector-constructions-syntactic.js +10 -16
  54. package/dist/analysts/plugins/detector-documentation.js +1 -0
  55. package/dist/analysts/react/analysts/analyst-react-hooks.js +4 -2
  56. package/dist/analysts/react/analysts/analyst-react.js +1 -1
  57. package/dist/analysts/react/corrections/correction-react.js +2 -2
  58. package/dist/analysts/react/detectors/detector-react-best-practices.js +1 -1
  59. package/dist/analysts/registry/autodiscovery.js +2 -2
  60. package/dist/analysts/tailwind/corrections/correction-tailwind.js +0 -8
  61. package/dist/analysts/tailwind/scorers/scorer-tailwind.js +1 -2
  62. package/dist/cli/commands/command-analysts.js +3 -2
  63. package/dist/cli/commands/command-formatters.js +1 -1
  64. package/dist/cli/commands/command-github-actions.js +1 -1
  65. package/dist/cli/commands/command-guardian.js +1 -1
  66. package/dist/cli/commands/command-marketplace.js +4 -4
  67. package/dist/cli/diagnostic/filters.js +9 -9
  68. package/dist/core/config/chalk-safe.js +4 -4
  69. package/dist/core/config/config.js +5 -5
  70. package/dist/core/execution/analysis-cache.js +1 -1
  71. package/dist/core/execution/ast-cache.js +1 -3
  72. package/dist/core/execution/executor.js +2 -4
  73. package/dist/core/execution/scanner.js +1 -1
  74. package/dist/core/messages/en/analysts/report-circleci-messages.js +7 -7
  75. package/dist/core/messages/en/caretakers/caretaker-messages.js +1 -7
  76. package/dist/core/messages/ja/analysts/report-circleci-messages.js +7 -7
  77. package/dist/core/messages/ja/caretakers/caretaker-messages.js +1 -7
  78. package/dist/core/messages/pt/analysts/report-circleci-messages.js +7 -7
  79. package/dist/core/messages/pt/caretakers/caretaker-messages.js +1 -7
  80. package/dist/core/messages/zh/analysts/report-circleci-messages.js +7 -7
  81. package/dist/core/messages/zh/caretakers/caretaker-messages.js +1 -7
  82. package/dist/core/parsing/parser.js +2 -3
  83. package/dist/core/parsing/utils.js +1 -0
  84. package/dist/core/workers/worker-pool.js +0 -0
  85. package/dist/guardian/records.js +1 -0
  86. package/dist/guardian/sentinel.js +1 -0
  87. package/dist/guardian/watcher-hidden.js +1 -0
  88. package/dist/licenses/fs-utils.js +2 -1
  89. package/dist/licenses/header-options.js +1 -0
  90. package/dist/licenses/index.js +1 -1
  91. package/dist/reports/generator-report.js +3 -3
  92. package/dist/reports/report-patterns-usage.js +1 -1
  93. package/dist/shared/data-processing/fragment-report.js +1 -1
  94. package/dist/shared/formatters/engines/stylelint.js +40 -34
  95. package/dist/shared/formatters/formatters/markdown.js +1 -1
  96. package/dist/shared/helpers/framework-detector.js +5 -1
  97. package/dist/shared/persistence/persistence.js +13 -5
  98. package/dist/shared/plugins/core-plugin.js +3 -1
  99. package/dist/types/analysts/tailwind-correction.js +2 -0
  100. package/dist/types/index.js +3 -3
  101. package/dist/types/processing/filters.js +5 -4
  102. package/dist/vulnerabilities/npm-audit.js +1 -0
  103. package/dist/vulnerabilities/scanner.js +1 -1
  104. package/dist/vulnerabilities/vulnerabilities.js +1 -0
  105. package/package.json +5 -10
package/README.md CHANGED
@@ -2,46 +2,12 @@
2
2
  Proveniência e Autoria: Este documento integra o projeto Mahoraga (licença MIT-0).
3
3
  ---
4
4
 
5
- <img src="./svg/banner-mahoraga.svg" width="100%">
6
-
7
- ---
8
-
9
5
  <div align="center">
10
6
 
11
- ![beta](./svg/badge-beta.svg)
12
- ![cobertura](./svg/badge-coverage.svg)
13
- ![mantido](./svg/badge-maintained.svg)
14
- ![pr-bem-vindo](./svg/badge-prs-welcome.svg)
15
- ![stavel](./svg/badge-stable.svg)
16
- ---
17
-
18
7
  <p><b>CLI modular para análise, diagnóstico e manutenção de projetos JavaScript/TypeScript com suporte multi-linguagem</b><p>
19
8
 
20
9
  </div>
21
10
 
22
- ---
23
-
24
- ## Índice
25
-
26
- - [Começar Rápido (30 segundos)](#começar-rápido-30-segundos)
27
- - [Fluxo Passo a Passo](#fluxo-passo-a-passo)
28
- - [Funcionalidades](#funcionalidades)
29
- - [Instalação](#instalação)
30
- - [Configuração](#configuração)
31
- - [Comandos da CLI](#comandos-da-cli)
32
- - [REST API](#rest-api)
33
- - [SDK Programático](#sdk-programático)
34
- - [GitHub App](#github-app)
35
- - [Editor (LSP / VSCode)](#editor-lsp--vscode)
36
- - [CI/CD](#cicd)
37
- - [Linguagens Suportadas](#linguagens-suportadas)
38
- - [Documentação Completa](#documentação-completa)
39
- - [Desenvolvimento](#desenvolvimento)
40
- - [Contribuir](#contribuir)
41
- - [Licença](#licença)
42
-
43
- ---
44
-
45
11
  ## Começar Rápido (30 segundos)
46
12
 
47
13
  ```bash
@@ -17,27 +17,27 @@ export const CAMADAS = {
17
17
  yaml: 'config',
18
18
  // Adicione mais mapeamentos conforme necessário
19
19
  };
20
+ function determinarCamadaIdeal(entry) {
21
+ const rel = entry.relPath;
22
+ const normalizado = rel.replace(/\\/g, '/');
23
+ const atual = normalizado.split('/')[0] || '';
24
+ let ideal = null;
25
+ const matchDireta = Object.entries(CAMADAS).find(([, dir]) => normalizado.startsWith(`${dir.replace(/\\/g, '/')}/`));
26
+ if (matchDireta) {
27
+ ideal = matchDireta[1];
28
+ }
29
+ else {
30
+ const nome = path.basename(rel);
31
+ const [, tipo] = /\.([^.]+)\.[^.]+$/.exec(nome) ?? [];
32
+ if (tipo && CAMADAS[tipo]) {
33
+ ideal = CAMADAS[tipo];
34
+ }
35
+ }
36
+ return { arquivo: rel, atual, ideal };
37
+ }
20
38
  export async function analisarEstrutura(fileEntries, _baseDir = process.cwd()) {
21
39
  const limit = pLimit(CONCORRENCIA);
22
- const resultados = await Promise.all(fileEntries.map((entry) => limit(() => {
23
- const rel = entry.relPath;
24
- // Normaliza para separador POSIX para evitar dependência de platform e necessidade de mock em testes
25
- const normalizado = rel.replace(/\\/g, '/');
26
- const atual = normalizado.split('/')[0] || '';
27
- let ideal = null;
28
- const matchDireta = Object.entries(CAMADAS).find(([, dir]) => normalizado.startsWith(`${dir.replace(/\\/g, '/')}/`));
29
- if (matchDireta) {
30
- ideal = matchDireta[1];
31
- }
32
- else {
33
- const nome = path.basename(rel);
34
- const [, tipo] = /\.([^.]+)\.[^.]+$/.exec(nome) ?? [];
35
- if (tipo && CAMADAS[tipo]) {
36
- ideal = CAMADAS[tipo];
37
- }
38
- }
39
- return { arquivo: rel, atual, ideal };
40
- })));
40
+ const resultados = await Promise.all(fileEntries.map((entry) => limit(() => determinarCamadaIdeal(entry))));
41
41
  return resultados;
42
42
  }
43
43
  export { analisarEstrutura as alinhamentoEstrutural };
@@ -19,6 +19,13 @@ import { lerEstado, salvarEstado } from '../../shared/persistence/index.js';
19
19
  const { log, ArquetiposExtraMensagens } = getMessages();
20
20
  // Nome do arquivo legado (para compatibilidade)
21
21
  const ARQUETIPO_PERSONALIZADO_FILENAME = 'mahoraga.repo.arquetipo.json';
22
+ function logVerbose(fn) {
23
+ const isTest = (process.env.VITEST ?? '') !== '';
24
+ const isVerbose = log.verbose ?? false;
25
+ if (!isTest && isVerbose) {
26
+ fn();
27
+ }
28
+ }
22
29
  /**
23
30
  * Carrega o arquétipo personalizado do projeto atual
24
31
  * Tenta primeiro o novo caminho (.mahoraga/estrutura.arquetipo.json),
@@ -38,12 +45,7 @@ export async function carregarArquetipoPersonalizado(baseDir = process.cwd()) {
38
45
  const arquetipoLegado = await lerEstado(caminhoLegado, null);
39
46
  // Validação básica
40
47
  if (!arquetipoLegado?.nome || !arquetipoLegado.arquetipoOficial) {
41
- // Reduz ruído em testes e quando não estiver em modo verbose
42
- const isTest = (process.env.VITEST ?? '') !== '';
43
- const isVerbose = log.verbose ?? false;
44
- if (!isTest && isVerbose) {
45
- log.aviso(ArquetiposExtraMensagens.naoEncontrado.replace('{novo}', novoCaminho).replace('{legado}', caminhoLegado));
46
- }
48
+ logVerbose(() => { log.aviso(ArquetiposExtraMensagens.naoEncontrado.replace('{novo}', novoCaminho).replace('{legado}', caminhoLegado)); });
47
49
  return null;
48
50
  }
49
51
  return arquetipoLegado;
@@ -41,6 +41,34 @@ if (process.env.VITEST) {
41
41
  scoreArquetipo
42
42
  };
43
43
  }
44
+ function aplicarBoostConfianca(candidato, arquetiposCandidatos, melhorDeteccao) {
45
+ if (arquetiposCandidatos.some((arq) => candidato.nome.includes(arq) || arq.includes(candidato.nome))) {
46
+ const boostConfianca = Math.min(melhorDeteccao.confiancaTotal * 100 * 0.8, 60);
47
+ return {
48
+ ...candidato,
49
+ confidence: Math.max(candidato.confidence + boostConfianca, 70),
50
+ explicacaoSimilaridade: `[Boost contextual: ${melhorDeteccao.tecnologia} detectado com ${(melhorDeteccao.confiancaTotal * 100).toFixed(0)}% confiança] ${candidato.explicacaoSimilaridade ?? ''}`
51
+ };
52
+ }
53
+ return candidato;
54
+ }
55
+ function criarArquetipoVirtual(melhorDeteccao, tecnologiaDetectada) {
56
+ return {
57
+ nome: tecnologiaDetectada,
58
+ score: Math.round(melhorDeteccao.confiancaTotal * 100),
59
+ confidence: Math.round(melhorDeteccao.confiancaTotal * 100),
60
+ matchedRequired: [],
61
+ missingRequired: [],
62
+ matchedOptional: [],
63
+ dependencyMatches: (melhorDeteccao.evidencias ?? []).filter(erro => erro.tipo === 'dependencia').map(erro => erro.valor),
64
+ filePadraoMatches: (melhorDeteccao.evidencias ?? []).filter(erro => erro.tipo === 'estrutura').map(erro => erro.valor),
65
+ forbiddenPresent: [],
66
+ anomalias: [],
67
+ sugestaoPadronizacao: (melhorDeteccao.sugestoesMelhoria ?? []).join('; '),
68
+ explicacaoSimilaridade: `Detectado via sistema inteligente contextual com ${(melhorDeteccao.confiancaTotal * 100).toFixed(0)}% confiança`,
69
+ descricao: `Projeto ${melhorDeteccao.tecnologia} identificado por análise contextual`
70
+ };
71
+ }
44
72
  export async function detectarArquetipos(contexto, baseDir, options) {
45
73
  if (!options?.quiet && config.VERBOSE) {
46
74
  console.log(DetectorArquetiposExtraMensagens.debugChamado, contexto.arquivos.length, 'arquivos');
@@ -115,34 +143,9 @@ export async function detectarArquetipos(contexto, baseDir, options) {
115
143
  else {
116
144
  // Aplicar boost de confiança para arquétipos relacionados
117
145
  const boostAplicado = candidatos.some((candidato) => arquetiposCandidatos.some((arq) => candidato.nome.includes(arq) || arq.includes(candidato.nome)));
118
- candidatos = candidatos.map((candidato) => {
119
- if (arquetiposCandidatos.some((arq) => candidato.nome.includes(arq) || arq.includes(candidato.nome))) {
120
- const boostConfianca = Math.min(melhorDeteccao.confiancaTotal * 100 * 0.8, 60);
121
- return {
122
- ...candidato,
123
- confidence: Math.max(candidato.confidence + boostConfianca, 70),
124
- explicacaoSimilaridade: `[Boost contextual: ${melhorDeteccao.tecnologia} detectado com ${(melhorDeteccao.confiancaTotal * 100).toFixed(0)}% confiança] ${candidato.explicacaoSimilaridade ?? ''}`
125
- };
126
- }
127
- return candidato;
128
- });
146
+ candidatos = candidatos.map((candidato) => aplicarBoostConfianca(candidato, arquetiposCandidatos, melhorDeteccao));
129
147
  if (!boostAplicado) {
130
- const arquetipoVirtual = {
131
- nome: tecnologiaDetectada,
132
- score: Math.round(melhorDeteccao.confiancaTotal * 100),
133
- confidence: Math.round(melhorDeteccao.confiancaTotal * 100),
134
- matchedRequired: [],
135
- missingRequired: [],
136
- matchedOptional: [],
137
- dependencyMatches: (melhorDeteccao.evidencias ?? []).filter(erro => erro.tipo === 'dependencia').map(erro => erro.valor),
138
- filePadraoMatches: (melhorDeteccao.evidencias ?? []).filter(erro => erro.tipo === 'estrutura').map(erro => erro.valor),
139
- forbiddenPresent: [],
140
- anomalias: [],
141
- sugestaoPadronizacao: (melhorDeteccao.sugestoesMelhoria ?? []).join('; '),
142
- explicacaoSimilaridade: `Detectado via sistema inteligente contextual com ${(melhorDeteccao.confiancaTotal * 100).toFixed(0)}% confiança`,
143
- descricao: `Projeto ${melhorDeteccao.tecnologia} identificado por análise contextual`
144
- };
145
- candidatos.unshift(arquetipoVirtual);
148
+ candidatos.unshift(criarArquetipoVirtual(melhorDeteccao, tecnologiaDetectada));
146
149
  }
147
150
  }
148
151
  }
@@ -17,6 +17,28 @@ _sinaisAvancados // prefixo _ para ignorar warning de unused
17
17
  anomalias: []
18
18
  };
19
19
  }
20
+ function processarImportsFramework(imports, tecnologias, sinais) {
21
+ for (const indice of imports) {
22
+ if (typeof indice.source.value === 'string') {
23
+ const importSource = indice.source.value.toLowerCase();
24
+ if (importSource.includes('react') || importSource.includes('vue') || importSource.includes('angular')) {
25
+ tecnologias.set('frontend-framework', (tecnologias.get('frontend-framework') ?? 0) + 1);
26
+ }
27
+ if (importSource.includes('express') || importSource.includes('fastify') || importSource.includes('nestjs')) {
28
+ tecnologias.set('backend-framework', (tecnologias.get('backend-framework') ?? 0) + 1);
29
+ }
30
+ if (importSource.includes('prisma') || importSource.includes('mongoose') || importSource.includes('typeorm')) {
31
+ tecnologias.set('orm', (tecnologias.get('orm') ?? 0) + 1);
32
+ }
33
+ if (importSource.includes('jest') || importSource.includes('vitest') || importSource.includes('mocha')) {
34
+ tecnologias.set('testing-framework', (tecnologias.get('testing-framework') ?? 0) + 1);
35
+ }
36
+ if (/express|react|next|electron|discord\.js|telegraf/.test(importSource)) {
37
+ sinais.frameworksDetectados.push(indice.source.value);
38
+ }
39
+ }
40
+ }
41
+ }
20
42
  export function extrairSinaisAvancados(fileEntries, packageJson, _p0, _baseDir, _arquivos) {
21
43
  // Auxiliar para checar se o nó possui id.name string
22
44
  const hasIdNome = (node) => {
@@ -46,7 +68,7 @@ export function extrairSinaisAvancados(fileEntries, packageJson, _p0, _baseDir,
46
68
  for (const fe of fileEntries) {
47
69
  let body = [];
48
70
  if (fe.ast && 'node' in fe.ast && Array.isArray(fe.ast.node.body)) {
49
- body = fe.ast.node.body;
71
+ body = (fe.ast.node).body;
50
72
  }
51
73
  // Análise inteligente de padrões
52
74
  const conteudo = fe.content ?? '';
@@ -134,31 +156,7 @@ export function extrairSinaisAvancados(fileEntries, packageJson, _p0, _baseDir,
134
156
  // Classes
135
157
  sinais.classes += body.filter((node) => node.type === 'ClassDeclaration').length;
136
158
  // Frameworks por import (expandido)
137
- for (const indice of imports) {
138
- if (typeof indice.source.value === 'string') {
139
- const importSource = indice.source.value.toLowerCase();
140
- // Frameworks web
141
- if (importSource.includes('react') || importSource.includes('vue') || importSource.includes('angular')) {
142
- tecnologias.set('frontend-framework', (tecnologias.get('frontend-framework') ?? 0) + 1);
143
- }
144
- // Frameworks backend
145
- if (importSource.includes('express') || importSource.includes('fastify') || importSource.includes('nestjs')) {
146
- tecnologias.set('backend-framework', (tecnologias.get('backend-framework') ?? 0) + 1);
147
- }
148
- // ORMs
149
- if (importSource.includes('prisma') || importSource.includes('mongoose') || importSource.includes('typeorm')) {
150
- tecnologias.set('orm', (tecnologias.get('orm') ?? 0) + 1);
151
- }
152
- // Testing
153
- if (importSource.includes('jest') || importSource.includes('vitest') || importSource.includes('mocha')) {
154
- tecnologias.set('testing-framework', (tecnologias.get('testing-framework') ?? 0) + 1);
155
- }
156
- // Lista original mantida
157
- if (/express|react|next|electron|discord\.js|telegraf/.test(importSource)) {
158
- sinais.frameworksDetectados.push(indice.source.value);
159
- }
160
- }
161
- }
159
+ processarImportsFramework(imports, tecnologias, sinais);
162
160
  // Padrões de pastas/arquivos (expandido)
163
161
  const rel = fe.relPath.replace(/\\/g, '/');
164
162
  if (/src\/controllers|pages|api|prisma|packages|apps|src\/routes|src\/services|src\/repositories/.test(rel)) {
@@ -23,7 +23,7 @@ export function extrairSinaisProjeto(fileEntries, packageJson) {
23
23
  for (const fe of fileEntries) {
24
24
  let body = [];
25
25
  if (fe.ast && 'node' in fe.ast && Array.isArray(fe.ast.node.body)) {
26
- body = fe.ast.node.body;
26
+ body = (fe.ast.node).body;
27
27
  }
28
28
  // Funções
29
29
  sinais.funcoes += body.filter((node) => node.type === 'FunctionDeclaration').length;
@@ -3,15 +3,52 @@ import { statSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { getTypesDirectoryRelPosix } from '../../core/config/index.js';
5
5
  import { carregarConfigEstrategia, destinoPara, deveIgnorar, normalizarRel } from '../../shared/helpers/index.js';
6
- /**
7
- * Estrategista/Planejador de Estrutura
8
- *
9
- * Responsável por: dado o conjunto de arquivos e um catálogo de arquétipos,
10
- * sugerir um plano de reorganização (mover arquivos) com base em regras de nomeação
11
- * e diretórios-alvo padronizados. Não aplica mudanças no disco (apenas sugere).
12
- *
13
- * Domínio ideal: arquitetos (diagnóstico/planejamento). A execução fica com zeladores.
14
- */
6
+ function processarArquivoParaMovimento(rel, cfg, baseDir, sinaisAvancados, rels, mover, conflitos) {
7
+ if (deveIgnorar(rel, cfg.ignorarPastas)) {
8
+ return;
9
+ }
10
+ if (/__(tests|mocks)__/.test(rel) || /\.(test|spec)\.[jt]sx?$/.test(rel) || /fixtures\//.test(rel)) {
11
+ return;
12
+ }
13
+ if (!rel.endsWith('.ts') && !rel.endsWith('.js')) {
14
+ return;
15
+ }
16
+ const base = path.posix.basename(rel);
17
+ if (/^(eslint|vitest|next|tailwind|postcss|prettier|jest)\.config\.[jt]s$/i.test(base)) {
18
+ return;
19
+ }
20
+ const res = destinoPara(rel, cfg.raizCodigo, cfg.criarSubpastasPorEntidade, cfg.apenasCategoriasConfiguradas, cfg.categoriasMapa);
21
+ if (!res.destinoDir) {
22
+ return;
23
+ }
24
+ const currentDir = path.posix.dirname(rel);
25
+ const alreadyInTarget = currentDir === res.destinoDir || currentDir.startsWith(`${res.destinoDir}/`);
26
+ if (alreadyInTarget) {
27
+ return;
28
+ }
29
+ let destinoDirAjustado = res.destinoDir;
30
+ const motivoAjustado = res.motivo ?? 'Reorganização padrão';
31
+ if (sinaisAvancados) {
32
+ destinoDirAjustado = ajustarDestinoPorSinais(rel, res.destinoDir, sinaisAvancados, motivoAjustado);
33
+ }
34
+ const destino = path.posix.join(destinoDirAjustado, path.posix.basename(rel));
35
+ let destinoExiste = rels.includes(destino);
36
+ if (!destinoExiste) {
37
+ try {
38
+ const abs = path.join(baseDir, destino.replace(/\\/g, '/'));
39
+ statSync(abs);
40
+ destinoExiste = true;
41
+ }
42
+ catch {
43
+ destinoExiste = false;
44
+ }
45
+ }
46
+ if (destinoExiste) {
47
+ conflitos.push({ alvo: destino, motivo: 'destino já existe' });
48
+ return;
49
+ }
50
+ mover.push({ de: rel, para: destino, motivo: motivoAjustado });
51
+ }
15
52
  export async function gerarPlanoEstrategico(contexto, opcoes = {}, sinaisAvancados) {
16
53
  const typesDir = getTypesDirectoryRelPosix();
17
54
  const cfg = await carregarConfigEstrategia(contexto.baseDir, {
@@ -24,63 +61,7 @@ export async function gerarPlanoEstrategico(contexto, opcoes = {}, sinaisAvancad
24
61
  const rels = contexto.arquivos.map(arquivo => normalizarRel(arquivo.relPath));
25
62
  const isTestLike = (caminho) => /__(tests|mocks)__/.test(caminho) || /\.(test|spec)\.[jt]sx?$/.test(caminho) || /fixtures\//.test(caminho);
26
63
  for (const rel of rels) {
27
- if (deveIgnorar(rel, cfg.ignorarPastas)) {
28
- continue;
29
- }
30
- if (isTestLike(rel)) {
31
- continue;
32
- } // não mover testes/fixtures
33
- // Evitar mexer em arquivos fora do escopo de código (por agora)
34
- if (!rel.endsWith('.ts') && !rel.endsWith('.js')) {
35
- continue;
36
- }
37
- // Respeita convenções de ferramentas no root: não mover configs globais
38
- const base = path.posix.basename(rel);
39
- if (/^(eslint|vitest|next|tailwind|postcss|prettier|jest)\.config\.[jt]s$/i.test(base)) {
40
- continue;
41
- }
42
- const res = destinoPara(rel, cfg.raizCodigo, cfg.criarSubpastasPorEntidade, cfg.apenasCategoriasConfiguradas, cfg.categoriasMapa);
43
- if (!res.destinoDir) {
44
- continue;
45
- }
46
- const currentDir = path.posix.dirname(rel);
47
- const alreadyInTarget = currentDir === res.destinoDir || currentDir.startsWith(`${res.destinoDir}/`);
48
- if (alreadyInTarget) {
49
- continue;
50
- }
51
- // [LANCAMENTO] INTELIGÊNCIA CONTEXTUAL: Ajustar destino baseado em sinais avançados
52
- let destinoDirAjustado = res.destinoDir;
53
- const motivoAjustado = res.motivo ?? 'Reorganização padrão';
54
- if (sinaisAvancados) {
55
- destinoDirAjustado = ajustarDestinoPorSinais(rel, res.destinoDir, sinaisAvancados, motivoAjustado);
56
- }
57
- // Mantém o mesmo nome do arquivo; apenas move para pasta de destino
58
- const destino = path.posix.join(destinoDirAjustado, path.posix.basename(rel));
59
- // Conflito se já existe arquivo listado ou presente no filesystem
60
- let destinoExiste = rels.includes(destino);
61
- if (!destinoExiste) {
62
- try {
63
- const abs = path.join(contexto.baseDir, destino.replace(/\\/g, '/'));
64
- // fs.statSync usado de forma segura; se falhar, considera inexistente
65
- statSync(abs);
66
- destinoExiste = true;
67
- }
68
- catch {
69
- destinoExiste = false;
70
- }
71
- }
72
- if (destinoExiste) {
73
- conflitos.push({
74
- alvo: destino,
75
- motivo: 'destino já existe'
76
- });
77
- continue;
78
- }
79
- mover.push({
80
- de: rel,
81
- para: destino,
82
- motivo: motivoAjustado
83
- });
64
+ processarArquivoParaMovimento(rel, cfg, contexto.baseDir, sinaisAvancados, rels, mover, conflitos);
84
65
  }
85
66
  // Deduplicação simples
86
67
  const seen = new Set();
@@ -11,6 +11,21 @@ export function registrarDetectorAzure(detector) {
11
11
  export function obterDetectoresAzure() {
12
12
  return [...detectoresRegistrados];
13
13
  }
14
+ function processarResultadosPlugins(plugResults, ocorrencias, relPath) {
15
+ for (const results of plugResults) {
16
+ for (const item of results) {
17
+ ocorrencias.push({
18
+ tipo: `AZURE_${item.tipo.toUpperCase().replace(/-/g, '_')}`,
19
+ mensagem: item.descricao,
20
+ relPath,
21
+ linha: item.linha ?? 1,
22
+ coluna: 1,
23
+ sugestao: item.sugestao ?? '',
24
+ nivel: item.severidade === 'critica' || item.severidade === 'alta' ? 'erro' : item.severidade === 'media' ? 'aviso' : 'info',
25
+ });
26
+ }
27
+ }
28
+ }
14
29
  export const analistaAzurePipelines = {
15
30
  nome: 'azure-pipelines',
16
31
  categoria: 'workflows',
@@ -23,7 +38,11 @@ export const analistaAzurePipelines = {
23
38
  const securityResults = await Promise.resolve(detectorAzureSecurity.aplicar(conteudo, relPath, null, _fc, contexto));
24
39
  const performanceResults = await Promise.resolve(detectorAzurePerformance.aplicar(conteudo, relPath, null, _fc, contexto));
25
40
  const estruturaResults = await Promise.resolve(detectorAzureEstrutura.aplicar(conteudo, relPath, null, _fc, contexto));
26
- ocorrencias.push(...securityResults, ...performanceResults, ...estruturaResults);
41
+ for (const r of [securityResults, performanceResults, estruturaResults]) {
42
+ if (r) {
43
+ ocorrencias.push(...(Array.isArray(r) ? r : [r]));
44
+ }
45
+ }
27
46
  // 2. Análise Estrutural básica
28
47
  const doc = parseDocument(conteudo);
29
48
  const configData = doc.toJS();
@@ -56,19 +75,7 @@ export const analistaAzurePipelines = {
56
75
  // 3. Plugins registrados
57
76
  const caminhos = contexto?.arquivos.map(arquivo => arquivo.relPath) ?? [];
58
77
  const plugResults = await Promise.all(detectoresRegistrados.map(detector => Promise.resolve(detector.testar(configData, { conteudo, caminhos })).catch(() => [])));
59
- for (const results of plugResults) {
60
- for (const item of results) {
61
- ocorrencias.push({
62
- tipo: `AZURE_${item.tipo.toUpperCase().replace(/-/g, '_')}`,
63
- mensagem: item.descricao,
64
- relPath,
65
- linha: item.linha ?? 1,
66
- coluna: 1,
67
- sugestao: item.sugestao ?? '',
68
- nivel: item.severidade === 'critica' || item.severidade === 'alta' ? 'erro' : item.severidade === 'media' ? 'aviso' : 'info',
69
- });
70
- }
71
- }
78
+ processarResultadosPlugins(plugResults, ocorrencias, relPath);
72
79
  }
73
80
  catch {
74
81
  // Falha silenciosa no parse
@@ -2,30 +2,33 @@
2
2
  import { messages } from '../../../core/messages/index.js';
3
3
  import { splitLines } from '../../../shared/helpers/index.js';
4
4
  import { criarOcorrencia } from '../../../types/index.js';
5
+ function processarLinhaAzure(linha, numeroLinha, correcoes) {
6
+ if (/vmImage:\s*['"]?.*latest['"]?/i.test(linha)) {
7
+ correcoes.push({
8
+ tipo: 'azure-fix-latest-image',
9
+ original: linha,
10
+ sugerido: linha.replace(/latest/i, 'ubuntu-22.04'),
11
+ linha: numeroLinha,
12
+ confianca: 90
13
+ });
14
+ }
15
+ if (/(password|token|secret|key):\s*['"]?([^$({][a-zA-Z0-9_\-]{8,})['"]?/gi.test(linha)) {
16
+ const replaced = linha.replace(/['"][^'"]+['"]/, '$(SECRET_NAME)');
17
+ correcoes.push({
18
+ tipo: 'azure-use-variable',
19
+ original: linha,
20
+ sugerido: replaced,
21
+ linha: numeroLinha,
22
+ confianca: 90
23
+ });
24
+ }
25
+ }
5
26
  export function detectarCorrecoesAzure(src, _relPath) {
6
27
  const correcoes = [];
7
28
  const linhas = splitLines(src);
8
29
  linhas.forEach((linha, index) => {
9
30
  const numeroLinha = index + 1;
10
- if (/vmImage:\s*['"]?.*latest['"]?/i.test(linha)) {
11
- correcoes.push({
12
- tipo: 'azure-fix-latest-image',
13
- original: linha,
14
- sugerido: linha.replace(/latest/i, 'ubuntu-22.04'),
15
- linha: numeroLinha,
16
- confianca: 90
17
- });
18
- }
19
- if (/(password|token|secret|key):\s*['"]?([^$({][a-zA-Z0-9_\-]{8,})['"]?/gi.test(linha)) {
20
- const replaced = linha.replace(/['"][^'"]+['"]/, '$(SECRET_NAME)');
21
- correcoes.push({
22
- tipo: 'azure-use-variable',
23
- original: linha,
24
- sugerido: replaced,
25
- linha: numeroLinha,
26
- confianca: 90
27
- });
28
- }
31
+ processarLinhaAzure(linha, numeroLinha, correcoes);
29
32
  });
30
33
  if (!/task:\s*Cache@/i.test(src) && !/CacheBeta@/i.test(src)) {
31
34
  correcoes.push({
@@ -1,6 +1,26 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import { messages } from '../../../core/messages/index.js';
3
3
  import { criarOcorrencia } from '../../../types/index.js';
4
+ function detectarParallelDeployment(src, relPath, ocorrencias) {
5
+ if (/environment:/i.test(src) && /deploy/i.test(src)) {
6
+ const deployJobs = src.match(/- job:\s*\w+/g);
7
+ const deployStages = src.match(/- stage:\s*\w+/g);
8
+ const totalDeployUnits = (deployJobs ? deployJobs.length : 0) + (deployStages ? deployStages.length : 0);
9
+ if (totalDeployUnits > 1) {
10
+ const hasDependsOn = /dependsOn:/i.test(src);
11
+ if (!hasDependsOn) {
12
+ ocorrencias.push(criarOcorrencia({
13
+ tipo: 'azure-pipelines-missing-parallel-deployment',
14
+ nivel: 'info',
15
+ mensagem: messages.RelatorioAzureMensagens.missingParallelDeployment,
16
+ relPath,
17
+ linha: 1,
18
+ sugestao: messages.RelatorioAzureMensagens.missingParallelDeploymentSugestao,
19
+ }));
20
+ }
21
+ }
22
+ }
23
+ }
4
24
  export const detectorAzurePerformance = {
5
25
  nome: 'detector-azure-performance',
6
26
  categoria: 'performance',
@@ -47,24 +67,7 @@ export const detectorAzurePerformance = {
47
67
  }));
48
68
  }
49
69
  // 4. Parallel deployment detection
50
- if (/environment:/i.test(src) && /deploy/i.test(src)) {
51
- const deployJobs = src.match(/- job:\s*\w+/g);
52
- const deployStages = src.match(/- stage:\s*\w+/g);
53
- const totalDeployUnits = (deployJobs ? deployJobs.length : 0) + (deployStages ? deployStages.length : 0);
54
- if (totalDeployUnits > 1) {
55
- const hasDependsOn = /dependsOn:/i.test(src);
56
- if (!hasDependsOn) {
57
- ocorrencias.push(criarOcorrencia({
58
- tipo: 'azure-pipelines-missing-parallel-deployment',
59
- nivel: 'info',
60
- mensagem: messages.RelatorioAzureMensagens.missingParallelDeployment,
61
- relPath,
62
- linha: 1,
63
- sugestao: messages.RelatorioAzureMensagens.missingParallelDeploymentSugestao,
64
- }));
65
- }
66
- }
67
- }
70
+ detectarParallelDeployment(src, relPath, ocorrencias);
68
71
  // 5. Large artifact staging
69
72
  const publishArtifacts = src.match(/task:\s*PublishBuildArtifacts@/gi);
70
73
  if (publishArtifacts && publishArtifacts.length > 3) {
@@ -16,6 +16,30 @@ const SECRET_PATTERNS = [
16
16
  /(connection[_-]?string)\s*[:=]\s*['"][^'"]+['"]/gi,
17
17
  /(azure[_-]?devops[_-]?pat)\s*[:=]\s*['"][^'"]+['"]/gi,
18
18
  ];
19
+ function detectarSecretVarsPlainText(src, relPath, ocorrencias) {
20
+ const variableLines = src.match(/^\s*variables\s*:\s*$/im);
21
+ if (!variableLines) {
22
+ return;
23
+ }
24
+ const varSection = src.slice(src.indexOf('variables:'));
25
+ const secretVars = varSection.match(/([a-zA-Z_]\w*)\s*:\s*['"][^'"]+['"]/g);
26
+ if (!secretVars) {
27
+ return;
28
+ }
29
+ for (const sv of secretVars) {
30
+ const varName = sv.split(':')[0].trim();
31
+ if (/(password|token|secret|key|connection)/i.test(varName)) {
32
+ ocorrencias.push(criarOcorrencia({
33
+ tipo: 'azure-pipelines-secret-variable-plaintext',
34
+ nivel: 'erro',
35
+ mensagem: `Variável '${varName}' definida em texto plano na seção variables`,
36
+ relPath,
37
+ linha: 1,
38
+ sugestao: 'Use um grupo de variáveis com proteção de segredo ou referencie como $(varName)',
39
+ }));
40
+ }
41
+ }
42
+ }
19
43
  export const detectorAzureSecurity = {
20
44
  nome: 'detector-azure-security',
21
45
  categoria: 'seguranca',
@@ -148,26 +172,7 @@ export const detectorAzureSecurity = {
148
172
  }
149
173
  }
150
174
  // 10. Secret variable detection in plain text
151
- const variableLines = src.match(/^\s*variables\s*:\s*$/im);
152
- if (variableLines) {
153
- const varSection = src.slice(src.indexOf('variables:'));
154
- const secretVars = varSection.match(/([a-zA-Z_]\w*)\s*:\s*['"][^'"]+['"]/g);
155
- if (secretVars) {
156
- for (const sv of secretVars) {
157
- const varName = sv.split(':')[0].trim();
158
- if (/(password|token|secret|key|connection)/i.test(varName)) {
159
- ocorrencias.push(criarOcorrencia({
160
- tipo: 'azure-pipelines-secret-variable-plaintext',
161
- nivel: 'erro',
162
- mensagem: `Variável '${varName}' definida em texto plano na seção variables`,
163
- relPath,
164
- linha: 1,
165
- sugestao: 'Use um grupo de variáveis com proteção de segredo ou referencie como $(varName)',
166
- }));
167
- }
168
- }
169
- }
170
- }
175
+ detectarSecretVarsPlainText(src, relPath, ocorrencias);
171
176
  return ocorrencias;
172
177
  }
173
178
  };