@mocoto/mahoraga 0.14.7 → 0.14.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,10 +8,7 @@ Proveniência e Autoria: Este documento integra o projeto @mocoto/mahoraga (lice
8
8
 
9
9
  </div>
10
10
 
11
- > Este projeto não está mais no GitHub. Publicado exclusivamente no npm como `@mocoto/mahoraga`.
12
-
13
- <p> Desisti de desistir do projeto, até parece que vou aceitar perder pra vocês 🤪 </p>
14
- <p> Caso tenha receio em ajudar com codigo você pode me ajudar ao usar e encontrar falsos postivos/negativos e me mandar relatorios por meio desse e-mail ai em baixo, me ajudaria muito, pois nem todos os erros e bugs eu consigo resolver em ambiente de desenvolvimento sendo preciso uso em produção para serem encontrados</p>
11
+ Caso tenha receio em ajudar com codigo você pode me ajudar ao usar e encontrar falsos postivos/negativos e me mandar relatorios por meio desse e-mail ai em baixo, me ajudaria muito, pois nem todos os erros e bugs eu consigo resolver em ambiente de desenvolvimento sendo preciso uso em produção para serem encontrados
15
12
 
16
13
  [Gmail](mailto:mocoto.persona@gmail.com)
17
14
 
@@ -178,7 +175,6 @@ mahoraga atualizar --global # auto-update
178
175
 
179
176
  ### Relatórios Profissionais
180
177
 
181
- - **Múltiplos formatos**: JSON, Markdown
182
178
  - **Métricas detalhadas**: complexidade ciclomática, duplicação, cobertura, performance com histórico
183
179
  - **Baseline de performance**: snapshots e comparação ao longo do tempo
184
180
  - **Scan de licenças**: verificação de dependências e geração de THIRD-PARTY-NOTICES
@@ -187,8 +183,6 @@ mahoraga atualizar --global # auto-update
187
183
 
188
184
  ### Integrações CI/CD
189
185
 
190
- - **GitHub Actions**: scan de workflows, score 0-100, check runs, quality gate
191
- - **GitHub App**: análise automática em PRs e pushes
192
186
  - **GitLab CI / CircleCI / Jenkins / Azure Pipelines**: análise de pipelines
193
187
  - **Conversão** entre plataformas de CI/CD
194
188
 
@@ -252,9 +246,9 @@ O Mahoraga lê um arquivo `mahoraga.config.json` na raiz do seu projeto (ou vari
252
246
  |----------|-----------|--------|
253
247
  | `LOG_LEVEL` | Nível de log | `info` |
254
248
  | `LOG_ESTRUTURADO` | Logs em JSON | `false` |
255
- | `SUKUNA_DEBUG` | Modo debug | não definido |
256
- | `SUKUNA_STATE_DIR` | Diretório de estado | `.mahoraga` |
257
- | `SUKUNA_API_PORT` | Porta do GitHub App | `3100` |
249
+ | `MAHORAGA_DEBUG` | Modo debug | não definido |
250
+ | `MAHORAGA_STATE_DIR` | Diretório de estado | `.mahoraga` |
251
+ | `MAHORAGA_API_PORT` | Porta do GitHub App | `3100` |
258
252
  | `GITHUB_APP_ID` | ID do GitHub App | não definido |
259
253
  | `GITHUB_PRIVATE_KEY` | Chave RSA do GitHub App | não definido |
260
254
  | `GITHUB_WEBHOOK_SECRET` | Webhook secret | não definido |
@@ -281,7 +275,7 @@ npm run lint # ESLint
281
275
  | JavaScript | Babel | Nativo |
282
276
  | TypeScript | Babel | Nativo |
283
277
  | HTML | htmlparser2 | Nativo |
284
- | CSS | css-tree | Nativo |
278
+ | CSS | postcss | Nativo |
285
279
  | XML | fast-xml-parser | Nativo |
286
280
  | Python | Heurístico | Nativo |
287
281
  | PHP | Heurístico | Nativo |
@@ -336,5 +330,3 @@ Dependências de terceiros listadas em [THIRD-PARTY-NOTICES.txt](./THIRD-PARTY-N
336
330
  ---
337
331
 
338
332
  **Feito para os iniciantes**
339
-
340
- **_Estou testando otypescript 7.0.2 Até o momento parece muito bom. o problema é que tive que remover o eslint porque ele nao roda com ts 7, vou esperar eles atualizar e voltar o eslint para o projeto._**
@@ -4,7 +4,7 @@
4
4
  */
5
5
  const detectoresJsTs = [];
6
6
  export function registrarDetectorJsTs(detector) {
7
- if (!detector.nome) {
7
+ if (!detector || !detector.nome) {
8
8
  return;
9
9
  }
10
10
  const exists = detectoresJsTs.some(d => d.nome === detector.nome);
@@ -35,6 +35,9 @@ export const AGRESSIVA_AUTO_CORRECAO_CONFIGURACAO = {
35
35
  // Backwards compat constant name used elsewhere in the codebase
36
36
  export const AUTO_CORRECAO_CONFIGURACAO_PADROES = PADRAO_AUTO_CORRECAO_CONFIGURACAO;
37
37
  export function shouldExcludeFile(fileCaminho, config) {
38
+ if (!config.excludePadroes || !Array.isArray(config.excludePadroes)) {
39
+ return false;
40
+ }
38
41
  return config.excludePadroes.some(pattern => {
39
42
  const regexPadrao = pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*').replace(/\?/g, '.');
40
43
  return new RegExp(regexPadrao).test(fileCaminho);
@@ -48,7 +51,10 @@ export function shouldExcludeFunction(functionName, config) {
48
51
  }
49
52
  export function isCategoryAllowed(category, config) {
50
53
  // allowedCategories is an array of known category strings - coerce to string[] for safe includes check
51
- return (config.allowedCategories).includes(category);
54
+ if (!config.allowedCategories || !Array.isArray(config.allowedCategories)) {
55
+ return true;
56
+ }
57
+ return config.allowedCategories.includes(category);
52
58
  }
53
59
  export function hasMinimumConfidence(confidence, config) {
54
60
  if (typeof config.minConfidence !== 'number') {
@@ -63,9 +69,8 @@ export function getAutoFixConfig(mode) {
63
69
  case 'aggressive':
64
70
  return AGRESSIVA_AUTO_CORRECAO_CONFIGURACAO;
65
71
  case 'balanced':
66
- case undefined: {
67
- throw new Error('Not implemented yet: undefined case');
68
- }
72
+ case undefined:
73
+ return PADRAO_AUTO_CORRECAO_CONFIGURACAO;
69
74
  default:
70
75
  return PADRAO_AUTO_CORRECAO_CONFIGURACAO;
71
76
  }
@@ -110,7 +110,7 @@ export const configPadrao = {
110
110
  ANALISE_INCREMENTAL_ENABLED: false,
111
111
  ANALISE_INCREMENTAL_STATE_PATH: path.join(SUKUNA_ESTADO, 'incremental-analise.json'),
112
112
  ANALISE_INCREMENTAL_VERSION: 1,
113
- ANALISE_CACHE_ENABLED: true,
113
+ ANALISE_CACHE_ENABLED: false,
114
114
  ANALISE_CACHE_STATE_PATH: path.join(SUKUNA_ESTADO, 'analysis-cache.json'),
115
115
  ANALISE_CACHE_VERSION: 1,
116
116
  ANALISE_CACHE_TTL_MS: 86400000,
@@ -163,11 +163,11 @@ export const ICONES_ZELADOR = {
163
163
  export function getIcone(categoria, nome) {
164
164
  switch (categoria) {
165
165
  case 'nivel':
166
- return ICONES_NIVEL[nome];
166
+ return ICONES_NIVEL[nome] ?? ICONES_NIVEL.info;
167
167
  case 'status':
168
- return ICONES_STATUS[nome];
168
+ return ICONES_STATUS[nome] ?? ICONES_STATUS.pendente;
169
169
  case 'acao':
170
- return ICONES_ACAO[nome];
170
+ return ICONES_ACAO[nome] ?? '[*]';
171
171
  default:
172
172
  return '[*]';
173
173
  }
@@ -6,9 +6,9 @@ export function parseComJson(codigo) {
6
6
  try {
7
7
  const parsed = JSON.parse(codigo);
8
8
  const info = {
9
- type: Array.isArray(parsed) ? 'array' : typeof parsed,
9
+ type: parsed === null ? 'null' : (Array.isArray(parsed) ? 'array' : typeof parsed),
10
10
  };
11
- if (typeof parsed === 'object' && !Array.isArray(parsed)) {
11
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
12
12
  const keys = Object.keys(parsed);
13
13
  info.keys = keys;
14
14
  info.depth = estimateDepth(parsed, 0);
@@ -27,6 +27,12 @@ export function parseComTypeScript(codigo, tsx = false) {
27
27
  errorRecovery: true,
28
28
  plugins: plugins
29
29
  });
30
+ if (ast.errors && ast.errors.length > 0) {
31
+ for (const err of ast.errors) {
32
+ logCore.erroTs(err.message ?? String(err), getCurrentParsingFile());
33
+ }
34
+ return null;
35
+ }
30
36
  return wrapMinimal(tsx ? 'tsx-tsc' : 'ts-tsc', {
31
37
  kind: 'SourceFile',
32
38
  statements: ast.program.body.length
@@ -40,7 +40,7 @@ export function criarSchemaMetadata(versao = VERSAO_ATUAL, descricaoPersonalizad
40
40
  export function validarSchema(relatorio) {
41
41
  const erros = [];
42
42
  // Verificar estrutura básica
43
- if (typeof relatorio !== 'object') {
43
+ if (typeof relatorio !== 'object' || relatorio === null) {
44
44
  erros.push('Relatório deve ser um objeto');
45
45
  return {
46
46
  valido: false,
@@ -0,0 +1,14 @@
1
+ // SPDX-License-Identifier: MIT
2
+ async function request(path) {
3
+ const response = await fetch(path);
4
+ if (!response.ok) {
5
+ const body = await response.json();
6
+ throw new Error(body.erro ?? `HTTP ${response.status}`);
7
+ }
8
+ return response.json();
9
+ }
10
+ export const api = {
11
+ getAppInfo() {
12
+ return request('/api/github/app-info');
13
+ },
14
+ };
@@ -9,6 +9,9 @@
9
9
  * A chave de deduplicação é composta por: relPath|linha|tipo|mensagem.
10
10
  */
11
11
  export function dedupeOcorrencias(arr) {
12
+ if (!arr || !Array.isArray(arr)) {
13
+ return [];
14
+ }
12
15
  const seen = new Map();
13
16
  for (const ocorrencia of arr) {
14
17
  const key = `${ocorrencia.relPath ?? ''}|${String(ocorrencia.linha ?? '')}|${ocorrencia.tipo ?? ''}|${ocorrencia.mensagem ?? ''}`;
@@ -10,7 +10,7 @@ export function formatarCodeMinimo(code, opts) {
10
10
  const formatted = normalizarNewlinesFinais(removerEspacosFinaisPorLinha(normalized));
11
11
  return {
12
12
  ok: true,
13
- parser: opts?.parser,
13
+ parser: (opts?.parser ?? 'code'),
14
14
  formatted,
15
15
  changed: formatted !== normalizarNewlinesFinais(normalized),
16
16
  };
@@ -93,13 +93,19 @@ const REPEATABLE_TO_SINGLE = new Set([',', '.', '!', '?', ';']);
93
93
  const REPEATABLE_KEEP_UP_TO_3 = new Set(['.', '!', '?']);
94
94
  const MULTI_PUNCT_RE = /([,\.!?:;_\-])\1+/g;
95
95
  const SPACE_BEFORE_PUNCT_RE = /\s+([,.:;!?])/g;
96
- const NO_SPACE_AFTER_NON_DOT_PUNCT_RE = /([,:;!?])([^\s\)\]\}\[!,;:?])/g;
96
+ const NO_SPACE_AFTER_NON_DOT_PUNCT_RE = /([,:;!?])([^\s\)\]\}\[!,;:?"'])/g;
97
97
  const SPACE_BEFORE_ELLIPSIS_RE = /([^0-9])(\.{3})/g;
98
98
  const PROTECTED_PATTERNS = [/https?:\/\/[^\s"'<>]+/gi, /ftp:\/\/[^\s"'<>]+/gi, /file:\/\/[^\s"'<>]+/gi, /www\.[^\s"'<>]+/gi, /[a-zA-Z0-9](?:\.[a-zA-Z0-9-]+)+\.[a-zA-Z]{2,}/g, /(?:^|(?<=\s|["'=(\[,]))\.\//g, /(?:^|(?<=\s|["'=(\[,]))\.\.\//g, /(?<=[/\\])[\w-]+\.[\w]+/g, /\d+\.\d+(?:\.\d+)*/g, /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, /[a-f0-9]{7,}\.[a-f0-9]+/gi, /[\w-]+\.[\w-]+\.[\w]+/g,
99
99
  // Proteger paths e nomes de arquivo com extensão
100
100
  /(?:^|[\s"'=(\[,])([\/\\][\w-]+(?:[\/\\][\w-]+)*[\/\\]?[\w-]+\.[a-zA-Z0-9]+)/g, /(?:^|[\s"'=(\[,])([\w-]+(?:[\/\\][\w-]+)+\.[a-zA-Z0-9]+)/g,
101
101
  // Proteger imports/require paths
102
- /(?:from\s+|require\(|import\s+)(["'])([^"']+\.[a-zA-Z0-9]+)\1/g];
102
+ /(?:from\s+|require\(|import\s+)(["'])([^"']+\.[a-zA-Z0-9]+)\1/g,
103
+ // Proteger expansões de parâmetro shell ${...} - operadores :- := :? :+ e slicing não devem ser tocados
104
+ /\$\{[^}]*\}/g,
105
+ // Proteger terminadores de case shell: ;; ;;& ;&
106
+ /;;[;&]?|;&/g,
107
+ // Proteger expressões de teste shell [[ ... ]] e (( ... ))
108
+ /\[\[[^\]]*\]\]|\(\([^)]*\)\)/g];
103
109
  function protectSequences(code) {
104
110
  const placeholders = [];
105
111
  let result = code;
@@ -299,14 +305,22 @@ function fixSpacingAroundPunct(code) {
299
305
  if (shouldSkipLineForPunctuation(line, idx, lines)) {
300
306
  return line;
301
307
  }
302
- const t1 = line.replace(SPACE_BEFORE_PUNCT_RE, '$1');
303
- const t2 = t1.replace(NO_SPACE_AFTER_NON_DOT_PUNCT_RE, (m, p1, _p2, off) => fixNonDotPunct(m, p1, _p2, off, t1));
308
+ // Proteger espaço antes de ! quando é operador de negação shell:
309
+ // [[ ! preservar o espaço, não remover
310
+ // Also protect (( ! and case ! patterns
311
+ const shellNegationProtected = line
312
+ .replace(/(\[\[)\s+(!)/g, '$1\x00SHL_NEG\x00')
313
+ .replace(/(\(\()\s+(!)/g, '$1\x00SHL_NEG\x00');
314
+ const t1 = shellNegationProtected.replace(SPACE_BEFORE_PUNCT_RE, '$1');
315
+ // Restaurar proteção de negação shell
316
+ const t1restored = t1.replace(/\x00SHL_NEG\x00/g, ' !');
317
+ const t2 = t1restored.replace(NO_SPACE_AFTER_NON_DOT_PUNCT_RE, (m, p1, _p2, off) => fixNonDotPunct(m, p1, _p2, off, t1restored));
304
318
  const t3 = t2.replace(NO_SPACE_AFTER_DOT_RE, (m, off) => fixDotPunct(m, off, t2));
305
319
  const t4 = t3.replace(SPACE_BEFORE_ELLIPSIS_RE, '$1 $2');
306
320
  if (t4 !== line) {
307
321
  changed = true;
308
322
  const countBefore = (line.match(SPACE_BEFORE_PUNCT_RE) ?? []).length;
309
- const countNonDot = (t1.match(NO_SPACE_AFTER_NON_DOT_PUNCT_RE) ?? []).length;
323
+ const countNonDot = (t1restored.match(NO_SPACE_AFTER_NON_DOT_PUNCT_RE) ?? []).length;
310
324
  const countDot = (t2.match(NO_SPACE_AFTER_DOT_RE) ?? []).length;
311
325
  const countEllipsis = (t3.match(SPACE_BEFORE_ELLIPSIS_RE) ?? []).length;
312
326
  totalChanges += countBefore + countNonDot + countDot + countEllipsis;
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // @mahoraga-disable ml-deep-nesting
3
+ import { execSync } from 'node:child_process';
3
4
  import { corrigirPontuacaoECodigo, limitarLinhasEmBranco, normalizarFimDeLinha, normalizarNewlinesFinais, removerBom } from './commons.js';
4
5
  function parseShellLine(raw, ctx) {
5
6
  const trimmed = raw.trimEnd();
@@ -129,6 +130,82 @@ function parseShellLine(raw, ctx) {
129
130
  }
130
131
  };
131
132
  }
133
+ /**
134
+ * Verifica se um índice dentro de uma linha está entre aspas (single ou double).
135
+ */
136
+ function isIndexInQuotes(line, index) {
137
+ let inSingle = false;
138
+ let inDouble = false;
139
+ let escaped = false;
140
+ for (let i = 0; i < index && i < line.length; i++) {
141
+ const ch = line[i];
142
+ if (escaped) {
143
+ escaped = false;
144
+ continue;
145
+ }
146
+ if (ch === '\\') {
147
+ escaped = true;
148
+ continue;
149
+ }
150
+ if (inSingle) {
151
+ if (ch === "'")
152
+ inSingle = false;
153
+ continue;
154
+ }
155
+ if (inDouble) {
156
+ if (ch === '"')
157
+ inDouble = false;
158
+ continue;
159
+ }
160
+ if (ch === "'") {
161
+ inSingle = true;
162
+ continue;
163
+ }
164
+ if (ch === '"') {
165
+ inDouble = true;
166
+ continue;
167
+ }
168
+ }
169
+ return inSingle || inDouble;
170
+ }
171
+ /**
172
+ * Verifica se um caractere pipe (|) está em posição de pipe shell real
173
+ * (fora de aspas, não dentro de string/regex literal).
174
+ */
175
+ function isRealShellPipe(line, index) {
176
+ if (isIndexInQuotes(line, index))
177
+ return false;
178
+ // Verificar se o | está precedido por $ ou ` (subshell) - isso é pipe real
179
+ const before = line.slice(0, index);
180
+ // Se | está dentro de expressão $() ou `` , é pipe dentro de subshell - ainda é pipe real
181
+ // Mas se está dentro de '...' ou "..." já foi filtrado acima
182
+ return true;
183
+ }
184
+ /**
185
+ * Aplica espaçamento em redirecionamento simples: >file → > file
186
+ * Mas preserva redirecionamentos atômicos (2>&1, &>, etc.)
187
+ */
188
+ function aplicarEspacamentoRedirecionamento(line) {
189
+ // Primeiro, proteger redirecionamentos atômicos com placeholders
190
+ const atomicos = [];
191
+ let result = line.replace(
192
+ // Captura operadores de redirecionamento atômicos (compostos por 2+ caracteres):
193
+ // &>, &>>, 2>, 2>>, 2>&1, 3>&-, |&, <<, <<-, <>, >|, >>, >&, <&
194
+ // NÃO captura > ou < simples (esses devem ganhar espaço)
195
+ /(?:\d+>&?\d*|&>>?|<<[-]?|\|&|<>|>[\|]|>>|>&|<&)/g, (match) => {
196
+ const id = atomicos.length;
197
+ atomicos.push(match);
198
+ return `\x00ATOMIC_REDIR_${id}\x00`;
199
+ });
200
+ // Agora aplicar espaçamento em redirecionamentos simples restantes
201
+ // cmd>file → cmd > file (espaço antes e depois)
202
+ result = result.replace(/(?<!\x00)([^\s><])([><])([^\s><])/g, '$1 $2 $3');
203
+ // Restaurar placeholders
204
+ result = result.replace(/\x00ATOMIC_REDIR_(\d+)\x00/g, (_m, id) => {
205
+ return atomicos[Number(id)] ?? _m;
206
+ });
207
+ return result;
208
+ }
132
209
  function corrigirEspacamentoShell(lines) {
133
210
  const out = [];
134
211
  let ctx = {
@@ -152,6 +229,11 @@ function corrigirEspacamentoShell(lines) {
152
229
  out.push(output);
153
230
  continue;
154
231
  }
232
+ // Shebang (#!): preservar intacto, sem espaços
233
+ if (/^\s*#!/.test(output)) {
234
+ out.push(output);
235
+ continue;
236
+ }
155
237
  // Comentários: garantir espaço após #
156
238
  if (/^\s*#\S/.test(output)) {
157
239
  out.push(output.replace(/^(\s*)#(\S)/, '$1# $2'));
@@ -163,23 +245,44 @@ function corrigirEspacamentoShell(lines) {
163
245
  out.push(output.replace(/^(\s*[a-zA-Z_][\w]*)\s+=\s/, '$1='));
164
246
  continue;
165
247
  }
166
- // Espaçamento em pipes: | → | (espaços em torno)
248
+ // Espaçamento em pipes reais do shell (fora de aspas/strings): cmd|cmdcmd | cmd
167
249
  if (/\S\|\S/.test(output)) {
168
- out.push(output.replace(/(\S)\|(\S)/g, '$1 | $2'));
169
- continue;
170
- }
171
- // Espaçamento em redirecionamentos: >file → > file
172
- if (/\S[><]\S/.test(output)) {
173
- out.push(output.replace(/(\S)([><])(\S)/g, '$1 $2 $3'));
174
- continue;
250
+ // Só aplicar espaços em pipes que são reais do shell (fora de aspas)
251
+ const withPipeSpaces = output.replace(/(\S)\|(\S)/g, (match, before, after, offset) => {
252
+ if (isRealShellPipe(output, offset + before.length)) {
253
+ return `${before} | ${after}`;
254
+ }
255
+ return match;
256
+ });
257
+ // Se houve mudança, continue; senão cai nos próximos handlers
258
+ if (withPipeSpaces !== output) {
259
+ out.push(aplicarEspacamentoRedirecionamento(withPipeSpaces));
260
+ continue;
261
+ }
175
262
  }
176
- out.push(output);
263
+ // Espaçamento em redirecionamentos: >file → > file (preservando atômicos)
264
+ out.push(aplicarEspacamentoRedirecionamento(output));
177
265
  }
178
266
  return out;
179
267
  }
268
+ /* ────────────────────────── Validação de sintaxe ────────────────────────── */
269
+ /**
270
+ * Valida a sintaxe shell usando bash -n.
271
+ * Retorna true se a sintaxe for válida, false caso contrário.
272
+ */
273
+ function validarSintaxeShell(code) {
274
+ try {
275
+ execSync('bash -n', { input: code, timeout: 5000, stdio: ['pipe', 'ignore', 'ignore'] });
276
+ return true;
277
+ }
278
+ catch {
279
+ return false;
280
+ }
281
+ }
180
282
  /* ────────────────────────── Public API ────────────────────────── */
181
283
  /**
182
284
  * Shell formatter com detecção de contexto para here-docs, quoting e continuação.
285
+ * Inclui gate de segurança: se bash -n falhar, retorna o código original sem formatação.
183
286
  */
184
287
  export function formatarShellMinimo(code) {
185
288
  const normalized = normalizarFimDeLinha(removerBom(code));
@@ -193,6 +296,22 @@ export function formatarShellMinimo(code) {
193
296
  const formatted = normalizarNewlinesFinais(comPontuacao);
194
297
  const baseline = normalizarNewlinesFinais(normalized);
195
298
  const anyChanged = formatted !== baseline;
299
+ // Gate de segurança: validar sintaxe com bash -n, reverter se falhar
300
+ if (anyChanged && lines.length > 1) {
301
+ try {
302
+ execSync('bash -n', { input: formatted, timeout: 5000, stdio: ['pipe', 'ignore', 'ignore'] });
303
+ }
304
+ catch {
305
+ // bash -n falhou → reverter para o código original
306
+ return {
307
+ ok: true,
308
+ parser: 'shell',
309
+ formatted: baseline,
310
+ changed: false,
311
+ reason: 'shell-syntax-error-revert'
312
+ };
313
+ }
314
+ }
196
315
  return {
197
316
  ok: true,
198
317
  parser: 'shell',
@@ -30,6 +30,9 @@ export class DedupeManager {
30
30
  export function runUniqueVisitor(ast, relPath, visitor, initialState = {}) {
31
31
  const manager = new DedupeManager(relPath);
32
32
  const state = { ...initialState, manager, relPath };
33
+ if (ast === null || ast === undefined) {
34
+ return [];
35
+ }
33
36
  const nodeToTraverse = ast.node ?? ast;
34
37
  if (!nodeToTraverse) {
35
38
  return [];
@@ -6,7 +6,7 @@
6
6
  * Não toca disco; apenas retorna o novo conteúdo.
7
7
  */
8
8
  import path from 'node:path';
9
- import { normalizePath } from './index.js';
9
+ import { normalizePath } from './path.js';
10
10
  /**
11
11
  * Normaliza o caminho de import para uma chave consistente (POSIX).
12
12
  */
@@ -13,6 +13,13 @@ export async function lerRelatorioVersionado(options) {
13
13
  try {
14
14
  // Ler arquivo
15
15
  const conteudo = await lerEstado(caminho);
16
+ // Verificar se arquivo existe
17
+ if (conteudo === null || conteudo === undefined) {
18
+ return {
19
+ sucesso: false,
20
+ erro: 'Arquivo não encontrado ou vazio',
21
+ };
22
+ }
16
23
  let relatorioFinal = conteudo;
17
24
  let migrado = false;
18
25
  // Validar schema se solicitado
@@ -92,6 +99,12 @@ export async function lerDadosRelatorio(caminho) {
92
99
  export async function verificarSchemaRelatorio(caminho) {
93
100
  try {
94
101
  const conteudo = await lerEstado(caminho);
102
+ if (conteudo === null || conteudo === undefined) {
103
+ return {
104
+ valido: false,
105
+ erros: ['Arquivo não encontrado ou vazio'],
106
+ };
107
+ }
95
108
  const validacao = validarSchema(conteudo);
96
109
  return {
97
110
  valido: validacao.valido,
@@ -268,9 +268,9 @@ export function normalizarRel(relPath) {
268
268
  export async function carregarConfigEstrategia(baseDir, overrides) {
269
269
  const caminho = path.join(baseDir, '.mahoraga', 'estrutura.json');
270
270
  const lido = await lerEstado(caminho);
271
- const cfgObj = !Array.isArray(lido) && typeof lido === 'object' ? lido : {};
271
+ const cfgObj = lido !== null && !Array.isArray(lido) && typeof lido === 'object' ? lido : {};
272
272
  const nomePreset = overrides?.preset ?? cfgObj['preset'];
273
- const preset = nomePreset && PRESETS[nomePreset].nome ? PRESETS[nomePreset] : undefined;
273
+ const preset = nomePreset && PRESETS[nomePreset]?.nome ? PRESETS[nomePreset] : undefined;
274
274
  const base = {
275
275
  raizCodigo: 'src',
276
276
  criarSubpastasPorEntidade: true,
@@ -0,0 +1,38 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * Type guard: verifica se um valor é um GuardianResult válido.
4
+ * Aceita null/undefined como válido (representa "sem resultado ainda").
5
+ */
6
+ export function isGuardianResult(value) {
7
+ if (value === null || value === undefined) {
8
+ return true;
9
+ }
10
+ if (typeof value !== 'object') {
11
+ return false;
12
+ }
13
+ const obj = value;
14
+ if (typeof obj.status !== 'string') {
15
+ return false;
16
+ }
17
+ const knownStatuses = ['ok', 'erro', 'alteracoes-detectadas', 'baseline-aceito', 'baseline-criado'];
18
+ return knownStatuses.includes(obj.status);
19
+ }
20
+ /**
21
+ * Converte/conforma um GuardianResult.
22
+ * - undefined → null
23
+ * - status 'baseline-aceito' → 'ok'
24
+ * - status desconhecido retorna null
25
+ */
26
+ export function converterResultadoGuardian(result) {
27
+ if (result === undefined) {
28
+ return null;
29
+ }
30
+ if (result.status === 'baseline-aceito') {
31
+ return { ...result, status: 'ok' };
32
+ }
33
+ const knownStatuses = ['ok', 'erro', 'alteracoes-detectadas'];
34
+ if (!knownStatuses.includes(result.status)) {
35
+ return null;
36
+ }
37
+ return result;
38
+ }
@@ -0,0 +1,12 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * Verifica se um valor é uma PendenciaProcessavel válida.
4
+ * Deve ter `arquivo` (string) e `motivo` (string).
5
+ */
6
+ export function isPendenciaProcessavel(value) {
7
+ if (value === null || value === undefined || typeof value !== 'object') {
8
+ return false;
9
+ }
10
+ const obj = value;
11
+ return typeof obj.arquivo === 'string' && typeof obj.motivo === 'string';
12
+ }
@@ -0,0 +1,111 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { SharedCommonMensagens } from '../../core/messages/index.js';
3
+ /**
4
+ * Extrai mensagem de erro de qualquer valor de forma segura.
5
+ */
6
+ export function extrairMensagemErro(err) {
7
+ if (err === null || err === undefined) {
8
+ return SharedCommonMensagens?.erroDesconhecido ?? 'Erro desconhecido';
9
+ }
10
+ if (err instanceof Error) {
11
+ return err.message;
12
+ }
13
+ if (typeof err === 'string') {
14
+ return err;
15
+ }
16
+ if (typeof err === 'object') {
17
+ const obj = err;
18
+ if (typeof obj.message === 'string') {
19
+ return obj.message;
20
+ }
21
+ if (obj.message instanceof Error) {
22
+ return obj.message.message;
23
+ }
24
+ }
25
+ return SharedCommonMensagens?.erroDesconhecido ?? 'Erro desconhecido';
26
+ }
27
+ /**
28
+ * Valida um valor com type guard e retorna fallback se falhar.
29
+ */
30
+ export function validarSeguro(validador, valor, fallback) {
31
+ if (validador(valor)) {
32
+ return valor;
33
+ }
34
+ console.warn('validação falhou, usando fallback', valor);
35
+ return fallback;
36
+ }
37
+ /**
38
+ * Converte um valor com type guard e retorna fallback se falhar.
39
+ */
40
+ export function converterSeguro(valor, validador, fallback) {
41
+ if (validador(valor)) {
42
+ return valor;
43
+ }
44
+ return fallback;
45
+ }
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ export function isErroComMensagem(value) {
48
+ if (value === null || value === undefined) {
49
+ return false;
50
+ }
51
+ if (typeof value !== 'object') {
52
+ return false;
53
+ }
54
+ return typeof value.message === 'string';
55
+ }
56
+ export function isGlobalComVitest(value) {
57
+ if (value === null || value === undefined || typeof value !== 'object') {
58
+ return false;
59
+ }
60
+ return 'vi' in value;
61
+ }
62
+ export function isIntlComDisplayNames(value) {
63
+ if (value === null || value === undefined || typeof value !== 'object') {
64
+ return false;
65
+ }
66
+ // Aceita tanto se tiver DisplayNames quanto qualquer objeto (fallback)
67
+ return true;
68
+ }
69
+ export function isGlobalComImport(value) {
70
+ if (value === null || value === undefined || typeof value !== 'object') {
71
+ return false;
72
+ }
73
+ // Aceita tanto se tiver import quanto qualquer objeto (fallback)
74
+ return true;
75
+ }
76
+ export function isConfigPlugin(value) {
77
+ return value !== null && value !== undefined && typeof value === 'object';
78
+ }
79
+ export function isPlanoSugestaoEstrutura(value) {
80
+ if (value === null || value === undefined || typeof value !== 'object') {
81
+ return false;
82
+ }
83
+ const obj = value;
84
+ return Array.isArray(obj.mover);
85
+ }
86
+ export function isSnapshotAnalise(value) {
87
+ if (value === null || value === undefined || typeof value !== 'object') {
88
+ return false;
89
+ }
90
+ const obj = value;
91
+ return typeof obj.timestamp === 'string' && Array.isArray(obj.arquivos);
92
+ }
93
+ export function isEntradaMapaReversao(value) {
94
+ if (value === null || value === undefined || typeof value !== 'object') {
95
+ return false;
96
+ }
97
+ const obj = value;
98
+ if (typeof obj.arquivo !== 'string') {
99
+ return false;
100
+ }
101
+ return ['adicionar', 'remover', 'modificar'].includes(obj.operacao);
102
+ }
103
+ export function isStringNaoVazia(value) {
104
+ return typeof value === 'string' && value.trim().length > 0;
105
+ }
106
+ export function isNumeroValido(value) {
107
+ return typeof value === 'number' && Number.isFinite(value);
108
+ }
109
+ export function isArrayNaoVazio(value) {
110
+ return Array.isArray(value) && value.length > 0;
111
+ }
@@ -31,8 +31,13 @@ export async function scanVulnerabilities(opts = {}) {
31
31
  }
32
32
  catch (err) {
33
33
  if (isNpmAuditError(err)) {
34
- const parsed = JSON.parse(err.stdout);
35
- return normalizeAuditResult(parsed);
34
+ try {
35
+ const parsed = JSON.parse(err.stdout);
36
+ return normalizeAuditResult(parsed);
37
+ }
38
+ catch {
39
+ return emptyResult();
40
+ }
36
41
  }
37
42
  return emptyResult();
38
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mocoto/mahoraga",
3
- "version": "0.14.7",
3
+ "version": "0.14.8",
4
4
  "type": "module",
5
5
  "description": "C.L.I modular para análise, diagnóstico e manutenção de projetos JavaScript/TypeScript e multi-stack leve",
6
6
  "author": "rei macaco",
@@ -122,7 +122,7 @@
122
122
  "chalk": "^5.6.2",
123
123
  "commander": "^15.0.0",
124
124
  "dotenv": "^17.4.2",
125
- "fast-xml-parser": "^5.9.3",
125
+ "fast-xml-parser": "^5.10.0",
126
126
  "htmlparser2": "^12.0.0",
127
127
  "marked": "^18.0.6",
128
128
  "micromatch": "^4.0.8",
@@ -130,7 +130,7 @@
130
130
  "openpgp": "^6.3.1",
131
131
  "ora": "^9.4.1",
132
132
  "p-limit": "^7.3.0",
133
- "postcss": "^8.5.16",
133
+ "postcss": "^8.5.17",
134
134
  "postcss-sass": "^0.5.0",
135
135
  "postcss-scss": "^4.0.9",
136
136
  "xxhashjs": "^0.2.2",