@semacode/cli 1.5.29 → 1.5.31
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/dist/comandos.js +69 -0
- package/dist/comandos.js.map +1 -1
- package/dist/dev/index.d.ts +18 -0
- package/dist/dev/index.js +143 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/guard.d.ts +35 -0
- package/dist/guard.js +164 -0
- package/dist/guard.js.map +1 -0
- package/dist/index.js.map +1 -1
- package/dist/init/index.d.ts +23 -0
- package/dist/init/index.js +112 -0
- package/dist/init/index.js.map +1 -0
- package/dist/init/templates.d.ts +11 -0
- package/dist/init/templates.js +660 -0
- package/dist/init/templates.js.map +1 -0
- package/dist/sync/index.d.ts +24 -0
- package/dist/sync/index.js +174 -0
- package/dist/sync/index.js.map +1 -0
- package/exemplos/.prepack-generated +1 -0
- package/node_modules/@sema/gerador-css/package.json +14 -7
- package/node_modules/@sema/gerador-css/src/index.ts +605 -0
- package/node_modules/@sema/gerador-css/tsconfig.json +13 -0
- package/node_modules/@sema/gerador-css/tsconfig.tsbuildinfo +1 -0
- package/node_modules/@sema/gerador-dart/package.json +14 -7
- package/node_modules/@sema/gerador-dart/src/index.ts +52 -0
- package/node_modules/@sema/gerador-dart/tsconfig.json +13 -0
- package/node_modules/@sema/gerador-dart/tsconfig.tsbuildinfo +1 -0
- package/node_modules/@sema/gerador-html/package.json +14 -7
- package/node_modules/@sema/gerador-html/src/index.ts +185 -0
- package/node_modules/@sema/gerador-html/tsconfig.json +13 -0
- package/node_modules/@sema/gerador-html/tsconfig.tsbuildinfo +1 -0
- package/node_modules/@sema/gerador-javascript/package.json +14 -7
- package/node_modules/@sema/gerador-javascript/src/index.ts +461 -0
- package/node_modules/@sema/gerador-javascript/tsconfig.json +13 -0
- package/node_modules/@sema/gerador-javascript/tsconfig.tsbuildinfo +1 -0
- package/node_modules/@sema/gerador-lua/package.json +14 -7
- package/node_modules/@sema/gerador-lua/src/index.ts +359 -0
- package/node_modules/@sema/gerador-lua/tsconfig.json +13 -0
- package/node_modules/@sema/gerador-lua/tsconfig.tsbuildinfo +1 -0
- package/node_modules/@sema/gerador-python/package.json +14 -7
- package/node_modules/@sema/gerador-python/src/index.ts +850 -0
- package/node_modules/@sema/gerador-python/tsconfig.json +13 -0
- package/node_modules/@sema/gerador-python/tsconfig.tsbuildinfo +1 -0
- package/node_modules/@sema/gerador-typescript/package.json +14 -7
- package/node_modules/@sema/gerador-typescript/src/index.ts +876 -0
- package/node_modules/@sema/gerador-typescript/tsconfig.json +13 -0
- package/node_modules/@sema/gerador-typescript/tsconfig.tsbuildinfo +1 -0
- package/node_modules/@sema/nucleo/dist/diagnosticos/melhorador.d.ts +22 -0
- package/node_modules/@sema/nucleo/dist/diagnosticos/melhorador.js +97 -0
- package/node_modules/@sema/nucleo/dist/diagnosticos/melhorador.js.map +1 -0
- package/node_modules/@sema/nucleo/dist/index.d.ts +1 -0
- package/node_modules/@sema/nucleo/dist/index.js +1 -0
- package/node_modules/@sema/nucleo/dist/index.js.map +1 -1
- package/node_modules/@sema/nucleo/package.json +10 -7
- package/node_modules/@sema/nucleo/src/ast/tipos.ts +207 -0
- package/node_modules/@sema/nucleo/src/diagnosticos/index.ts +43 -0
- package/node_modules/@sema/nucleo/src/diagnosticos/melhorador.ts +130 -0
- package/node_modules/@sema/nucleo/src/formatador/index.ts +530 -0
- package/node_modules/@sema/nucleo/src/index.ts +184 -0
- package/node_modules/@sema/nucleo/src/ir/conversor.ts +1037 -0
- package/node_modules/@sema/nucleo/src/ir/modelos.ts +403 -0
- package/node_modules/@sema/nucleo/src/lexer/lexer.ts +166 -0
- package/node_modules/@sema/nucleo/src/lexer/tokens.ts +79 -0
- package/node_modules/@sema/nucleo/src/parser/gramatica.ebnf +41 -0
- package/node_modules/@sema/nucleo/src/parser/parser.ts +936 -0
- package/node_modules/@sema/nucleo/src/persistencia/contratos.ts +379 -0
- package/node_modules/@sema/nucleo/src/semantico/analisador.ts +3126 -0
- package/node_modules/@sema/nucleo/src/semantico/estruturas.ts +665 -0
- package/node_modules/@sema/nucleo/src/semantico/seguranca.ts +362 -0
- package/node_modules/@sema/nucleo/src/util/arquivos.ts +28 -0
- package/node_modules/@sema/nucleo/tsconfig.json +9 -0
- package/node_modules/@sema/nucleo/tsconfig.tsbuildinfo +1 -0
- package/node_modules/@sema/padroes/package.json +10 -7
- package/node_modules/@sema/padroes/src/index.ts +382 -0
- package/node_modules/@sema/padroes/tsconfig.json +9 -0
- package/node_modules/@sema/padroes/tsconfig.tsbuildinfo +1 -0
- package/package.json +74 -75
- package/AGENTS.md +0 -294
- package/AGENT_CONTEXT_PACK.json +0 -164
- package/LICENSE +0 -22
- package/SEMA_BRIEF.curto.txt +0 -11
- package/SEMA_BRIEF.md +0 -511
- package/SEMA_BRIEF.micro.txt +0 -9
- package/SEMA_INDEX.json +0 -7588
- package/docs/AGENT_STARTER.md +0 -109
- package/docs/api.md +0 -82
- package/docs/cli.md +0 -175
- package/docs/como-ensinar-a-sema-para-ia.md +0 -155
- package/docs/deploy.md +0 -93
- package/docs/documentacao.md +0 -88
- package/docs/env.md +0 -105
- package/docs/extensao-vscode.md +0 -53
- package/docs/fluxo-pratico-ia-sema.md +0 -187
- package/docs/instalacao-e-primeiro-uso.md +0 -134
- package/docs/integracao-com-ia.md +0 -110
- package/docs/mcp.md +0 -292
- package/docs/pagamento-ponta-a-ponta.md +0 -171
- package/docs/persistencia-vendor-first.md +0 -151
- package/docs/prompt-base-ia-sema.md +0 -111
- package/docs/repositories.md +0 -54
- package/docs/rollback.md +0 -49
- package/docs/seguranca.md +0 -126
- package/docs/sintaxe.md +0 -218
- package/llms-full.txt +0 -35
- package/llms.txt +0 -18
|
@@ -0,0 +1,3126 @@
|
|
|
1
|
+
import { criarDiagnostico, type Diagnostico } from "../diagnosticos/index.js";
|
|
2
|
+
import type {
|
|
3
|
+
BlocoCasoTesteAst,
|
|
4
|
+
BlocoGenericoAst,
|
|
5
|
+
CampoAst,
|
|
6
|
+
EntityAst,
|
|
7
|
+
EnumAst,
|
|
8
|
+
FlowAst,
|
|
9
|
+
ModuloAst,
|
|
10
|
+
RouteAst,
|
|
11
|
+
StateAst,
|
|
12
|
+
TaskAst,
|
|
13
|
+
TypeAst,
|
|
14
|
+
} from "../ast/tipos.js";
|
|
15
|
+
import {
|
|
16
|
+
CAMPOS_DATABASE_SUPORTADOS,
|
|
17
|
+
CAMPOS_RECURSO_PERSISTENCIA_SUPORTADOS,
|
|
18
|
+
classificarCompatibilidadePersistencia,
|
|
19
|
+
nomeTipoRecursoPersistencia,
|
|
20
|
+
normalizarConsistenciaPersistencia,
|
|
21
|
+
normalizarDurabilidadePersistencia,
|
|
22
|
+
normalizarEngineBanco,
|
|
23
|
+
normalizarModeloConsultaPersistencia,
|
|
24
|
+
normalizarModeloTransacaoPersistencia,
|
|
25
|
+
parsearBooleanoPersistencia,
|
|
26
|
+
recursoPersistenciaPodeSerPortavel,
|
|
27
|
+
} from "../persistencia/contratos.js";
|
|
28
|
+
import {
|
|
29
|
+
ehCategoriaEfeitoSemantico,
|
|
30
|
+
ehCriticidadeEfeitoSemantico,
|
|
31
|
+
extrairReferenciasDaExpressao,
|
|
32
|
+
parsearEfeitoSemantico,
|
|
33
|
+
parsearEtapaFlow,
|
|
34
|
+
parsearExpressaoSemantica,
|
|
35
|
+
parsearTransicaoEstado,
|
|
36
|
+
} from "./estruturas.js";
|
|
37
|
+
import {
|
|
38
|
+
CLASSIFICACOES_DADO_SUPORTADAS,
|
|
39
|
+
MODOS_AUTH_SUPORTADOS,
|
|
40
|
+
MOTIVOS_AUDIT_SUPORTADOS,
|
|
41
|
+
ORIGENS_AUTH_SUPORTADAS,
|
|
42
|
+
PRINCIPAIS_AUTH_SUPORTADOS,
|
|
43
|
+
REDACOES_LOG_SUPORTADAS,
|
|
44
|
+
TENANTS_AUTHZ_SUPORTADOS,
|
|
45
|
+
contratoDadosTemSegredoOuCredencial,
|
|
46
|
+
contratoDadosTemSensivel,
|
|
47
|
+
extrairContratoAudit,
|
|
48
|
+
extrairContratoAuth,
|
|
49
|
+
extrairContratoAuthz,
|
|
50
|
+
extrairContratoDados,
|
|
51
|
+
extrairContratoForbidden,
|
|
52
|
+
extrairContratoSegredos,
|
|
53
|
+
efeitoEhPrivilegiado,
|
|
54
|
+
efeitoRequerSegredo,
|
|
55
|
+
forbiddenContemRegra,
|
|
56
|
+
} from "./seguranca.js";
|
|
57
|
+
|
|
58
|
+
export interface SimboloSemantico {
|
|
59
|
+
nome: string;
|
|
60
|
+
categoria: "tipo" | "entity" | "enum" | "task" | "flow" | "route" | "state" | "worker" | "evento" | "fila" | "cron" | "webhook" | "cache" | "storage" | "policy" | "database";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CampoSemantico {
|
|
64
|
+
nome: string;
|
|
65
|
+
tipo: string;
|
|
66
|
+
modificadores: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ErroSemanticoTask {
|
|
70
|
+
codigo: string;
|
|
71
|
+
mensagem: string;
|
|
72
|
+
categoria?: string;
|
|
73
|
+
recuperabilidade?: string;
|
|
74
|
+
acaoChamador?: string;
|
|
75
|
+
impactaEstado?: boolean;
|
|
76
|
+
requerCompensacao?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ResumoTaskSemantico {
|
|
80
|
+
input: CampoSemantico[];
|
|
81
|
+
output: CampoSemantico[];
|
|
82
|
+
errors: ErroSemanticoTask[];
|
|
83
|
+
guarantees: string[];
|
|
84
|
+
implementacoes: ImplementacaoTaskSemantica[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface InteropSemantico {
|
|
88
|
+
origem: "ts" | "py" | "dart" | "lua" | "cs" | "java" | "go" | "rust" | "cpp";
|
|
89
|
+
caminho: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ImplementacaoTaskSemantica {
|
|
93
|
+
origem: "ts" | "py" | "dart" | "lua" | "cs" | "java" | "go" | "rust" | "cpp";
|
|
94
|
+
caminho: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ContextoSemantico {
|
|
98
|
+
modulo: string;
|
|
99
|
+
simbolos: Map<string, SimboloSemantico>;
|
|
100
|
+
tiposConhecidos: Set<string>;
|
|
101
|
+
tasksConhecidas: Set<string>;
|
|
102
|
+
tarefasDetalhadas: Map<string, ResumoTaskSemantico>;
|
|
103
|
+
statesConhecidos: Map<string, { transicoes: Set<string> }>;
|
|
104
|
+
modulosImportados: string[];
|
|
105
|
+
interoperabilidades: InteropSemantico[];
|
|
106
|
+
enumsConhecidos: Map<string, Set<string>>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ResultadoSemantico {
|
|
110
|
+
contexto: ContextoSemantico;
|
|
111
|
+
diagnosticos: Diagnostico[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface OpcoesAnaliseSemantica {
|
|
115
|
+
contextosModulos?: Map<string, ContextoSemantico>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function ehUseInterop(
|
|
119
|
+
use: ModuloAst["uses"][number],
|
|
120
|
+
): use is ModuloAst["uses"][number] & { origem: InteropSemantico["origem"] } {
|
|
121
|
+
return use.origem !== "sema";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const TIPOS_PRIMITIVOS = new Set([
|
|
125
|
+
"Texto",
|
|
126
|
+
"Numero",
|
|
127
|
+
"Inteiro",
|
|
128
|
+
"Decimal",
|
|
129
|
+
"Booleano",
|
|
130
|
+
"Data",
|
|
131
|
+
"DataHora",
|
|
132
|
+
"Timestamp",
|
|
133
|
+
"Id",
|
|
134
|
+
"Email",
|
|
135
|
+
"Url",
|
|
136
|
+
"Json",
|
|
137
|
+
"Objeto",
|
|
138
|
+
"Vazio",
|
|
139
|
+
]);
|
|
140
|
+
const TIPOS_COMPOSTOS_SUPORTADOS = new Set(["Lista", "Mapa", "Opcional", "Ou"]);
|
|
141
|
+
const CAMPOS_VINCULO_SUPORTADOS = new Set([
|
|
142
|
+
"arquivo",
|
|
143
|
+
"simbolo",
|
|
144
|
+
"recurso",
|
|
145
|
+
"superficie",
|
|
146
|
+
"rota",
|
|
147
|
+
"teste",
|
|
148
|
+
"tabela",
|
|
149
|
+
"fila",
|
|
150
|
+
"job",
|
|
151
|
+
"policy",
|
|
152
|
+
"artefato",
|
|
153
|
+
"evento",
|
|
154
|
+
"cache",
|
|
155
|
+
"storage",
|
|
156
|
+
"worker",
|
|
157
|
+
"cron",
|
|
158
|
+
"webhook",
|
|
159
|
+
]);
|
|
160
|
+
const CAMPOS_EXECUCAO_SUPORTADOS = new Set([
|
|
161
|
+
"idempotencia",
|
|
162
|
+
"timeout",
|
|
163
|
+
"retry",
|
|
164
|
+
"compensacao",
|
|
165
|
+
"criticidade_operacional",
|
|
166
|
+
]);
|
|
167
|
+
const CAMPOS_AUTH_SUPORTADOS = new Set([
|
|
168
|
+
"modo",
|
|
169
|
+
"estrategia",
|
|
170
|
+
"principal",
|
|
171
|
+
"origem",
|
|
172
|
+
]);
|
|
173
|
+
const CAMPOS_AUTHZ_SUPORTADOS = new Set([
|
|
174
|
+
"papel",
|
|
175
|
+
"papeis",
|
|
176
|
+
"escopo",
|
|
177
|
+
"escopos",
|
|
178
|
+
"politica",
|
|
179
|
+
"tenant",
|
|
180
|
+
]);
|
|
181
|
+
const CAMPOS_DADOS_SUPORTADOS = new Set([
|
|
182
|
+
"classificacao_padrao",
|
|
183
|
+
"redacao_log",
|
|
184
|
+
"retencao",
|
|
185
|
+
]);
|
|
186
|
+
const CAMPOS_AUDIT_SUPORTADOS = new Set([
|
|
187
|
+
"evento",
|
|
188
|
+
"ator",
|
|
189
|
+
"correlacao",
|
|
190
|
+
"retencao",
|
|
191
|
+
"motivo",
|
|
192
|
+
]);
|
|
193
|
+
const CAMPOS_SEGREDO_SUPORTADOS = new Set([
|
|
194
|
+
"origem",
|
|
195
|
+
"escopo",
|
|
196
|
+
"acesso",
|
|
197
|
+
"rotacao",
|
|
198
|
+
"nao_logar",
|
|
199
|
+
"nao_retornar",
|
|
200
|
+
"mascarar",
|
|
201
|
+
]);
|
|
202
|
+
const CAMPOS_ERRO_OPERACIONAL = new Set([
|
|
203
|
+
"mensagem",
|
|
204
|
+
"categoria",
|
|
205
|
+
"recuperabilidade",
|
|
206
|
+
"acao_chamador",
|
|
207
|
+
"impacta_estado",
|
|
208
|
+
"requer_compensacao",
|
|
209
|
+
]);
|
|
210
|
+
const CRITICIDADES_OPERACIONAIS = new Set(["baixa", "media", "alta", "critica"]);
|
|
211
|
+
type PerfilCompatibilidadeSemantica = "publico" | "interno" | "experimental" | "legado" | "deprecado";
|
|
212
|
+
|
|
213
|
+
const PADRAO_CAMINHO_INTEROP = /^[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z_][A-Za-z0-9_-]*)*$/;
|
|
214
|
+
|
|
215
|
+
function normalizarOrigemImplementacao(valor: string): ImplementacaoTaskSemantica["origem"] | undefined {
|
|
216
|
+
switch (valor.toLowerCase()) {
|
|
217
|
+
case "ts":
|
|
218
|
+
case "typescript":
|
|
219
|
+
return "ts";
|
|
220
|
+
case "py":
|
|
221
|
+
case "python":
|
|
222
|
+
return "py";
|
|
223
|
+
case "dart":
|
|
224
|
+
return "dart";
|
|
225
|
+
case "lua":
|
|
226
|
+
return "lua";
|
|
227
|
+
case "cs":
|
|
228
|
+
case "csharp":
|
|
229
|
+
case "dotnet":
|
|
230
|
+
return "cs";
|
|
231
|
+
case "java":
|
|
232
|
+
return "java";
|
|
233
|
+
case "go":
|
|
234
|
+
case "golang":
|
|
235
|
+
return "go";
|
|
236
|
+
case "rust":
|
|
237
|
+
case "rs":
|
|
238
|
+
return "rust";
|
|
239
|
+
case "cpp":
|
|
240
|
+
case "cxx":
|
|
241
|
+
case "cc":
|
|
242
|
+
case "c++":
|
|
243
|
+
return "cpp";
|
|
244
|
+
default:
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function extrairReferenciasDeTipos(texto: string): string[] {
|
|
250
|
+
const correspondencias = texto.match(/[A-Z][A-Za-z0-9_]*/g);
|
|
251
|
+
return (correspondencias ?? []).filter((referencia) => !TIPOS_COMPOSTOS_SUPORTADOS.has(referencia));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function extrairRaiz(referencia: string): string {
|
|
255
|
+
return referencia.split(".")[0] ?? referencia;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function ehMarcadorSemantico(referencia: string): boolean {
|
|
259
|
+
return ["persistencia", "sucesso", "estado"].includes(extrairRaiz(referencia));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function diagnosticoDuplicado(nome: string, categoria: string, intervalo?: CampoAst["intervalo"]): Diagnostico {
|
|
263
|
+
return criarDiagnostico(
|
|
264
|
+
"SEM001",
|
|
265
|
+
`${categoria} "${nome}" foi declarado mais de uma vez no mesmo modulo.`,
|
|
266
|
+
"erro",
|
|
267
|
+
intervalo,
|
|
268
|
+
"Use nomes unicos para simbolos do modulo.",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function validarCamposDeTipos(
|
|
273
|
+
campos: CampoAst[],
|
|
274
|
+
tiposConhecidos: Set<string>,
|
|
275
|
+
diagnosticos: Diagnostico[],
|
|
276
|
+
contexto: string,
|
|
277
|
+
): void {
|
|
278
|
+
for (const campo of campos) {
|
|
279
|
+
const referencias = extrairReferenciasDeTipos(campo.valor);
|
|
280
|
+
for (const referencia of referencias) {
|
|
281
|
+
if (!tiposConhecidos.has(referencia)) {
|
|
282
|
+
diagnosticos.push(
|
|
283
|
+
criarDiagnostico(
|
|
284
|
+
"SEM002",
|
|
285
|
+
`Tipo "${referencia}" nao foi encontrado em ${contexto}.`,
|
|
286
|
+
"erro",
|
|
287
|
+
campo.intervalo,
|
|
288
|
+
"Declare o tipo, entidade ou enum antes de usa-lo.",
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function localizarBloco(corpo: BlocoGenericoAst | undefined, nome: string): BlocoGenericoAst | undefined {
|
|
297
|
+
if (!corpo) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
return corpo.blocos.find((bloco): bloco is BlocoGenericoAst =>
|
|
301
|
+
bloco.tipo === "bloco_generico" && (bloco.palavraChave === nome || bloco.nome === nome));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function localizarCampo(bloco: BlocoGenericoAst | undefined, ...nomes: string[]): CampoAst | undefined {
|
|
305
|
+
if (!bloco) {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
return bloco.campos.find((campo) => nomes.includes(campo.nome));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function valorCampoCompleto(campo?: CampoAst): string | undefined {
|
|
312
|
+
if (!campo) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
return [campo.valor, ...campo.modificadores].join(" ").trim() || undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function parsearBooleanoSemantico(valor?: string): boolean | undefined {
|
|
319
|
+
if (!valor) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
if (valor === "verdadeiro" || valor === "true") {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
if (valor === "falso" || valor === "false") {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function converterCampoSemantico(campo: CampoAst): CampoSemantico {
|
|
332
|
+
return {
|
|
333
|
+
nome: campo.nome,
|
|
334
|
+
tipo: campo.valor,
|
|
335
|
+
modificadores: [...campo.modificadores],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function indicesCampos(campos: CampoSemantico[]): Map<string, CampoSemantico> {
|
|
340
|
+
return new Map(campos.map((campo) => [campo.nome, campo]));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function indiceErros(erros: ErroSemanticoTask[]): Map<string, ErroSemanticoTask> {
|
|
344
|
+
return new Map(erros.map((erro) => [erro.codigo, erro]));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function coletarErrosTask(task: TaskAst): ErroSemanticoTask[] {
|
|
348
|
+
const erros = new Map<string, ErroSemanticoTask>();
|
|
349
|
+
for (const campo of task.error?.campos ?? []) {
|
|
350
|
+
erros.set(campo.nome, {
|
|
351
|
+
codigo: campo.nome,
|
|
352
|
+
mensagem: [campo.valor, ...campo.modificadores].join(" ").trim(),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
for (const bloco of task.error?.blocos ?? []) {
|
|
357
|
+
if (bloco.tipo !== "bloco_generico") {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const codigo = bloco.nome ?? bloco.palavraChave;
|
|
361
|
+
if (!codigo || codigo === "desconhecido") {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
erros.set(codigo, {
|
|
366
|
+
codigo,
|
|
367
|
+
mensagem: valorCampoCompleto(localizarCampo(bloco, "mensagem")) ?? `Erro estruturado "${codigo}".`,
|
|
368
|
+
categoria: valorCampoCompleto(localizarCampo(bloco, "categoria")),
|
|
369
|
+
recuperabilidade: valorCampoCompleto(localizarCampo(bloco, "recuperabilidade")),
|
|
370
|
+
acaoChamador: valorCampoCompleto(localizarCampo(bloco, "acao_chamador")),
|
|
371
|
+
impactaEstado: parsearBooleanoSemantico(valorCampoCompleto(localizarCampo(bloco, "impacta_estado"))),
|
|
372
|
+
requerCompensacao: parsearBooleanoSemantico(valorCampoCompleto(localizarCampo(bloco, "requer_compensacao"))),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const bloco of task.tests?.blocos ?? []) {
|
|
377
|
+
if (bloco.tipo !== "caso_teste") {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
const codigoErro = bloco.error?.campos.find((campo) => campo.nome === "tipo")?.valor;
|
|
381
|
+
if (codigoErro && !erros.has(codigoErro)) {
|
|
382
|
+
erros.set(codigoErro, {
|
|
383
|
+
codigo: codigoErro,
|
|
384
|
+
mensagem: `Erro sintetico derivado do caso de teste "${bloco.nome}".`,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return [...erros.values()];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function coletarResumoTask(task: TaskAst): ResumoTaskSemantico {
|
|
393
|
+
return {
|
|
394
|
+
input: (task.input?.campos ?? []).map(converterCampoSemantico),
|
|
395
|
+
output: (task.output?.campos ?? []).map(converterCampoSemantico),
|
|
396
|
+
errors: coletarErrosTask(task),
|
|
397
|
+
guarantees: (task.guarantees?.linhas ?? []).map((linha) => linha.conteudo),
|
|
398
|
+
implementacoes: (task.impl?.campos ?? [])
|
|
399
|
+
.map((campo) => {
|
|
400
|
+
const origem = normalizarOrigemImplementacao(campo.nome);
|
|
401
|
+
return origem ? { origem, caminho: campo.valor } : undefined;
|
|
402
|
+
})
|
|
403
|
+
.filter((item): item is ImplementacaoTaskSemantica => Boolean(item)),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function validarImplementacoesTask(task: TaskAst, diagnosticos: Diagnostico[]): void {
|
|
408
|
+
if (!task.impl) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const origens = new Set<string>();
|
|
413
|
+
for (const campo of task.impl.campos) {
|
|
414
|
+
const origem = normalizarOrigemImplementacao(campo.nome);
|
|
415
|
+
if (!origem) {
|
|
416
|
+
diagnosticos.push(
|
|
417
|
+
criarDiagnostico(
|
|
418
|
+
"SEM059",
|
|
419
|
+
`Task "${task.nome}" declarou implementacao externa invalida em impl: "${campo.nome}".`,
|
|
420
|
+
"erro",
|
|
421
|
+
campo.intervalo,
|
|
422
|
+
"Use apenas ts, py, dart, lua, cs, java, go, rust ou cpp dentro do bloco impl.",
|
|
423
|
+
),
|
|
424
|
+
);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (origens.has(origem)) {
|
|
429
|
+
diagnosticos.push(
|
|
430
|
+
criarDiagnostico(
|
|
431
|
+
"SEM060",
|
|
432
|
+
`Task "${task.nome}" declarou mais de uma implementacao ${origem} no bloco impl.`,
|
|
433
|
+
"erro",
|
|
434
|
+
campo.intervalo,
|
|
435
|
+
"Cada origem externa deve aparecer no maximo uma vez dentro de impl.",
|
|
436
|
+
),
|
|
437
|
+
);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
origens.add(origem);
|
|
441
|
+
|
|
442
|
+
if (!PADRAO_CAMINHO_INTEROP.test(campo.valor)) {
|
|
443
|
+
diagnosticos.push(
|
|
444
|
+
criarDiagnostico(
|
|
445
|
+
"SEM061",
|
|
446
|
+
`Task "${task.nome}" declarou caminho invalido para impl ${origem}: "${campo.valor}".`,
|
|
447
|
+
"erro",
|
|
448
|
+
campo.intervalo,
|
|
449
|
+
"Use um identificador de implementacao como pacote.modulo.funcao ou app.servico.metodo.",
|
|
450
|
+
),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function validarVinculos(bloco: BlocoGenericoAst | undefined, diagnosticos: Diagnostico[], contexto: string): void {
|
|
457
|
+
if (!bloco) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
for (const campo of bloco.campos) {
|
|
462
|
+
if (!CAMPOS_VINCULO_SUPORTADOS.has(campo.nome)) {
|
|
463
|
+
diagnosticos.push(
|
|
464
|
+
criarDiagnostico(
|
|
465
|
+
"SEM064",
|
|
466
|
+
`Campo de vinculo "${campo.nome}" nao e suportado em ${contexto}.`,
|
|
467
|
+
"erro",
|
|
468
|
+
campo.intervalo,
|
|
469
|
+
"Use arquivo, simbolo, recurso, superficie, rota, teste, tabela, fila, job, policy, artefato, evento, cache, storage, worker, cron ou webhook.",
|
|
470
|
+
),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function extrairPerfilCompatibilidade(
|
|
477
|
+
bloco: BlocoGenericoAst | undefined,
|
|
478
|
+
padrao: PerfilCompatibilidadeSemantica = "interno",
|
|
479
|
+
): PerfilCompatibilidadeSemantica {
|
|
480
|
+
const perfil = bloco
|
|
481
|
+
? valorCampoCompleto(localizarCampo(bloco, "perfil", "compatibilidade"))?.toLowerCase()
|
|
482
|
+
: undefined;
|
|
483
|
+
if (
|
|
484
|
+
perfil === "publico"
|
|
485
|
+
|| perfil === "interno"
|
|
486
|
+
|| perfil === "experimental"
|
|
487
|
+
|| perfil === "legado"
|
|
488
|
+
|| perfil === "deprecado"
|
|
489
|
+
) {
|
|
490
|
+
return perfil;
|
|
491
|
+
}
|
|
492
|
+
return padrao;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function validarCamposSuportadosPersistencia(
|
|
496
|
+
bloco: BlocoGenericoAst,
|
|
497
|
+
camposSuportados: Set<string>,
|
|
498
|
+
diagnosticos: Diagnostico[],
|
|
499
|
+
contexto: string,
|
|
500
|
+
codigo: string,
|
|
501
|
+
dica: string,
|
|
502
|
+
): void {
|
|
503
|
+
for (const campo of bloco.campos) {
|
|
504
|
+
if (camposSuportados.has(campo.nome)) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
diagnosticos.push(
|
|
508
|
+
criarDiagnostico(
|
|
509
|
+
codigo,
|
|
510
|
+
`Campo de persistencia "${campo.nome}" nao e suportado em ${contexto}.`,
|
|
511
|
+
"erro",
|
|
512
|
+
campo.intervalo,
|
|
513
|
+
dica,
|
|
514
|
+
),
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function validarBooleanoPersistencia(
|
|
520
|
+
valor: string | undefined,
|
|
521
|
+
intervalo: CampoAst["intervalo"],
|
|
522
|
+
diagnosticos: Diagnostico[],
|
|
523
|
+
codigo: string,
|
|
524
|
+
mensagem: string,
|
|
525
|
+
): void {
|
|
526
|
+
if (!valor) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (parsearBooleanoPersistencia(valor) !== undefined) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
diagnosticos.push(
|
|
533
|
+
criarDiagnostico(
|
|
534
|
+
codigo,
|
|
535
|
+
mensagem,
|
|
536
|
+
"erro",
|
|
537
|
+
intervalo,
|
|
538
|
+
"Use verdadeiro/falso ou true/false.",
|
|
539
|
+
),
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function validarRecursoPersistencia(
|
|
544
|
+
database: BlocoGenericoAst,
|
|
545
|
+
recurso: BlocoGenericoAst,
|
|
546
|
+
diagnosticos: Diagnostico[],
|
|
547
|
+
): void {
|
|
548
|
+
const engine = normalizarEngineBanco(valorCampoCompleto(localizarCampo(database, "engine")));
|
|
549
|
+
if (!engine) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
validarCamposSuportadosPersistencia(
|
|
554
|
+
recurso,
|
|
555
|
+
CAMPOS_RECURSO_PERSISTENCIA_SUPORTADOS,
|
|
556
|
+
diagnosticos,
|
|
557
|
+
`resource "${recurso.nome ?? recurso.palavraChave}" do database "${database.nome ?? "database"}"`,
|
|
558
|
+
"SEM106",
|
|
559
|
+
"Use apenas entity, consistency, durability, transaction_model, query_model, portavel, mode, isolation, strategy, ttl, retention, path, from, to, surface, adapter, resource_kind, collection, table ou compatibilidade.",
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
const tipoRecurso = nomeTipoRecursoPersistencia(recurso);
|
|
563
|
+
if (!tipoRecurso) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const nomeRecurso = recurso.nome ?? tipoRecurso;
|
|
568
|
+
const contexto = `resource "${nomeRecurso}" do database "${database.nome ?? "database"}"`;
|
|
569
|
+
const consistency = valorCampoCompleto(localizarCampo(recurso, "consistency"));
|
|
570
|
+
const durability = valorCampoCompleto(localizarCampo(recurso, "durability"));
|
|
571
|
+
const transactionModel = valorCampoCompleto(localizarCampo(recurso, "transaction_model"));
|
|
572
|
+
const queryModel = valorCampoCompleto(localizarCampo(recurso, "query_model"));
|
|
573
|
+
const mode = valorCampoCompleto(localizarCampo(recurso, "mode"));
|
|
574
|
+
const portavel = valorCampoCompleto(localizarCampo(recurso, "portavel"));
|
|
575
|
+
const isolation = valorCampoCompleto(localizarCampo(recurso, "isolation"));
|
|
576
|
+
|
|
577
|
+
if (consistency && !normalizarConsistenciaPersistencia(consistency)) {
|
|
578
|
+
diagnosticos.push(
|
|
579
|
+
criarDiagnostico(
|
|
580
|
+
"SEM107",
|
|
581
|
+
`${contexto} declarou consistency invalida: "${consistency}".`,
|
|
582
|
+
"erro",
|
|
583
|
+
localizarCampo(recurso, "consistency")?.intervalo,
|
|
584
|
+
"Use eventual, forte, serializable, snapshot ou causal.",
|
|
585
|
+
),
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (durability && !normalizarDurabilidadePersistencia(durability)) {
|
|
590
|
+
diagnosticos.push(
|
|
591
|
+
criarDiagnostico(
|
|
592
|
+
"SEM108",
|
|
593
|
+
`${contexto} declarou durability invalida: "${durability}".`,
|
|
594
|
+
"erro",
|
|
595
|
+
localizarCampo(recurso, "durability")?.intervalo,
|
|
596
|
+
"Use baixa, media ou alta.",
|
|
597
|
+
),
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (transactionModel && !normalizarModeloTransacaoPersistencia(transactionModel)) {
|
|
602
|
+
diagnosticos.push(
|
|
603
|
+
criarDiagnostico(
|
|
604
|
+
"SEM109",
|
|
605
|
+
`${contexto} declarou transaction_model invalido: "${transactionModel}".`,
|
|
606
|
+
"erro",
|
|
607
|
+
localizarCampo(recurso, "transaction_model")?.intervalo,
|
|
608
|
+
"Use mvcc, bloqueio, documento, otimista ou single_thread.",
|
|
609
|
+
),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (queryModel && !normalizarModeloConsultaPersistencia(queryModel)) {
|
|
614
|
+
diagnosticos.push(
|
|
615
|
+
criarDiagnostico(
|
|
616
|
+
"SEM110",
|
|
617
|
+
`${contexto} declarou query_model invalido: "${queryModel}".`,
|
|
618
|
+
"erro",
|
|
619
|
+
localizarCampo(recurso, "query_model")?.intervalo,
|
|
620
|
+
"Use sql, documento, chave_valor, pipeline ou stream.",
|
|
621
|
+
),
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (mode && !normalizarModeloConsultaPersistencia(mode)) {
|
|
626
|
+
diagnosticos.push(
|
|
627
|
+
criarDiagnostico(
|
|
628
|
+
"SEM111",
|
|
629
|
+
`${contexto} declarou mode invalido: "${mode}".`,
|
|
630
|
+
"erro",
|
|
631
|
+
localizarCampo(recurso, "mode")?.intervalo,
|
|
632
|
+
"Use sql, documento, chave_valor, pipeline ou stream.",
|
|
633
|
+
),
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const compatibilidade = classificarCompatibilidadePersistencia(tipoRecurso, engine, { mode, isolation });
|
|
638
|
+
if (compatibilidade.status === "invalido") {
|
|
639
|
+
diagnosticos.push(
|
|
640
|
+
criarDiagnostico(
|
|
641
|
+
"SEM112",
|
|
642
|
+
`${contexto} nao e compativel com o engine ${engine}.`,
|
|
643
|
+
"erro",
|
|
644
|
+
recurso.intervalo,
|
|
645
|
+
compatibilidade.motivo,
|
|
646
|
+
),
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
validarBooleanoPersistencia(
|
|
651
|
+
portavel,
|
|
652
|
+
localizarCampo(recurso, "portavel")?.intervalo ?? recurso.intervalo,
|
|
653
|
+
diagnosticos,
|
|
654
|
+
"SEM113",
|
|
655
|
+
`${contexto} declarou portavel com valor invalido: "${portavel}".`,
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
if (parsearBooleanoPersistencia(portavel) && !recursoPersistenciaPodeSerPortavel(tipoRecurso, { mode, isolation })) {
|
|
659
|
+
diagnosticos.push(
|
|
660
|
+
criarDiagnostico(
|
|
661
|
+
"SEM114",
|
|
662
|
+
`${contexto} foi marcado como portavel, mas a compatibilidade entre os cinco bancos nao fecha sem perdas reais.`,
|
|
663
|
+
"aviso",
|
|
664
|
+
localizarCampo(recurso, "portavel")?.intervalo ?? recurso.intervalo,
|
|
665
|
+
"Remova portavel ou reduza o recurso para um baseline comum entre postgres, mysql, sqlite, mongodb e redis.",
|
|
666
|
+
),
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function validarDatabase(database: BlocoGenericoAst, diagnosticos: Diagnostico[]): void {
|
|
672
|
+
validarCamposSuportadosPersistencia(
|
|
673
|
+
database,
|
|
674
|
+
CAMPOS_DATABASE_SUPORTADOS,
|
|
675
|
+
diagnosticos,
|
|
676
|
+
`database "${database.nome ?? "database"}"`,
|
|
677
|
+
"SEM100",
|
|
678
|
+
"Use apenas engine, schema, database, consistency, durability, transaction_model, query_model, portavel, adapter, perfil ou compatibilidade.",
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
const campoEngine = localizarCampo(database, "engine");
|
|
682
|
+
const engineBruto = valorCampoCompleto(campoEngine);
|
|
683
|
+
if (!engineBruto) {
|
|
684
|
+
diagnosticos.push(
|
|
685
|
+
criarDiagnostico(
|
|
686
|
+
"SEM101",
|
|
687
|
+
`Database "${database.nome ?? "database"}" precisa declarar engine.`,
|
|
688
|
+
"erro",
|
|
689
|
+
database.intervalo,
|
|
690
|
+
"Use postgres, mysql, sqlite, mongodb ou redis.",
|
|
691
|
+
),
|
|
692
|
+
);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const engine = normalizarEngineBanco(engineBruto);
|
|
697
|
+
if (!engine) {
|
|
698
|
+
diagnosticos.push(
|
|
699
|
+
criarDiagnostico(
|
|
700
|
+
"SEM102",
|
|
701
|
+
`Database "${database.nome ?? "database"}" declarou engine invalido: "${engineBruto}".`,
|
|
702
|
+
"erro",
|
|
703
|
+
campoEngine?.intervalo ?? database.intervalo,
|
|
704
|
+
"Use postgres, mysql, sqlite, mongodb ou redis.",
|
|
705
|
+
),
|
|
706
|
+
);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const consistency = valorCampoCompleto(localizarCampo(database, "consistency"));
|
|
711
|
+
const durability = valorCampoCompleto(localizarCampo(database, "durability"));
|
|
712
|
+
const transactionModel = valorCampoCompleto(localizarCampo(database, "transaction_model"));
|
|
713
|
+
const queryModel = valorCampoCompleto(localizarCampo(database, "query_model"));
|
|
714
|
+
const portavel = valorCampoCompleto(localizarCampo(database, "portavel"));
|
|
715
|
+
|
|
716
|
+
if (consistency && !normalizarConsistenciaPersistencia(consistency)) {
|
|
717
|
+
diagnosticos.push(
|
|
718
|
+
criarDiagnostico(
|
|
719
|
+
"SEM103",
|
|
720
|
+
`Database "${database.nome ?? "database"}" declarou consistency invalida: "${consistency}".`,
|
|
721
|
+
"erro",
|
|
722
|
+
localizarCampo(database, "consistency")?.intervalo,
|
|
723
|
+
"Use eventual, forte, serializable, snapshot ou causal.",
|
|
724
|
+
),
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (durability && !normalizarDurabilidadePersistencia(durability)) {
|
|
729
|
+
diagnosticos.push(
|
|
730
|
+
criarDiagnostico(
|
|
731
|
+
"SEM104",
|
|
732
|
+
`Database "${database.nome ?? "database"}" declarou durability invalida: "${durability}".`,
|
|
733
|
+
"erro",
|
|
734
|
+
localizarCampo(database, "durability")?.intervalo,
|
|
735
|
+
"Use baixa, media ou alta.",
|
|
736
|
+
),
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (transactionModel && !normalizarModeloTransacaoPersistencia(transactionModel)) {
|
|
741
|
+
diagnosticos.push(
|
|
742
|
+
criarDiagnostico(
|
|
743
|
+
"SEM105",
|
|
744
|
+
`Database "${database.nome ?? "database"}" declarou transaction_model invalido: "${transactionModel}".`,
|
|
745
|
+
"erro",
|
|
746
|
+
localizarCampo(database, "transaction_model")?.intervalo,
|
|
747
|
+
"Use mvcc, bloqueio, documento, otimista ou single_thread.",
|
|
748
|
+
),
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (queryModel && !normalizarModeloConsultaPersistencia(queryModel)) {
|
|
753
|
+
diagnosticos.push(
|
|
754
|
+
criarDiagnostico(
|
|
755
|
+
"SEM115",
|
|
756
|
+
`Database "${database.nome ?? "database"}" declarou query_model invalido: "${queryModel}".`,
|
|
757
|
+
"erro",
|
|
758
|
+
localizarCampo(database, "query_model")?.intervalo,
|
|
759
|
+
"Use sql, documento, chave_valor, pipeline ou stream.",
|
|
760
|
+
),
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
validarBooleanoPersistencia(
|
|
765
|
+
portavel,
|
|
766
|
+
localizarCampo(database, "portavel")?.intervalo ?? database.intervalo,
|
|
767
|
+
diagnosticos,
|
|
768
|
+
"SEM116",
|
|
769
|
+
`Database "${database.nome ?? "database"}" declarou portavel com valor invalido: "${portavel}".`,
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
for (const recurso of database.blocos) {
|
|
773
|
+
if (recurso.tipo !== "bloco_generico") {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
validarRecursoPersistencia(database, recurso, diagnosticos);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function coletarSuperficiesModulo(modulo: ModuloAst): Array<{ tipo: SimboloSemantico["categoria"]; superficie: BlocoGenericoAst }> {
|
|
781
|
+
return [
|
|
782
|
+
...modulo.workers.map((superficie) => ({ tipo: "worker" as const, superficie })),
|
|
783
|
+
...modulo.eventos.map((superficie) => ({ tipo: "evento" as const, superficie })),
|
|
784
|
+
...modulo.filas.map((superficie) => ({ tipo: "fila" as const, superficie })),
|
|
785
|
+
...modulo.crons.map((superficie) => ({ tipo: "cron" as const, superficie })),
|
|
786
|
+
...modulo.webhooks.map((superficie) => ({ tipo: "webhook" as const, superficie })),
|
|
787
|
+
...modulo.caches.map((superficie) => ({ tipo: "cache" as const, superficie })),
|
|
788
|
+
...modulo.storages.map((superficie) => ({ tipo: "storage" as const, superficie })),
|
|
789
|
+
...modulo.policies.map((superficie) => ({ tipo: "policy" as const, superficie })),
|
|
790
|
+
];
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function superficieEhPublica(
|
|
794
|
+
superficie: BlocoGenericoAst,
|
|
795
|
+
tipoSuperficie: SimboloSemantico["categoria"],
|
|
796
|
+
): boolean {
|
|
797
|
+
return extrairPerfilCompatibilidade(superficie, tipoSuperficie === "webhook" ? "publico" : "interno") === "publico";
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function taskEhSensivel(task: TaskAst): boolean {
|
|
801
|
+
const criticidadeOperacional = task.execucao
|
|
802
|
+
? valorCampoCompleto(localizarCampo(task.execucao, "criticidade_operacional"))
|
|
803
|
+
: undefined;
|
|
804
|
+
if (criticidadeOperacional === "alta" || criticidadeOperacional === "critica") {
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return (task.effects?.linhas ?? []).some((linha) => {
|
|
809
|
+
const efeito = parsearEfeitoSemantico(linha.conteudo);
|
|
810
|
+
if (!efeito) {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
return efeito.categoria === "persistencia" || efeito.criticidade === "alta" || efeito.criticidade === "critica" || efeitoEhPrivilegiado(efeito);
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function taskTemRastreabilidade(task: TaskAst): boolean {
|
|
818
|
+
return Boolean(task.impl || task.vinculos);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function routeEhMutante(route: RouteAst): boolean {
|
|
822
|
+
const metodo = (localizarCampo(route.corpo, "metodo")?.valor ?? "").toUpperCase();
|
|
823
|
+
return ["POST", "PUT", "PATCH", "DELETE"].includes(metodo);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function validarExecucaoBloco(
|
|
827
|
+
execucao: BlocoGenericoAst | undefined,
|
|
828
|
+
diagnosticos: Diagnostico[],
|
|
829
|
+
contexto: string,
|
|
830
|
+
): void {
|
|
831
|
+
if (!execucao) {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
for (const campo of execucao.campos) {
|
|
836
|
+
if (!CAMPOS_EXECUCAO_SUPORTADOS.has(campo.nome)) {
|
|
837
|
+
diagnosticos.push(
|
|
838
|
+
criarDiagnostico(
|
|
839
|
+
"SEM065",
|
|
840
|
+
`Campo de execucao "${campo.nome}" nao e suportado em ${contexto}.`,
|
|
841
|
+
"erro",
|
|
842
|
+
campo.intervalo,
|
|
843
|
+
"Use apenas idempotencia, timeout, retry, compensacao ou criticidade_operacional.",
|
|
844
|
+
),
|
|
845
|
+
);
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (campo.nome === "criticidade_operacional") {
|
|
850
|
+
const criticidade = valorCampoCompleto(campo);
|
|
851
|
+
if (criticidade && !CRITICIDADES_OPERACIONAIS.has(criticidade)) {
|
|
852
|
+
diagnosticos.push(
|
|
853
|
+
criarDiagnostico(
|
|
854
|
+
"SEM066",
|
|
855
|
+
`Execucao de ${contexto} declarou criticidade_operacional invalida: "${criticidade}".`,
|
|
856
|
+
"erro",
|
|
857
|
+
campo.intervalo,
|
|
858
|
+
"Use apenas baixa, media, alta ou critica em execucao.",
|
|
859
|
+
),
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const criticidade = valorCampoCompleto(localizarCampo(execucao, "criticidade_operacional"));
|
|
866
|
+
const idempotencia = localizarCampo(execucao, "idempotencia");
|
|
867
|
+
if ((criticidade === "alta" || criticidade === "critica") && !idempotencia) {
|
|
868
|
+
diagnosticos.push(
|
|
869
|
+
criarDiagnostico(
|
|
870
|
+
"SEM101",
|
|
871
|
+
`Execucao critica em ${contexto} deveria declarar idempotencia explicita.`,
|
|
872
|
+
"aviso",
|
|
873
|
+
execucao.intervalo,
|
|
874
|
+
"Declare idempotencia: verdadeiro ou idempotencia: falso para a IA nao assumir retry seguro no escuro.",
|
|
875
|
+
),
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function validarExecucao(task: TaskAst, diagnosticos: Diagnostico[]): void {
|
|
881
|
+
validarExecucaoBloco(task.execucao, diagnosticos, `task "${task.nome}"`);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
interface PerfilSegurancaDeclarado {
|
|
885
|
+
auth: ReturnType<typeof extrairContratoAuth>;
|
|
886
|
+
authz: ReturnType<typeof extrairContratoAuthz>;
|
|
887
|
+
dados: ReturnType<typeof extrairContratoDados>;
|
|
888
|
+
audit: ReturnType<typeof extrairContratoAudit>;
|
|
889
|
+
segredos: ReturnType<typeof extrairContratoSegredos>;
|
|
890
|
+
forbidden: ReturnType<typeof extrairContratoForbidden>;
|
|
891
|
+
efeitoPrivilegiado: boolean;
|
|
892
|
+
dadosSensiveis: boolean;
|
|
893
|
+
exigeSegredos: boolean;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function validarAuthBloco(bloco: BlocoGenericoAst | undefined, diagnosticos: Diagnostico[], contexto: string): void {
|
|
897
|
+
if (!bloco) {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
for (const campo of bloco.campos) {
|
|
902
|
+
if (!CAMPOS_AUTH_SUPORTADOS.has(campo.nome)) {
|
|
903
|
+
diagnosticos.push(
|
|
904
|
+
criarDiagnostico(
|
|
905
|
+
"SEM074",
|
|
906
|
+
`Campo de auth "${campo.nome}" nao e suportado em ${contexto}.`,
|
|
907
|
+
"erro",
|
|
908
|
+
campo.intervalo,
|
|
909
|
+
"Use apenas modo, estrategia, principal ou origem em auth.",
|
|
910
|
+
),
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const auth = extrairContratoAuth(bloco);
|
|
916
|
+
if (auth.modo && !MODOS_AUTH_SUPORTADOS.has(auth.modo as (typeof MODOS_AUTH_SUPORTADOS extends Set<infer T> ? T : never))) {
|
|
917
|
+
diagnosticos.push(
|
|
918
|
+
criarDiagnostico(
|
|
919
|
+
"SEM075",
|
|
920
|
+
`Auth em ${contexto} declarou modo invalido: "${auth.modo}".`,
|
|
921
|
+
"erro",
|
|
922
|
+
bloco.intervalo,
|
|
923
|
+
"Use obrigatorio, opcional, anonimo, interno ou m2m.",
|
|
924
|
+
),
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
if (auth.principal && !PRINCIPAIS_AUTH_SUPORTADOS.has(auth.principal as (typeof PRINCIPAIS_AUTH_SUPORTADOS extends Set<infer T> ? T : never))) {
|
|
928
|
+
diagnosticos.push(
|
|
929
|
+
criarDiagnostico(
|
|
930
|
+
"SEM076",
|
|
931
|
+
`Auth em ${contexto} declarou principal invalido: "${auth.principal}".`,
|
|
932
|
+
"erro",
|
|
933
|
+
bloco.intervalo,
|
|
934
|
+
"Use usuario, servico, sistema ou anonimo.",
|
|
935
|
+
),
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
if (auth.origem && !ORIGENS_AUTH_SUPORTADAS.has(auth.origem as (typeof ORIGENS_AUTH_SUPORTADAS extends Set<infer T> ? T : never))) {
|
|
939
|
+
diagnosticos.push(
|
|
940
|
+
criarDiagnostico(
|
|
941
|
+
"SEM077",
|
|
942
|
+
`Auth em ${contexto} declarou origem invalida: "${auth.origem}".`,
|
|
943
|
+
"erro",
|
|
944
|
+
bloco.intervalo,
|
|
945
|
+
"Use publica, interna, worker, webhook, fila ou cron.",
|
|
946
|
+
),
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function validarAuthzBloco(bloco: BlocoGenericoAst | undefined, diagnosticos: Diagnostico[], contexto: string): void {
|
|
952
|
+
if (!bloco) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
for (const campo of bloco.campos) {
|
|
957
|
+
if (!CAMPOS_AUTHZ_SUPORTADOS.has(campo.nome)) {
|
|
958
|
+
diagnosticos.push(
|
|
959
|
+
criarDiagnostico(
|
|
960
|
+
"SEM078",
|
|
961
|
+
`Campo de authz "${campo.nome}" nao e suportado em ${contexto}.`,
|
|
962
|
+
"erro",
|
|
963
|
+
campo.intervalo,
|
|
964
|
+
"Use papel, papeis, escopo, escopos, politica ou tenant em authz.",
|
|
965
|
+
),
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const authz = extrairContratoAuthz(bloco);
|
|
971
|
+
if (authz.tenant && !TENANTS_AUTHZ_SUPORTADOS.has(authz.tenant as (typeof TENANTS_AUTHZ_SUPORTADOS extends Set<infer T> ? T : never))) {
|
|
972
|
+
diagnosticos.push(
|
|
973
|
+
criarDiagnostico(
|
|
974
|
+
"SEM079",
|
|
975
|
+
`Authz em ${contexto} declarou tenant invalido: "${authz.tenant}".`,
|
|
976
|
+
"erro",
|
|
977
|
+
bloco.intervalo,
|
|
978
|
+
"Use obrigatorio, opcional ou isolado.",
|
|
979
|
+
),
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
if (authz.papeis.length === 0 && authz.escopos.length === 0 && !authz.politica) {
|
|
983
|
+
diagnosticos.push(
|
|
984
|
+
criarDiagnostico(
|
|
985
|
+
"SEM080",
|
|
986
|
+
`Authz em ${contexto} precisa declarar papeis, escopos ou politica.`,
|
|
987
|
+
"erro",
|
|
988
|
+
bloco.intervalo,
|
|
989
|
+
"Explicite ao menos um papel, escopo ou politica para a autorizacao nao virar enfeite.",
|
|
990
|
+
),
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function validarDadosBloco(bloco: BlocoGenericoAst | undefined, diagnosticos: Diagnostico[], contexto: string): void {
|
|
996
|
+
if (!bloco) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
for (const campo of bloco.campos) {
|
|
1001
|
+
const valor = valorCampoCompleto(campo);
|
|
1002
|
+
if (CAMPOS_DADOS_SUPORTADOS.has(campo.nome)) {
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
if (valor && !CLASSIFICACOES_DADO_SUPORTADAS.has(valor as (typeof CLASSIFICACOES_DADO_SUPORTADAS extends Set<infer T> ? T : never))) {
|
|
1006
|
+
diagnosticos.push(
|
|
1007
|
+
criarDiagnostico(
|
|
1008
|
+
"SEM081",
|
|
1009
|
+
`Dados em ${contexto} declarou classificacao invalida para "${campo.nome}": "${valor}".`,
|
|
1010
|
+
"erro",
|
|
1011
|
+
campo.intervalo,
|
|
1012
|
+
"Use publico, interno, pii, financeiro, credencial ou segredo.",
|
|
1013
|
+
),
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const dados = extrairContratoDados(bloco);
|
|
1019
|
+
if (dados.classificacaoPadrao && !CLASSIFICACOES_DADO_SUPORTADAS.has(dados.classificacaoPadrao as (typeof CLASSIFICACOES_DADO_SUPORTADAS extends Set<infer T> ? T : never))) {
|
|
1020
|
+
diagnosticos.push(
|
|
1021
|
+
criarDiagnostico(
|
|
1022
|
+
"SEM081",
|
|
1023
|
+
`Dados em ${contexto} declarou classificacao_padrao invalida: "${dados.classificacaoPadrao}".`,
|
|
1024
|
+
"erro",
|
|
1025
|
+
bloco.intervalo,
|
|
1026
|
+
"Use publico, interno, pii, financeiro, credencial ou segredo.",
|
|
1027
|
+
),
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
if (dados.redacaoLog && !REDACOES_LOG_SUPORTADAS.has(dados.redacaoLog as (typeof REDACOES_LOG_SUPORTADAS extends Set<infer T> ? T : never))) {
|
|
1031
|
+
diagnosticos.push(
|
|
1032
|
+
criarDiagnostico(
|
|
1033
|
+
"SEM082",
|
|
1034
|
+
`Dados em ${contexto} declarou redacao_log invalida: "${dados.redacaoLog}".`,
|
|
1035
|
+
"erro",
|
|
1036
|
+
bloco.intervalo,
|
|
1037
|
+
"Use livre, parcial, obrigatoria ou proibida.",
|
|
1038
|
+
),
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
for (const subbloco of bloco.blocos) {
|
|
1043
|
+
if (subbloco.tipo !== "bloco_generico") {
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
const nomeSubbloco = subbloco.nome ?? subbloco.palavraChave;
|
|
1047
|
+
if (nomeSubbloco !== "input" && nomeSubbloco !== "output") {
|
|
1048
|
+
diagnosticos.push(
|
|
1049
|
+
criarDiagnostico(
|
|
1050
|
+
"SEM083",
|
|
1051
|
+
`Dados em ${contexto} nao suporta o subbloco "${nomeSubbloco}".`,
|
|
1052
|
+
"erro",
|
|
1053
|
+
subbloco.intervalo,
|
|
1054
|
+
"Use apenas campos diretos ou subblocos input/output para classificar dados.",
|
|
1055
|
+
),
|
|
1056
|
+
);
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
for (const campo of subbloco.campos) {
|
|
1061
|
+
const classificacao = valorCampoCompleto(campo);
|
|
1062
|
+
if (classificacao && !CLASSIFICACOES_DADO_SUPORTADAS.has(classificacao as (typeof CLASSIFICACOES_DADO_SUPORTADAS extends Set<infer T> ? T : never))) {
|
|
1063
|
+
diagnosticos.push(
|
|
1064
|
+
criarDiagnostico(
|
|
1065
|
+
"SEM081",
|
|
1066
|
+
`Dados em ${contexto} declarou classificacao invalida para "${nomeSubbloco}.${campo.nome}": "${classificacao}".`,
|
|
1067
|
+
"erro",
|
|
1068
|
+
campo.intervalo,
|
|
1069
|
+
"Use publico, interno, pii, financeiro, credencial ou segredo.",
|
|
1070
|
+
),
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function validarAuditBloco(bloco: BlocoGenericoAst | undefined, diagnosticos: Diagnostico[], contexto: string): void {
|
|
1078
|
+
if (!bloco) {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
for (const campo of bloco.campos) {
|
|
1083
|
+
if (!CAMPOS_AUDIT_SUPORTADOS.has(campo.nome)) {
|
|
1084
|
+
diagnosticos.push(
|
|
1085
|
+
criarDiagnostico(
|
|
1086
|
+
"SEM084",
|
|
1087
|
+
`Campo de audit "${campo.nome}" nao e suportado em ${contexto}.`,
|
|
1088
|
+
"erro",
|
|
1089
|
+
campo.intervalo,
|
|
1090
|
+
"Use evento, ator, correlacao, retencao ou motivo em audit.",
|
|
1091
|
+
),
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const audit = extrairContratoAudit(bloco);
|
|
1097
|
+
if (!audit.evento) {
|
|
1098
|
+
diagnosticos.push(
|
|
1099
|
+
criarDiagnostico(
|
|
1100
|
+
"SEM085",
|
|
1101
|
+
`Audit em ${contexto} precisa declarar evento.`,
|
|
1102
|
+
"erro",
|
|
1103
|
+
bloco.intervalo,
|
|
1104
|
+
"Explique qual evento auditavel sera registrado para a operacao.",
|
|
1105
|
+
),
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
if (audit.motivo && !MOTIVOS_AUDIT_SUPORTADOS.has(audit.motivo as (typeof MOTIVOS_AUDIT_SUPORTADOS extends Set<infer T> ? T : never))) {
|
|
1109
|
+
diagnosticos.push(
|
|
1110
|
+
criarDiagnostico(
|
|
1111
|
+
"SEM086",
|
|
1112
|
+
`Audit em ${contexto} declarou motivo invalido: "${audit.motivo}".`,
|
|
1113
|
+
"erro",
|
|
1114
|
+
bloco.intervalo,
|
|
1115
|
+
"Use obrigatorio, opcional ou dispensado.",
|
|
1116
|
+
),
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function validarSegredosBloco(bloco: BlocoGenericoAst | undefined, diagnosticos: Diagnostico[], contexto: string): void {
|
|
1122
|
+
if (!bloco) {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const segredos = extrairContratoSegredos(bloco);
|
|
1127
|
+
if (segredos.itens.length === 0) {
|
|
1128
|
+
diagnosticos.push(
|
|
1129
|
+
criarDiagnostico(
|
|
1130
|
+
"SEM087",
|
|
1131
|
+
`Segredos em ${contexto} precisa declarar ao menos um segredo nomeado.`,
|
|
1132
|
+
"erro",
|
|
1133
|
+
bloco.intervalo,
|
|
1134
|
+
"Use segredos { nome_do_segredo { origem: vault escopo: runtime ... } }.",
|
|
1135
|
+
),
|
|
1136
|
+
);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
for (const item of bloco.blocos) {
|
|
1141
|
+
if (item.tipo !== "bloco_generico") {
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
for (const campo of item.campos) {
|
|
1146
|
+
if (!CAMPOS_SEGREDO_SUPORTADOS.has(campo.nome)) {
|
|
1147
|
+
diagnosticos.push(
|
|
1148
|
+
criarDiagnostico(
|
|
1149
|
+
"SEM087",
|
|
1150
|
+
`Segredo "${item.nome ?? item.palavraChave}" em ${contexto} usa o campo "${campo.nome}", que nao e suportado.`,
|
|
1151
|
+
"erro",
|
|
1152
|
+
campo.intervalo,
|
|
1153
|
+
"Use origem, escopo, acesso, rotacao, nao_logar, nao_retornar ou mascarar.",
|
|
1154
|
+
),
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const nomeSegredo = item.nome ?? item.palavraChave;
|
|
1160
|
+
const origem = valorCampoCompleto(localizarCampo(item, "origem"));
|
|
1161
|
+
if (!origem) {
|
|
1162
|
+
diagnosticos.push(
|
|
1163
|
+
criarDiagnostico(
|
|
1164
|
+
"SEM088",
|
|
1165
|
+
`Segredo "${nomeSegredo}" em ${contexto} precisa declarar origem.`,
|
|
1166
|
+
"erro",
|
|
1167
|
+
item.intervalo,
|
|
1168
|
+
"Explicite a origem do segredo, como vault, env, secret_manager ou runtime.",
|
|
1169
|
+
),
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
for (const nomeBooleano of ["nao_logar", "nao_retornar", "mascarar"]) {
|
|
1174
|
+
const campo = localizarCampo(item, nomeBooleano);
|
|
1175
|
+
const valor = valorCampoCompleto(campo);
|
|
1176
|
+
if (campo && valor !== "verdadeiro" && valor !== "true" && valor !== "falso" && valor !== "false") {
|
|
1177
|
+
diagnosticos.push(
|
|
1178
|
+
criarDiagnostico(
|
|
1179
|
+
"SEM089",
|
|
1180
|
+
`Segredo "${nomeSegredo}" em ${contexto} declarou "${nomeBooleano}" com valor invalido: "${valor}".`,
|
|
1181
|
+
"erro",
|
|
1182
|
+
campo.intervalo,
|
|
1183
|
+
"Use verdadeiro/falso para campos booleanos de segredos.",
|
|
1184
|
+
),
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function validarForbiddenBloco(
|
|
1192
|
+
bloco: BlocoGenericoAst | undefined,
|
|
1193
|
+
efeitos: BlocoGenericoAst["linhas"],
|
|
1194
|
+
diagnosticos: Diagnostico[],
|
|
1195
|
+
contexto: string,
|
|
1196
|
+
): ReturnType<typeof extrairContratoForbidden> {
|
|
1197
|
+
const forbidden = extrairContratoForbidden(bloco);
|
|
1198
|
+
if (!bloco) {
|
|
1199
|
+
return forbidden;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
for (const regra of forbidden.regras) {
|
|
1203
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/u.test(regra)) {
|
|
1204
|
+
diagnosticos.push(
|
|
1205
|
+
criarDiagnostico(
|
|
1206
|
+
"SEM090",
|
|
1207
|
+
`Forbidden em ${contexto} declarou regra invalida: "${regra}".`,
|
|
1208
|
+
"erro",
|
|
1209
|
+
bloco.intervalo,
|
|
1210
|
+
"Use regras simples como network.egress, shell.exec, retorno.credencial ou log.segredo.",
|
|
1211
|
+
),
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
for (const linha of efeitos) {
|
|
1217
|
+
const efeito = parsearEfeitoSemantico(linha.conteudo);
|
|
1218
|
+
if (efeito && forbiddenContemRegra(forbidden, efeito.categoria)) {
|
|
1219
|
+
diagnosticos.push(
|
|
1220
|
+
criarDiagnostico(
|
|
1221
|
+
"SEM091",
|
|
1222
|
+
`Forbidden em ${contexto} proibe "${efeito.categoria}", mas effects ainda declara esse efeito.`,
|
|
1223
|
+
"erro",
|
|
1224
|
+
linha.intervalo,
|
|
1225
|
+
"Remova o efeito proibido ou ajuste o bloco forbidden para refletir a operacao permitida de verdade.",
|
|
1226
|
+
),
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return forbidden;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function coletarPerfilSegurancaDeclarado(
|
|
1235
|
+
corpo: BlocoGenericoAst,
|
|
1236
|
+
effects: BlocoGenericoAst | undefined,
|
|
1237
|
+
diagnosticos: Diagnostico[] | undefined,
|
|
1238
|
+
contexto: string,
|
|
1239
|
+
): PerfilSegurancaDeclarado {
|
|
1240
|
+
const authBloco = localizarBloco(corpo, "auth");
|
|
1241
|
+
const authzBloco = localizarBloco(corpo, "authz");
|
|
1242
|
+
const dadosBloco = localizarBloco(corpo, "dados");
|
|
1243
|
+
const auditBloco = localizarBloco(corpo, "audit");
|
|
1244
|
+
const segredosBloco = localizarBloco(corpo, "segredos");
|
|
1245
|
+
const forbiddenBloco = localizarBloco(corpo, "forbidden");
|
|
1246
|
+
|
|
1247
|
+
if (diagnosticos) {
|
|
1248
|
+
validarAuthBloco(authBloco, diagnosticos, contexto);
|
|
1249
|
+
validarAuthzBloco(authzBloco, diagnosticos, contexto);
|
|
1250
|
+
validarDadosBloco(dadosBloco, diagnosticos, contexto);
|
|
1251
|
+
validarAuditBloco(auditBloco, diagnosticos, contexto);
|
|
1252
|
+
validarSegredosBloco(segredosBloco, diagnosticos, contexto);
|
|
1253
|
+
}
|
|
1254
|
+
const forbidden = diagnosticos
|
|
1255
|
+
? validarForbiddenBloco(forbiddenBloco, effects?.linhas ?? [], diagnosticos, contexto)
|
|
1256
|
+
: extrairContratoForbidden(forbiddenBloco);
|
|
1257
|
+
|
|
1258
|
+
const auth = extrairContratoAuth(authBloco);
|
|
1259
|
+
const authz = extrairContratoAuthz(authzBloco);
|
|
1260
|
+
const dados = extrairContratoDados(dadosBloco);
|
|
1261
|
+
const audit = extrairContratoAudit(auditBloco);
|
|
1262
|
+
const segredos = extrairContratoSegredos(segredosBloco);
|
|
1263
|
+
const efeitosEstruturados = (effects?.linhas ?? [])
|
|
1264
|
+
.map((linha) => parsearEfeitoSemantico(linha.conteudo))
|
|
1265
|
+
.filter((efeito): efeito is NonNullable<typeof efeito> => Boolean(efeito));
|
|
1266
|
+
|
|
1267
|
+
return {
|
|
1268
|
+
auth,
|
|
1269
|
+
authz,
|
|
1270
|
+
dados,
|
|
1271
|
+
audit,
|
|
1272
|
+
segredos,
|
|
1273
|
+
forbidden,
|
|
1274
|
+
efeitoPrivilegiado: efeitosEstruturados.some((efeito) => efeitoEhPrivilegiado(efeito)),
|
|
1275
|
+
dadosSensiveis: contratoDadosTemSensivel(dados),
|
|
1276
|
+
exigeSegredos: efeitosEstruturados.some((efeito) => efeitoRequerSegredo(efeito)) || contratoDadosTemSegredoOuCredencial(dados),
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function emitirGuardrailsSeguranca(
|
|
1281
|
+
contexto: string,
|
|
1282
|
+
intervalo: CampoAst["intervalo"] | undefined,
|
|
1283
|
+
perfil: PerfilSegurancaDeclarado,
|
|
1284
|
+
diagnosticos: Diagnostico[],
|
|
1285
|
+
opcoes: { publico: boolean; sensivel: boolean },
|
|
1286
|
+
): void {
|
|
1287
|
+
const exigeAuth = opcoes.publico;
|
|
1288
|
+
const exigeAuthz = opcoes.publico || opcoes.sensivel || perfil.efeitoPrivilegiado || perfil.dadosSensiveis;
|
|
1289
|
+
const exigeDados = opcoes.publico || opcoes.sensivel || perfil.efeitoPrivilegiado;
|
|
1290
|
+
const exigeAudit = opcoes.publico || opcoes.sensivel || perfil.efeitoPrivilegiado || perfil.dadosSensiveis;
|
|
1291
|
+
const exigeSegredos = perfil.exigeSegredos;
|
|
1292
|
+
const exigeForbidden = perfil.efeitoPrivilegiado || perfil.dadosSensiveis;
|
|
1293
|
+
|
|
1294
|
+
if (exigeAuth && !perfil.auth.explicita) {
|
|
1295
|
+
diagnosticos.push(
|
|
1296
|
+
criarDiagnostico(
|
|
1297
|
+
"SEM094",
|
|
1298
|
+
`${contexto} deveria declarar auth explicita para reduzir ambiguidade de seguranca na borda publica.`,
|
|
1299
|
+
"aviso",
|
|
1300
|
+
intervalo,
|
|
1301
|
+
"Declare auth { modo: obrigatorio|anonimo ... } para deixar a intencao da exposicao publica cristalina.",
|
|
1302
|
+
),
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
if (exigeAuthz && !perfil.authz.explicita) {
|
|
1306
|
+
diagnosticos.push(
|
|
1307
|
+
criarDiagnostico(
|
|
1308
|
+
"SEM095",
|
|
1309
|
+
`${contexto} deveria declarar authz explicita porque opera com risco, privilegio ou exposicao publica.`,
|
|
1310
|
+
"aviso",
|
|
1311
|
+
intervalo,
|
|
1312
|
+
"Declare papeis, escopos ou politica em authz para nao empurrar autorizacao para o limbo do codigo vivo.",
|
|
1313
|
+
),
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
if (exigeDados && !perfil.dados.explicita) {
|
|
1317
|
+
diagnosticos.push(
|
|
1318
|
+
criarDiagnostico(
|
|
1319
|
+
"SEM096",
|
|
1320
|
+
`${contexto} deveria classificar dados de forma explicita em dados { ... }.`,
|
|
1321
|
+
"aviso",
|
|
1322
|
+
intervalo,
|
|
1323
|
+
"Classifique input/output com publico, interno, pii, financeiro, credencial ou segredo.",
|
|
1324
|
+
),
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
if (exigeAudit && !perfil.audit.explicita) {
|
|
1328
|
+
diagnosticos.push(
|
|
1329
|
+
criarDiagnostico(
|
|
1330
|
+
"SEM097",
|
|
1331
|
+
`${contexto} deveria declarar audit explicita para operar com trilha semantica de seguranca.`,
|
|
1332
|
+
"aviso",
|
|
1333
|
+
intervalo,
|
|
1334
|
+
"Declare audit { evento: ... correlacao: ... motivo: ... } para nao depender de adivinhacao operacional.",
|
|
1335
|
+
),
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
if (exigeSegredos && !perfil.segredos.explicita) {
|
|
1339
|
+
diagnosticos.push(
|
|
1340
|
+
criarDiagnostico(
|
|
1341
|
+
"SEM098",
|
|
1342
|
+
`${contexto} deveria declarar segredos explicitos porque toca credencial, segredo ou secret.read.`,
|
|
1343
|
+
"aviso",
|
|
1344
|
+
intervalo,
|
|
1345
|
+
"Use segredos { nome { origem: vault escopo: runtime ... } } para governar acesso sensivel.",
|
|
1346
|
+
),
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
if (exigeForbidden && !perfil.forbidden.explicita) {
|
|
1350
|
+
diagnosticos.push(
|
|
1351
|
+
criarDiagnostico(
|
|
1352
|
+
"SEM099",
|
|
1353
|
+
`${contexto} deveria declarar forbidden explicito para proibir operacoes perigosas ou vazamento semantico.`,
|
|
1354
|
+
"aviso",
|
|
1355
|
+
intervalo,
|
|
1356
|
+
"Use forbidden { network.egress shell.exec log.segredo retorno.credencial } conforme o risco da operacao.",
|
|
1357
|
+
),
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function validarErroOperacional(task: TaskAst, diagnosticos: Diagnostico[]): void {
|
|
1363
|
+
if (!task.error) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const nomes = new Set<string>();
|
|
1368
|
+
for (const campo of task.error.campos) {
|
|
1369
|
+
if (nomes.has(campo.nome)) {
|
|
1370
|
+
diagnosticos.push(diagnosticoDuplicado(campo.nome, "Erro", campo.intervalo));
|
|
1371
|
+
}
|
|
1372
|
+
nomes.add(campo.nome);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
for (const bloco of task.error.blocos) {
|
|
1376
|
+
if (bloco.tipo !== "bloco_generico") {
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const codigo = bloco.nome ?? bloco.palavraChave;
|
|
1381
|
+
if (!codigo || codigo === "desconhecido") {
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
if (nomes.has(codigo)) {
|
|
1386
|
+
diagnosticos.push(diagnosticoDuplicado(codigo, "Erro", bloco.intervalo));
|
|
1387
|
+
}
|
|
1388
|
+
nomes.add(codigo);
|
|
1389
|
+
|
|
1390
|
+
const mensagem = localizarCampo(bloco, "mensagem");
|
|
1391
|
+
if (!mensagem) {
|
|
1392
|
+
diagnosticos.push(
|
|
1393
|
+
criarDiagnostico(
|
|
1394
|
+
"SEM067",
|
|
1395
|
+
`Erro estruturado "${codigo}" da task "${task.nome}" precisa declarar mensagem.`,
|
|
1396
|
+
"erro",
|
|
1397
|
+
bloco.intervalo,
|
|
1398
|
+
"Use error { codigo { mensagem: \"...\" categoria: dominio ... } }.",
|
|
1399
|
+
),
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
for (const campo of bloco.campos) {
|
|
1404
|
+
if (!CAMPOS_ERRO_OPERACIONAL.has(campo.nome)) {
|
|
1405
|
+
diagnosticos.push(
|
|
1406
|
+
criarDiagnostico(
|
|
1407
|
+
"SEM068",
|
|
1408
|
+
`Erro estruturado "${codigo}" da task "${task.nome}" usa o campo "${campo.nome}", que nao e suportado.`,
|
|
1409
|
+
"erro",
|
|
1410
|
+
campo.intervalo,
|
|
1411
|
+
"Use mensagem, categoria, recuperabilidade, acao_chamador, impacta_estado ou requer_compensacao.",
|
|
1412
|
+
),
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function validarSuperficie(
|
|
1420
|
+
superficie: BlocoGenericoAst,
|
|
1421
|
+
tipoSuperficie: SimboloSemantico["categoria"],
|
|
1422
|
+
tasksConhecidas: Set<string>,
|
|
1423
|
+
tiposConhecidos: Set<string>,
|
|
1424
|
+
diagnosticos: Diagnostico[],
|
|
1425
|
+
): void {
|
|
1426
|
+
const nomeSuperficie = superficie.nome ?? tipoSuperficie;
|
|
1427
|
+
const task = localizarCampo(superficie, "task", "tarefa");
|
|
1428
|
+
const input = localizarBloco(superficie, "input");
|
|
1429
|
+
const output = localizarBloco(superficie, "output");
|
|
1430
|
+
const effects = localizarBloco(superficie, "effects");
|
|
1431
|
+
const impl = localizarBloco(superficie, "impl");
|
|
1432
|
+
const vinculos = localizarBloco(superficie, "vinculos");
|
|
1433
|
+
const execucao = localizarBloco(superficie, "execucao");
|
|
1434
|
+
|
|
1435
|
+
if (!task && !impl && !vinculos) {
|
|
1436
|
+
diagnosticos.push(
|
|
1437
|
+
criarDiagnostico(
|
|
1438
|
+
"SEM069",
|
|
1439
|
+
`Superficie ${tipoSuperficie} "${nomeSuperficie}" precisa declarar task, impl ou vinculos para nao virar bloco decorativo.`,
|
|
1440
|
+
"erro",
|
|
1441
|
+
superficie.intervalo,
|
|
1442
|
+
"Declare ao menos uma task associada, um impl explicito ou vinculos rastreaveis com codigo vivo.",
|
|
1443
|
+
),
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (task && !tasksConhecidas.has(task.valor)) {
|
|
1448
|
+
diagnosticos.push(
|
|
1449
|
+
criarDiagnostico(
|
|
1450
|
+
"SEM070",
|
|
1451
|
+
`Superficie ${tipoSuperficie} "${nomeSuperficie}" referencia task "${task.valor}" que nao existe.`,
|
|
1452
|
+
"erro",
|
|
1453
|
+
task.intervalo,
|
|
1454
|
+
"Ajuste a task para apontar para uma task declarada ou importada.",
|
|
1455
|
+
),
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (input) {
|
|
1460
|
+
validarCamposDeTipos(input.campos, tiposConhecidos, diagnosticos, `input da superficie ${tipoSuperficie} ${nomeSuperficie}`);
|
|
1461
|
+
}
|
|
1462
|
+
if (output) {
|
|
1463
|
+
validarCamposDeTipos(output.campos, tiposConhecidos, diagnosticos, `output da superficie ${tipoSuperficie} ${nomeSuperficie}`);
|
|
1464
|
+
}
|
|
1465
|
+
if (effects) {
|
|
1466
|
+
validarEfeitosDeclarados(effects.linhas, diagnosticos, `effects da superficie ${tipoSuperficie} ${nomeSuperficie}`);
|
|
1467
|
+
}
|
|
1468
|
+
validarExecucaoBloco(execucao, diagnosticos, `superficie ${tipoSuperficie} "${nomeSuperficie}"`);
|
|
1469
|
+
validarVinculos(vinculos, diagnosticos, `${tipoSuperficie} ${nomeSuperficie}`);
|
|
1470
|
+
|
|
1471
|
+
const perfilSeguranca = coletarPerfilSegurancaDeclarado(
|
|
1472
|
+
superficie,
|
|
1473
|
+
effects,
|
|
1474
|
+
diagnosticos,
|
|
1475
|
+
`superficie ${tipoSuperficie} "${nomeSuperficie}"`,
|
|
1476
|
+
);
|
|
1477
|
+
emitirGuardrailsSeguranca(
|
|
1478
|
+
`Superficie ${tipoSuperficie} "${nomeSuperficie}"`,
|
|
1479
|
+
superficie.intervalo,
|
|
1480
|
+
perfilSeguranca,
|
|
1481
|
+
diagnosticos,
|
|
1482
|
+
{
|
|
1483
|
+
publico: superficieEhPublica(superficie, tipoSuperficie),
|
|
1484
|
+
sensivel: perfilSeguranca.efeitoPrivilegiado || perfilSeguranca.dadosSensiveis,
|
|
1485
|
+
},
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function recomporCaminhoRoute(campo?: CampoAst): string | undefined {
|
|
1490
|
+
if (!campo) {
|
|
1491
|
+
return undefined;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
return [campo.valor, ...campo.modificadores]
|
|
1495
|
+
.join(" ")
|
|
1496
|
+
.replace(/\s*\/\s*/g, "/")
|
|
1497
|
+
.trim();
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function serializarTransicao(origem: string, destino: string): string {
|
|
1501
|
+
return `${origem}->${destino}`;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function descreverSugestoes(valores: Iterable<string>, prefixo: string): string | undefined {
|
|
1505
|
+
const lista = [...new Set([...valores].filter(Boolean))].sort((a, b) => a.localeCompare(b, "pt-BR"));
|
|
1506
|
+
if (lista.length === 0) {
|
|
1507
|
+
return undefined;
|
|
1508
|
+
}
|
|
1509
|
+
const recorte = lista.slice(0, 6).join(", ");
|
|
1510
|
+
return `${prefixo}: ${recorte}${lista.length > 6 ? ", ..." : ""}.`;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function listarCandidatosUseRelativo(moduloAtual: string, caminhoImportado: string): string[] {
|
|
1514
|
+
const segmentos = moduloAtual.split(".").filter(Boolean);
|
|
1515
|
+
const caminhoNormalizado = caminhoImportado.replace(/^\.+/u, "").trim();
|
|
1516
|
+
if (!caminhoNormalizado || segmentos.length <= 1) {
|
|
1517
|
+
return [];
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const candidatos: string[] = [];
|
|
1521
|
+
for (let tamanho = segmentos.length - 1; tamanho >= 1; tamanho -= 1) {
|
|
1522
|
+
const candidato = [...segmentos.slice(0, tamanho), caminhoNormalizado].join(".");
|
|
1523
|
+
if (candidato !== caminhoImportado) {
|
|
1524
|
+
candidatos.push(candidato);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
return [...new Set(candidatos)];
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function resolverUseSema(
|
|
1532
|
+
moduloAtual: string,
|
|
1533
|
+
caminhoImportado: string,
|
|
1534
|
+
contextosModulos?: Map<string, ContextoSemantico>,
|
|
1535
|
+
): { caminhoResolvido?: string; candidatosRelativos: string[] } {
|
|
1536
|
+
if (!contextosModulos) {
|
|
1537
|
+
return { caminhoResolvido: undefined, candidatosRelativos: [] };
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
if (contextosModulos.has(caminhoImportado)) {
|
|
1541
|
+
return { caminhoResolvido: caminhoImportado, candidatosRelativos: [] };
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
const candidatosRelativos = listarCandidatosUseRelativo(moduloAtual, caminhoImportado);
|
|
1545
|
+
const caminhoResolvido = candidatosRelativos.find((candidato) => contextosModulos.has(candidato));
|
|
1546
|
+
return { caminhoResolvido, candidatosRelativos };
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function descreverDicaSintaxeExpressao(texto: string, dicaPadrao: string): string {
|
|
1550
|
+
const normalizado = texto.trim();
|
|
1551
|
+
if (
|
|
1552
|
+
/\sou\s/u.test(normalizado)
|
|
1553
|
+
&& (
|
|
1554
|
+
normalizado.includes("\"")
|
|
1555
|
+
|| normalizado.includes("'")
|
|
1556
|
+
|| /^[A-Za-z_][A-Za-z0-9_.]*\s*(==|!=|>|<|>=|<=)\s+.+\s+ou\s+.+$/u.test(normalizado)
|
|
1557
|
+
)
|
|
1558
|
+
&& !/\bem\s+\[/u.test(normalizado)
|
|
1559
|
+
) {
|
|
1560
|
+
const alvo = normalizado.match(/^([A-Za-z_][A-Za-z0-9_.]*)\s*(==|!=|>|<|>=|<=)/u)?.[1];
|
|
1561
|
+
if (alvo) {
|
|
1562
|
+
return `${dicaPadrao} Se a ideia era comparar ${alvo} contra varios valores, repita o campo em cada comparacao ou prefira "${alvo} em [A, B]".`;
|
|
1563
|
+
}
|
|
1564
|
+
return `${dicaPadrao} Se a ideia era comparar um campo contra varios valores, repita o campo em cada comparacao ou prefira "campo em [A, B]".`;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
return dicaPadrao;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function validarExpressoesDeclaradas(
|
|
1571
|
+
linhas: BlocoGenericoAst["linhas"],
|
|
1572
|
+
diagnosticos: Diagnostico[],
|
|
1573
|
+
contexto: {
|
|
1574
|
+
codigoErroSintaxe: string;
|
|
1575
|
+
codigoErroReferencia: string;
|
|
1576
|
+
nomeBloco: string;
|
|
1577
|
+
simbolosPermitidos: Set<string>;
|
|
1578
|
+
dicaSintaxe: string;
|
|
1579
|
+
dicaReferencia: string;
|
|
1580
|
+
aceitarMarcadoresSemanticos?: boolean;
|
|
1581
|
+
dicaReferenciaPersonalizada?: (raiz: string, linha: string) => string | undefined;
|
|
1582
|
+
},
|
|
1583
|
+
): void {
|
|
1584
|
+
for (const linha of linhas) {
|
|
1585
|
+
const expressao = parsearExpressaoSemantica(linha.conteudo);
|
|
1586
|
+
if (!expressao) {
|
|
1587
|
+
diagnosticos.push(
|
|
1588
|
+
criarDiagnostico(
|
|
1589
|
+
contexto.codigoErroSintaxe,
|
|
1590
|
+
`Declaracao invalida em ${contexto.nomeBloco}: "${linha.conteudo}".`,
|
|
1591
|
+
"erro",
|
|
1592
|
+
linha.intervalo,
|
|
1593
|
+
descreverDicaSintaxeExpressao(linha.conteudo, contexto.dicaSintaxe),
|
|
1594
|
+
),
|
|
1595
|
+
);
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
for (const referencia of extrairReferenciasDaExpressao(expressao)) {
|
|
1600
|
+
const raiz = extrairRaiz(referencia);
|
|
1601
|
+
const referenciaPermitida = contexto.simbolosPermitidos.has(raiz) || (contexto.aceitarMarcadoresSemanticos && ehMarcadorSemantico(raiz));
|
|
1602
|
+
if (!referenciaPermitida) {
|
|
1603
|
+
diagnosticos.push(
|
|
1604
|
+
criarDiagnostico(
|
|
1605
|
+
contexto.codigoErroReferencia,
|
|
1606
|
+
`Declaracao em ${contexto.nomeBloco} referencia "${raiz}", que nao pertence ao contexto permitido.`,
|
|
1607
|
+
"erro",
|
|
1608
|
+
linha.intervalo,
|
|
1609
|
+
contexto.dicaReferenciaPersonalizada?.(raiz, linha.conteudo) ?? contexto.dicaReferencia,
|
|
1610
|
+
),
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
function validarEfeitosDeclarados(linhas: BlocoGenericoAst["linhas"], diagnosticos: Diagnostico[], contexto: string): void {
|
|
1618
|
+
for (const linha of linhas) {
|
|
1619
|
+
const efeito = parsearEfeitoSemantico(linha.conteudo);
|
|
1620
|
+
if (!efeito) {
|
|
1621
|
+
diagnosticos.push(
|
|
1622
|
+
criarDiagnostico(
|
|
1623
|
+
"SEM023",
|
|
1624
|
+
`Declaracao invalida de efeito em ${contexto}: "${linha.conteudo}".`,
|
|
1625
|
+
"erro",
|
|
1626
|
+
linha.intervalo,
|
|
1627
|
+
"Use o formato \"categoria alvo\" ou \"categoria alvo detalhe\", podendo adicionar criticidade=..., privilegio=... e isolamento=....",
|
|
1628
|
+
),
|
|
1629
|
+
);
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (!ehCategoriaEfeitoSemantico(efeito.categoria)) {
|
|
1634
|
+
diagnosticos.push(
|
|
1635
|
+
criarDiagnostico(
|
|
1636
|
+
"SEM048",
|
|
1637
|
+
`Categoria de efeito "${efeito.categoria}" nao e suportada em ${contexto}.`,
|
|
1638
|
+
"erro",
|
|
1639
|
+
linha.intervalo,
|
|
1640
|
+
"Use categorias como persistencia, consulta, evento, auditoria, db.write, queue.publish, fs.write, network.egress, secret.read ou shell.exec.",
|
|
1641
|
+
),
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
if (efeito.criticidadeTexto && !ehCriticidadeEfeitoSemantico(efeito.criticidadeTexto)) {
|
|
1646
|
+
diagnosticos.push(
|
|
1647
|
+
criarDiagnostico(
|
|
1648
|
+
"SEM052",
|
|
1649
|
+
`Criticidade de efeito "${efeito.criticidadeTexto}" nao e suportada em ${contexto}.`,
|
|
1650
|
+
"erro",
|
|
1651
|
+
linha.intervalo,
|
|
1652
|
+
"Use apenas criticidade=baixa, criticidade=media, criticidade=alta ou criticidade=critica.",
|
|
1653
|
+
),
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
if (
|
|
1658
|
+
efeito.privilegioTexto
|
|
1659
|
+
&& !["leitura", "escrita", "publicacao", "execucao", "admin", "egress"].includes(efeito.privilegioTexto)
|
|
1660
|
+
) {
|
|
1661
|
+
diagnosticos.push(
|
|
1662
|
+
criarDiagnostico(
|
|
1663
|
+
"SEM092",
|
|
1664
|
+
`Privilegio de efeito "${efeito.privilegioTexto}" nao e suportado em ${contexto}.`,
|
|
1665
|
+
"erro",
|
|
1666
|
+
linha.intervalo,
|
|
1667
|
+
"Use privilegio=leitura, privilegio=escrita, privilegio=publicacao, privilegio=execucao, privilegio=admin ou privilegio=egress.",
|
|
1668
|
+
),
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (
|
|
1673
|
+
efeito.isolamentoTexto
|
|
1674
|
+
&& !["tenant", "processo", "host", "vps", "global"].includes(efeito.isolamentoTexto)
|
|
1675
|
+
) {
|
|
1676
|
+
diagnosticos.push(
|
|
1677
|
+
criarDiagnostico(
|
|
1678
|
+
"SEM093",
|
|
1679
|
+
`Isolamento de efeito "${efeito.isolamentoTexto}" nao e suportado em ${contexto}.`,
|
|
1680
|
+
"erro",
|
|
1681
|
+
linha.intervalo,
|
|
1682
|
+
"Use isolamento=tenant, isolamento=processo, isolamento=host, isolamento=vps ou isolamento=global.",
|
|
1683
|
+
),
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function validarState(
|
|
1690
|
+
state: StateAst,
|
|
1691
|
+
tiposConhecidos: Set<string>,
|
|
1692
|
+
enumsConhecidos: Map<string, Set<string>>,
|
|
1693
|
+
diagnosticos: Diagnostico[],
|
|
1694
|
+
): void {
|
|
1695
|
+
const possuiConteudo = state.corpo.campos.length > 0 || state.corpo.linhas.length > 0 || state.corpo.blocos.length > 0;
|
|
1696
|
+
if (!possuiConteudo) {
|
|
1697
|
+
diagnosticos.push(
|
|
1698
|
+
criarDiagnostico(
|
|
1699
|
+
"SEM011",
|
|
1700
|
+
`Bloco state${state.nome ? ` "${state.nome}"` : ""} precisa declarar campos, linhas ou subblocos.`,
|
|
1701
|
+
"erro",
|
|
1702
|
+
state.intervalo,
|
|
1703
|
+
"Use state para modelar informacao ou transicao observavel, nao como bloco vazio.",
|
|
1704
|
+
),
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
validarCamposDeTipos(state.corpo.campos, tiposConhecidos, diagnosticos, `state ${state.nome ?? "<anonimo>"}`);
|
|
1709
|
+
const fields = localizarBloco(state.corpo, "fields");
|
|
1710
|
+
if (fields) {
|
|
1711
|
+
validarCamposDeTipos(fields.campos, tiposConhecidos, diagnosticos, `fields do state ${state.nome ?? "<anonimo>"}`);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
const nomesCampos = new Set([
|
|
1715
|
+
...state.corpo.campos.map((campo) => campo.nome),
|
|
1716
|
+
...(fields?.campos ?? []).map((campo) => campo.nome),
|
|
1717
|
+
]);
|
|
1718
|
+
|
|
1719
|
+
const invariants = localizarBloco(state.corpo, "invariants");
|
|
1720
|
+
if (invariants) {
|
|
1721
|
+
validarExpressoesDeclaradas(invariants.linhas, diagnosticos, {
|
|
1722
|
+
codigoErroSintaxe: "SEM024",
|
|
1723
|
+
codigoErroReferencia: "SEM025",
|
|
1724
|
+
nomeBloco: `invariants do state ${state.nome ?? "<anonimo>"}`,
|
|
1725
|
+
simbolosPermitidos: nomesCampos,
|
|
1726
|
+
dicaSintaxe: "Use expressoes como \"campo existe\", \"campo == valor\" ou \"campo em [A, B]\".",
|
|
1727
|
+
dicaReferencia: "Referencie apenas campos do proprio state nas invariantes.",
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const transitions = localizarBloco(state.corpo, "transitions");
|
|
1732
|
+
if (transitions) {
|
|
1733
|
+
const campoTransicao = (fields?.campos ?? []).find((campo) => campo.nome === "status" || campo.nome === "estado")
|
|
1734
|
+
?? state.corpo.campos.find((campo) => campo.nome === "status" || campo.nome === "estado");
|
|
1735
|
+
|
|
1736
|
+
if (!campoTransicao) {
|
|
1737
|
+
diagnosticos.push(
|
|
1738
|
+
criarDiagnostico(
|
|
1739
|
+
"SEM026",
|
|
1740
|
+
`State ${state.nome ? `"${state.nome}" ` : ""}declarou transitions sem um campo status ou estado.`,
|
|
1741
|
+
"erro",
|
|
1742
|
+
transitions.intervalo,
|
|
1743
|
+
"Adicione um campo status ou estado para ancorar semanticamente as transicoes.",
|
|
1744
|
+
),
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
const enumValores = campoTransicao ? enumsConhecidos.get(campoTransicao.valor) : undefined;
|
|
1749
|
+
for (const linha of transitions.linhas) {
|
|
1750
|
+
const transicao = parsearTransicaoEstado(linha.conteudo);
|
|
1751
|
+
if (!transicao) {
|
|
1752
|
+
diagnosticos.push(
|
|
1753
|
+
criarDiagnostico(
|
|
1754
|
+
"SEM027",
|
|
1755
|
+
`Transicao invalida em state ${state.nome ?? "<anonimo>"}: "${linha.conteudo}".`,
|
|
1756
|
+
"erro",
|
|
1757
|
+
linha.intervalo,
|
|
1758
|
+
"Use o formato \"ORIGEM -> DESTINO\" para declarar transicoes.",
|
|
1759
|
+
),
|
|
1760
|
+
);
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
if (enumValores) {
|
|
1765
|
+
if (!enumValores.has(transicao.origem)) {
|
|
1766
|
+
diagnosticos.push(
|
|
1767
|
+
criarDiagnostico(
|
|
1768
|
+
"SEM028",
|
|
1769
|
+
`Transicao do state ${state.nome ?? "<anonimo>"} usa origem "${transicao.origem}" fora do enum ${campoTransicao?.valor}.`,
|
|
1770
|
+
"erro",
|
|
1771
|
+
linha.intervalo,
|
|
1772
|
+
"Use apenas valores declarados no enum associado ao campo status/estado.",
|
|
1773
|
+
),
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
if (!enumValores.has(transicao.destino)) {
|
|
1777
|
+
diagnosticos.push(
|
|
1778
|
+
criarDiagnostico(
|
|
1779
|
+
"SEM029",
|
|
1780
|
+
`Transicao do state ${state.nome ?? "<anonimo>"} usa destino "${transicao.destino}" fora do enum ${campoTransicao?.valor}.`,
|
|
1781
|
+
"erro",
|
|
1782
|
+
linha.intervalo,
|
|
1783
|
+
"Use apenas valores declarados no enum associado ao campo status/estado.",
|
|
1784
|
+
),
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function validarInvariantesDeCampos(
|
|
1793
|
+
bloco: BlocoGenericoAst,
|
|
1794
|
+
nomeBloco: string,
|
|
1795
|
+
diagnosticos: Diagnostico[],
|
|
1796
|
+
): void {
|
|
1797
|
+
const fields = localizarBloco(bloco, "fields");
|
|
1798
|
+
const nomesCampos = new Set([
|
|
1799
|
+
...bloco.campos.map((campo) => campo.nome),
|
|
1800
|
+
...(fields?.campos ?? []).map((campo) => campo.nome),
|
|
1801
|
+
]);
|
|
1802
|
+
|
|
1803
|
+
const invariants = localizarBloco(bloco, "invariants");
|
|
1804
|
+
if (!invariants) {
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
validarExpressoesDeclaradas(invariants.linhas, diagnosticos, {
|
|
1809
|
+
codigoErroSintaxe: "SEM062",
|
|
1810
|
+
codigoErroReferencia: "SEM063",
|
|
1811
|
+
nomeBloco: `invariants de ${nomeBloco}`,
|
|
1812
|
+
simbolosPermitidos: nomesCampos,
|
|
1813
|
+
dicaSintaxe: "Use expressoes como \"campo existe\", \"campo == valor\" ou \"campo em [A, B]\".",
|
|
1814
|
+
dicaReferencia: "Referencie apenas campos declarados no proprio bloco ao escrever invariantes de dominio.",
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function validarFlow(
|
|
1819
|
+
flow: FlowAst,
|
|
1820
|
+
tasksConhecidas: Set<string>,
|
|
1821
|
+
tarefasDetalhadas: Map<string, ResumoTaskSemantico>,
|
|
1822
|
+
diagnosticos: Diagnostico[],
|
|
1823
|
+
): void {
|
|
1824
|
+
const possuiEtapas = flow.corpo.linhas.length > 0 || flow.corpo.campos.length > 0 || flow.corpo.blocos.length > 0;
|
|
1825
|
+
if (!possuiEtapas) {
|
|
1826
|
+
diagnosticos.push(
|
|
1827
|
+
criarDiagnostico(
|
|
1828
|
+
"SEM012",
|
|
1829
|
+
`Flow "${flow.nome}" precisa declarar ao menos uma etapa.`,
|
|
1830
|
+
"erro",
|
|
1831
|
+
flow.intervalo,
|
|
1832
|
+
"Adicione linhas declarativas, campos ou subblocos dentro de flow.",
|
|
1833
|
+
),
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
const effects = localizarBloco(flow.corpo, "effects");
|
|
1838
|
+
if (effects) {
|
|
1839
|
+
validarEfeitosDeclarados(effects.linhas, diagnosticos, `effects do flow ${flow.nome}`);
|
|
1840
|
+
}
|
|
1841
|
+
validarVinculos(flow.vinculos, diagnosticos, `flow ${flow.nome}`);
|
|
1842
|
+
|
|
1843
|
+
const tarefasReferenciadas = flow.corpo.campos
|
|
1844
|
+
.filter((campo) => campo.nome === "task" || campo.nome === "tarefa")
|
|
1845
|
+
.map((campo) => campo.valor);
|
|
1846
|
+
|
|
1847
|
+
for (const tarefa of tarefasReferenciadas) {
|
|
1848
|
+
if (!tasksConhecidas.has(tarefa)) {
|
|
1849
|
+
diagnosticos.push(
|
|
1850
|
+
criarDiagnostico(
|
|
1851
|
+
"SEM013",
|
|
1852
|
+
`Flow "${flow.nome}" referencia task "${tarefa}" que nao existe.`,
|
|
1853
|
+
"erro",
|
|
1854
|
+
flow.intervalo,
|
|
1855
|
+
"Declare a task no mesmo modulo ou ajuste a referencia do flow.",
|
|
1856
|
+
),
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
const etapas = flow.corpo.linhas
|
|
1862
|
+
.map((linha) => ({ linha, etapa: parsearEtapaFlow(linha.conteudo) }))
|
|
1863
|
+
.filter((item) => item.linha.conteudo.trim().startsWith("etapa "));
|
|
1864
|
+
|
|
1865
|
+
const nomesEtapas = new Set<string>();
|
|
1866
|
+
const contextoFlow = new Set(flow.corpo.campos.map((campo) => campo.nome));
|
|
1867
|
+
const etapasValidas = new Map<string, NonNullable<(typeof etapas)[number]["etapa"]>>();
|
|
1868
|
+
for (const item of etapas) {
|
|
1869
|
+
if (!item.etapa) {
|
|
1870
|
+
diagnosticos.push(
|
|
1871
|
+
criarDiagnostico(
|
|
1872
|
+
"SEM032",
|
|
1873
|
+
`Linha de etapa invalida em flow "${flow.nome}": "${item.linha.conteudo}".`,
|
|
1874
|
+
"erro",
|
|
1875
|
+
item.linha.intervalo,
|
|
1876
|
+
"Use o formato \"etapa nome usa task com campo=valor quando expressao depende_de etapa_a, etapa_b em_sucesso proxima em_erro falha\".",
|
|
1877
|
+
),
|
|
1878
|
+
);
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (nomesEtapas.has(item.etapa.nome)) {
|
|
1883
|
+
diagnosticos.push(
|
|
1884
|
+
criarDiagnostico(
|
|
1885
|
+
"SEM033",
|
|
1886
|
+
`Flow "${flow.nome}" declarou a etapa "${item.etapa.nome}" mais de uma vez.`,
|
|
1887
|
+
"erro",
|
|
1888
|
+
item.linha.intervalo,
|
|
1889
|
+
"Use nomes unicos para cada etapa estruturada do flow.",
|
|
1890
|
+
),
|
|
1891
|
+
);
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
nomesEtapas.add(item.etapa.nome);
|
|
1896
|
+
etapasValidas.set(item.etapa.nome, item.etapa);
|
|
1897
|
+
|
|
1898
|
+
if (item.etapa.task && !tasksConhecidas.has(item.etapa.task)) {
|
|
1899
|
+
const sugestoesTasks = descreverSugestoes(tasksConhecidas, "Tasks conhecidas no contexto");
|
|
1900
|
+
diagnosticos.push(
|
|
1901
|
+
criarDiagnostico(
|
|
1902
|
+
"SEM034",
|
|
1903
|
+
`Etapa "${item.etapa.nome}" do flow "${flow.nome}" usa task "${item.etapa.task}" que nao existe.`,
|
|
1904
|
+
"erro",
|
|
1905
|
+
item.linha.intervalo,
|
|
1906
|
+
sugestoesTasks ?? "Ajuste a task da etapa para apontar para uma task declarada ou importada.",
|
|
1907
|
+
),
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
if (item.etapa.task) {
|
|
1912
|
+
const detalhesTask = tarefasDetalhadas.get(item.etapa.task);
|
|
1913
|
+
if (detalhesTask) {
|
|
1914
|
+
const indiceInput = indicesCampos(detalhesTask.input);
|
|
1915
|
+
for (const mapeamento of item.etapa.mapeamentos) {
|
|
1916
|
+
const campoInput = indiceInput.get(mapeamento.campo);
|
|
1917
|
+
if (!campoInput) {
|
|
1918
|
+
diagnosticos.push(
|
|
1919
|
+
criarDiagnostico(
|
|
1920
|
+
"SEM042",
|
|
1921
|
+
`Etapa "${item.etapa.nome}" do flow "${flow.nome}" mapeia o campo "${mapeamento.campo}", que nao existe no input da task "${item.etapa.task}".`,
|
|
1922
|
+
"erro",
|
|
1923
|
+
item.linha.intervalo,
|
|
1924
|
+
"Use apenas campos declarados no input da task associada a etapa.",
|
|
1925
|
+
),
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
const raizValor = extrairRaiz(mapeamento.valor);
|
|
1930
|
+
const ehLiteral = ["verdadeiro", "falso", "nulo"].includes(mapeamento.valor)
|
|
1931
|
+
|| /^-?\d+(?:\.\d+)?$/.test(mapeamento.valor)
|
|
1932
|
+
|| /^".*"$/.test(mapeamento.valor);
|
|
1933
|
+
const aceitaLiteralTextual = Boolean(
|
|
1934
|
+
campoInput
|
|
1935
|
+
&& ["Texto", "Id", "Email", "Url"].includes(campoInput.tipo)
|
|
1936
|
+
&& !mapeamento.valor.includes(".")
|
|
1937
|
+
&& !contextoFlow.has(raizValor)
|
|
1938
|
+
&& !nomesEtapas.has(raizValor),
|
|
1939
|
+
);
|
|
1940
|
+
if (!ehLiteral && !aceitaLiteralTextual && !contextoFlow.has(raizValor) && !nomesEtapas.has(raizValor)) {
|
|
1941
|
+
const sugestoesContexto = [
|
|
1942
|
+
descreverSugestoes(contextoFlow, "Campos do flow"),
|
|
1943
|
+
descreverSugestoes(nomesEtapas, "Etapas conhecidas"),
|
|
1944
|
+
].filter(Boolean).join(" ");
|
|
1945
|
+
diagnosticos.push(
|
|
1946
|
+
criarDiagnostico(
|
|
1947
|
+
"SEM043",
|
|
1948
|
+
`Etapa "${item.etapa.nome}" do flow "${flow.nome}" referencia "${mapeamento.valor}" fora do contexto conhecido.`,
|
|
1949
|
+
"erro",
|
|
1950
|
+
item.linha.intervalo,
|
|
1951
|
+
sugestoesContexto || "Mapeie usando campos do proprio flow, saidas de etapas anteriores ou literais simples.",
|
|
1952
|
+
),
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
if (mapeamento.valor.includes(".")) {
|
|
1957
|
+
const [etapaOrigem, campoSaida] = mapeamento.valor.split(".", 2);
|
|
1958
|
+
const etapaReferenciada = etapaOrigem ? etapasValidas.get(etapaOrigem) : undefined;
|
|
1959
|
+
const taskReferenciada = etapaReferenciada?.task ? tarefasDetalhadas.get(etapaReferenciada.task) : undefined;
|
|
1960
|
+
const indiceOutput = taskReferenciada ? indicesCampos(taskReferenciada.output) : undefined;
|
|
1961
|
+
if (etapaOrigem && campoSaida && etapaReferenciada && indiceOutput && !indiceOutput.has(campoSaida)) {
|
|
1962
|
+
diagnosticos.push(
|
|
1963
|
+
criarDiagnostico(
|
|
1964
|
+
"SEM044",
|
|
1965
|
+
`Etapa "${item.etapa.nome}" do flow "${flow.nome}" referencia a saida "${campoSaida}" da etapa "${etapaOrigem}", mas essa saida nao existe na task associada.`,
|
|
1966
|
+
"erro",
|
|
1967
|
+
item.linha.intervalo,
|
|
1968
|
+
"Use apenas campos declarados no output da task da etapa de origem.",
|
|
1969
|
+
),
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
if (item.etapa.condicao) {
|
|
1978
|
+
const referencias = extrairReferenciasDaExpressao(item.etapa.condicao).map((referencia) => extrairRaiz(referencia));
|
|
1979
|
+
for (const referencia of referencias) {
|
|
1980
|
+
if (!ehMarcadorSemantico(referencia) && !tasksConhecidas.has(referencia) && !nomesEtapas.has(referencia) && !contextoFlow.has(referencia)) {
|
|
1981
|
+
diagnosticos.push(
|
|
1982
|
+
criarDiagnostico(
|
|
1983
|
+
"SEM035",
|
|
1984
|
+
`Condicao da etapa "${item.etapa.nome}" em flow "${flow.nome}" referencia "${referencia}" fora do contexto atual.`,
|
|
1985
|
+
"erro",
|
|
1986
|
+
item.linha.intervalo,
|
|
1987
|
+
"No MVP atual, condicoes de flow devem apontar para marcadores semanticos, campos do flow, tasks conhecidas ou etapas anteriores.",
|
|
1988
|
+
),
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
for (const item of etapas) {
|
|
1996
|
+
if (!item.etapa) {
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
for (const dependencia of item.etapa.dependencias) {
|
|
2000
|
+
if (!nomesEtapas.has(dependencia)) {
|
|
2001
|
+
const sugestoesEtapas = descreverSugestoes(nomesEtapas, "Etapas declaradas");
|
|
2002
|
+
diagnosticos.push(
|
|
2003
|
+
criarDiagnostico(
|
|
2004
|
+
"SEM036",
|
|
2005
|
+
`Etapa "${item.etapa.nome}" do flow "${flow.nome}" depende de "${dependencia}", que nao foi declarada.`,
|
|
2006
|
+
"erro",
|
|
2007
|
+
item.linha.intervalo,
|
|
2008
|
+
sugestoesEtapas ?? "Declare a etapa dependente no mesmo flow antes de referencia-la.",
|
|
2009
|
+
),
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
for (const destino of [item.etapa.emSucesso, item.etapa.emErro].filter(Boolean)) {
|
|
2015
|
+
if (destino && !nomesEtapas.has(destino)) {
|
|
2016
|
+
const sugestoesEtapas = descreverSugestoes(nomesEtapas, "Etapas declaradas");
|
|
2017
|
+
diagnosticos.push(
|
|
2018
|
+
criarDiagnostico(
|
|
2019
|
+
"SEM045",
|
|
2020
|
+
`Etapa "${item.etapa.nome}" do flow "${flow.nome}" aponta para "${destino}" em ramificacao, mas essa etapa nao foi declarada.`,
|
|
2021
|
+
"erro",
|
|
2022
|
+
item.linha.intervalo,
|
|
2023
|
+
sugestoesEtapas ?? "Declare a etapa de destino no mesmo flow antes de usa-la em em_sucesso ou em_erro.",
|
|
2024
|
+
),
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
if (item.etapa.task) {
|
|
2030
|
+
const detalhesTask = tarefasDetalhadas.get(item.etapa.task);
|
|
2031
|
+
const indiceErrors = indiceErros(detalhesTask?.errors ?? []);
|
|
2032
|
+
for (const rotaErro of item.etapa.porErro) {
|
|
2033
|
+
if (!indiceErrors.has(rotaErro.tipo)) {
|
|
2034
|
+
const sugestoesErros = descreverSugestoes(indiceErrors.keys(), "Erros declarados pela task");
|
|
2035
|
+
diagnosticos.push(
|
|
2036
|
+
criarDiagnostico(
|
|
2037
|
+
"SEM046",
|
|
2038
|
+
`Etapa "${item.etapa.nome}" do flow "${flow.nome}" roteia o erro "${rotaErro.tipo}", mas esse erro nao pertence ao contrato da task "${item.etapa.task}".`,
|
|
2039
|
+
"erro",
|
|
2040
|
+
item.linha.intervalo,
|
|
2041
|
+
sugestoesErros ?? "Use apenas erros declarados pela task ou cobertos por testes de erro do contrato atual.",
|
|
2042
|
+
),
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
if (!nomesEtapas.has(rotaErro.destino)) {
|
|
2047
|
+
const sugestoesEtapas = descreverSugestoes(nomesEtapas, "Etapas declaradas");
|
|
2048
|
+
diagnosticos.push(
|
|
2049
|
+
criarDiagnostico(
|
|
2050
|
+
"SEM047",
|
|
2051
|
+
`Etapa "${item.etapa.nome}" do flow "${flow.nome}" aponta o erro "${rotaErro.tipo}" para "${rotaErro.destino}", mas essa etapa nao foi declarada.`,
|
|
2052
|
+
"erro",
|
|
2053
|
+
item.linha.intervalo,
|
|
2054
|
+
sugestoesEtapas ?? "Declare a etapa de destino no mesmo flow antes de usa-la em por_erro.",
|
|
2055
|
+
),
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
function validarVinculoEstadoDaTask(
|
|
2064
|
+
task: TaskAst,
|
|
2065
|
+
statesConhecidos: Map<string, { transicoes: Set<string> }>,
|
|
2066
|
+
diagnosticos: Diagnostico[],
|
|
2067
|
+
): void {
|
|
2068
|
+
if (!task.state) {
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
const nomeEstado = task.state.nome ?? task.state.campos.find((campo) => campo.nome === "state" || campo.nome === "estado")?.valor;
|
|
2073
|
+
if (!nomeEstado) {
|
|
2074
|
+
diagnosticos.push(
|
|
2075
|
+
criarDiagnostico(
|
|
2076
|
+
"SEM037",
|
|
2077
|
+
`Task "${task.nome}" declarou state sem indicar qual estado ela governa.`,
|
|
2078
|
+
"erro",
|
|
2079
|
+
task.state.intervalo,
|
|
2080
|
+
"Use \"state nome_do_estado { ... }\" ou declare \"estado: nome_do_estado\" dentro do bloco state da task.",
|
|
2081
|
+
),
|
|
2082
|
+
);
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
const blocoTransitions = localizarBloco(task.state, "transitions");
|
|
2087
|
+
const linhasTransicao = blocoTransitions?.linhas ?? task.state.linhas;
|
|
2088
|
+
const transicoesLocais = new Set(
|
|
2089
|
+
linhasTransicao
|
|
2090
|
+
.map((linha) => parsearTransicaoEstado(linha.conteudo))
|
|
2091
|
+
.filter((transicao): transicao is NonNullable<typeof transicao> => Boolean(transicao))
|
|
2092
|
+
.map((transicao) => serializarTransicao(transicao.origem, transicao.destino)),
|
|
2093
|
+
);
|
|
2094
|
+
|
|
2095
|
+
const estadoConhecido = statesConhecidos.get(nomeEstado)
|
|
2096
|
+
?? (transicoesLocais.size > 0 ? { transicoes: transicoesLocais } : undefined);
|
|
2097
|
+
if (!estadoConhecido) {
|
|
2098
|
+
diagnosticos.push(
|
|
2099
|
+
criarDiagnostico(
|
|
2100
|
+
"SEM038",
|
|
2101
|
+
`Task "${task.nome}" referencia state "${nomeEstado}", mas esse state nao foi encontrado.`,
|
|
2102
|
+
"erro",
|
|
2103
|
+
task.state.intervalo,
|
|
2104
|
+
"Declare o state no mesmo modulo ou importe um modulo que exponha esse state.",
|
|
2105
|
+
),
|
|
2106
|
+
);
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
if (linhasTransicao.length === 0) {
|
|
2111
|
+
diagnosticos.push(
|
|
2112
|
+
criarDiagnostico(
|
|
2113
|
+
"SEM039",
|
|
2114
|
+
`Task "${task.nome}" referencia state "${nomeEstado}" sem declarar transicoes.`,
|
|
2115
|
+
"erro",
|
|
2116
|
+
task.state.intervalo,
|
|
2117
|
+
"Declare ao menos uma transicao para explicitar como a task altera o estado.",
|
|
2118
|
+
),
|
|
2119
|
+
);
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
for (const linha of linhasTransicao) {
|
|
2124
|
+
const transicao = parsearTransicaoEstado(linha.conteudo);
|
|
2125
|
+
if (!transicao) {
|
|
2126
|
+
diagnosticos.push(
|
|
2127
|
+
criarDiagnostico(
|
|
2128
|
+
"SEM040",
|
|
2129
|
+
`Task "${task.nome}" declarou uma transicao invalida no state "${nomeEstado}": "${linha.conteudo}".`,
|
|
2130
|
+
"erro",
|
|
2131
|
+
linha.intervalo,
|
|
2132
|
+
"Use o formato \"ORIGEM -> DESTINO\" dentro do bloco transitions da task.",
|
|
2133
|
+
),
|
|
2134
|
+
);
|
|
2135
|
+
continue;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
if (!estadoConhecido.transicoes.has(serializarTransicao(transicao.origem, transicao.destino))) {
|
|
2139
|
+
diagnosticos.push(
|
|
2140
|
+
criarDiagnostico(
|
|
2141
|
+
"SEM041",
|
|
2142
|
+
`Task "${task.nome}" declarou a transicao "${transicao.origem} -> ${transicao.destino}" fora do contrato do state "${nomeEstado}".`,
|
|
2143
|
+
"erro",
|
|
2144
|
+
linha.intervalo,
|
|
2145
|
+
"Use apenas transicoes declaradas no state associado a task.",
|
|
2146
|
+
),
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
function validarContratoRoute(
|
|
2153
|
+
route: RouteAst,
|
|
2154
|
+
taskNome: string,
|
|
2155
|
+
tarefasDetalhadas: Map<string, ResumoTaskSemantico>,
|
|
2156
|
+
diagnosticos: Diagnostico[],
|
|
2157
|
+
): void {
|
|
2158
|
+
const inputPublico = localizarBloco(route.corpo, "input");
|
|
2159
|
+
const outputPublico = localizarBloco(route.corpo, "output");
|
|
2160
|
+
const errorPublico = localizarBloco(route.corpo, "error");
|
|
2161
|
+
const detalhesTask = tarefasDetalhadas.get(taskNome);
|
|
2162
|
+
|
|
2163
|
+
if (!detalhesTask) {
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
const indiceInputTask = indicesCampos(detalhesTask.input);
|
|
2168
|
+
const indiceOutputTask = indicesCampos(detalhesTask.output);
|
|
2169
|
+
const indiceErrorsTask = indiceErros(detalhesTask.errors);
|
|
2170
|
+
|
|
2171
|
+
if (inputPublico) {
|
|
2172
|
+
for (const campo of inputPublico.campos) {
|
|
2173
|
+
const campoTask = indiceInputTask.get(campo.nome);
|
|
2174
|
+
if (!campoTask) {
|
|
2175
|
+
diagnosticos.push(
|
|
2176
|
+
criarDiagnostico(
|
|
2177
|
+
"SEM049",
|
|
2178
|
+
`Route "${route.nome}" expoe o campo de input "${campo.nome}", mas ele nao existe na task "${taskNome}".`,
|
|
2179
|
+
"erro",
|
|
2180
|
+
campo.intervalo,
|
|
2181
|
+
"Use apenas campos declarados no input da task associada a route.",
|
|
2182
|
+
),
|
|
2183
|
+
);
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
if (campoTask.tipo !== campo.valor) {
|
|
2188
|
+
diagnosticos.push(
|
|
2189
|
+
criarDiagnostico(
|
|
2190
|
+
"SEM053",
|
|
2191
|
+
`Route "${route.nome}" declara o campo publico "${campo.nome}" com tipo "${campo.valor}", mas a task "${taskNome}" usa "${campoTask.tipo}".`,
|
|
2192
|
+
"erro",
|
|
2193
|
+
campo.intervalo,
|
|
2194
|
+
"Mantenha o tipo publico coerente com o contrato interno da task.",
|
|
2195
|
+
),
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
if (outputPublico) {
|
|
2202
|
+
for (const campo of outputPublico.campos) {
|
|
2203
|
+
const campoTask = indiceOutputTask.get(campo.nome);
|
|
2204
|
+
if (!campoTask) {
|
|
2205
|
+
diagnosticos.push(
|
|
2206
|
+
criarDiagnostico(
|
|
2207
|
+
"SEM050",
|
|
2208
|
+
`Route "${route.nome}" expoe o campo de output "${campo.nome}", mas ele nao existe na task "${taskNome}".`,
|
|
2209
|
+
"erro",
|
|
2210
|
+
campo.intervalo,
|
|
2211
|
+
"Use apenas campos declarados no output da task associada a route.",
|
|
2212
|
+
),
|
|
2213
|
+
);
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
if (campoTask.tipo !== campo.valor) {
|
|
2218
|
+
diagnosticos.push(
|
|
2219
|
+
criarDiagnostico(
|
|
2220
|
+
"SEM054",
|
|
2221
|
+
`Route "${route.nome}" declara o campo publico de saida "${campo.nome}" com tipo "${campo.valor}", mas a task "${taskNome}" usa "${campoTask.tipo}".`,
|
|
2222
|
+
"erro",
|
|
2223
|
+
campo.intervalo,
|
|
2224
|
+
"Mantenha o output publico coerente com o contrato interno da task.",
|
|
2225
|
+
),
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
if (errorPublico) {
|
|
2232
|
+
for (const campo of errorPublico.campos) {
|
|
2233
|
+
if (!indiceErrorsTask.has(campo.nome)) {
|
|
2234
|
+
diagnosticos.push(
|
|
2235
|
+
criarDiagnostico(
|
|
2236
|
+
"SEM051",
|
|
2237
|
+
`Route "${route.nome}" expoe o erro "${campo.nome}", mas ele nao pertence ao contrato da task "${taskNome}".`,
|
|
2238
|
+
"erro",
|
|
2239
|
+
campo.intervalo,
|
|
2240
|
+
"Exponha apenas erros declarados pela task associada a route.",
|
|
2241
|
+
),
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
function validarRoute(
|
|
2249
|
+
route: RouteAst,
|
|
2250
|
+
tasksConhecidas: Set<string>,
|
|
2251
|
+
tarefasDetalhadas: Map<string, ResumoTaskSemantico>,
|
|
2252
|
+
diagnosticos: Diagnostico[],
|
|
2253
|
+
): void {
|
|
2254
|
+
const metodo = localizarCampo(route.corpo, "metodo");
|
|
2255
|
+
const caminho = localizarCampo(route.corpo, "caminho");
|
|
2256
|
+
const caminhoResolvido = recomporCaminhoRoute(caminho);
|
|
2257
|
+
const task = localizarCampo(route.corpo, "task", "tarefa");
|
|
2258
|
+
const effects = localizarBloco(route.corpo, "effects");
|
|
2259
|
+
|
|
2260
|
+
if (!metodo) {
|
|
2261
|
+
diagnosticos.push(
|
|
2262
|
+
criarDiagnostico(
|
|
2263
|
+
"SEM014",
|
|
2264
|
+
`Route "${route.nome}" precisa declarar o campo metodo.`,
|
|
2265
|
+
"erro",
|
|
2266
|
+
route.intervalo,
|
|
2267
|
+
"Use um campo como metodo: GET, POST, PUT, PATCH ou DELETE.",
|
|
2268
|
+
),
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
if (!caminho) {
|
|
2273
|
+
diagnosticos.push(
|
|
2274
|
+
criarDiagnostico(
|
|
2275
|
+
"SEM015",
|
|
2276
|
+
`Route "${route.nome}" precisa declarar o campo caminho.`,
|
|
2277
|
+
"erro",
|
|
2278
|
+
route.intervalo,
|
|
2279
|
+
"Use um campo como caminho: \"/recurso\".",
|
|
2280
|
+
),
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
if (metodo) {
|
|
2285
|
+
const metodosValidos = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
2286
|
+
if (!metodosValidos.has(metodo.valor.toUpperCase())) {
|
|
2287
|
+
diagnosticos.push(
|
|
2288
|
+
criarDiagnostico(
|
|
2289
|
+
"SEM016",
|
|
2290
|
+
`Route "${route.nome}" usa metodo invalido "${metodo.valor}".`,
|
|
2291
|
+
"erro",
|
|
2292
|
+
metodo.intervalo,
|
|
2293
|
+
"Use apenas GET, POST, PUT, PATCH ou DELETE no MVP.",
|
|
2294
|
+
),
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
if (caminho && (!caminhoResolvido || !caminhoResolvido.startsWith("/"))) {
|
|
2300
|
+
diagnosticos.push(
|
|
2301
|
+
criarDiagnostico(
|
|
2302
|
+
"SEM017",
|
|
2303
|
+
`Route "${route.nome}" precisa usar um caminho iniciando com '/'.`,
|
|
2304
|
+
"erro",
|
|
2305
|
+
caminho.intervalo,
|
|
2306
|
+
"Exemplo valido: caminho: \"/produtos\".",
|
|
2307
|
+
),
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
if (effects) {
|
|
2312
|
+
validarEfeitosDeclarados(effects.linhas, diagnosticos, `effects da route ${route.nome}`);
|
|
2313
|
+
}
|
|
2314
|
+
validarVinculos(route.vinculos, diagnosticos, `route ${route.nome}`);
|
|
2315
|
+
|
|
2316
|
+
const perfilSeguranca = coletarPerfilSegurancaDeclarado(
|
|
2317
|
+
route.corpo,
|
|
2318
|
+
effects,
|
|
2319
|
+
diagnosticos,
|
|
2320
|
+
`route "${route.nome}"`,
|
|
2321
|
+
);
|
|
2322
|
+
emitirGuardrailsSeguranca(
|
|
2323
|
+
`Route "${route.nome}"`,
|
|
2324
|
+
route.intervalo,
|
|
2325
|
+
perfilSeguranca,
|
|
2326
|
+
diagnosticos,
|
|
2327
|
+
{
|
|
2328
|
+
publico: extrairPerfilCompatibilidade(route.corpo, "publico") === "publico",
|
|
2329
|
+
sensivel: routeEhMutante(route) || perfilSeguranca.efeitoPrivilegiado || perfilSeguranca.dadosSensiveis,
|
|
2330
|
+
},
|
|
2331
|
+
);
|
|
2332
|
+
|
|
2333
|
+
if (task && !tasksConhecidas.has(task.valor)) {
|
|
2334
|
+
diagnosticos.push(
|
|
2335
|
+
criarDiagnostico(
|
|
2336
|
+
"SEM018",
|
|
2337
|
+
`Route "${route.nome}" referencia task "${task.valor}" que nao existe.`,
|
|
2338
|
+
"erro",
|
|
2339
|
+
task.intervalo,
|
|
2340
|
+
"Ajuste o campo task da route para apontar para uma task declarada no modulo.",
|
|
2341
|
+
),
|
|
2342
|
+
);
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
if (task) {
|
|
2347
|
+
validarContratoRoute(route, task.valor, tarefasDetalhadas, diagnosticos);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
function validarGuardrailsSeguranca(modulo: ModuloAst, diagnosticos: Diagnostico[]): void {
|
|
2352
|
+
const superficies = coletarSuperficiesModulo(modulo);
|
|
2353
|
+
|
|
2354
|
+
for (const task of modulo.tasks) {
|
|
2355
|
+
const motivos = new Set<string>();
|
|
2356
|
+
const rotasPublicasAssociadas = modulo.routes.filter((route) =>
|
|
2357
|
+
localizarCampo(route.corpo, "task", "tarefa")?.valor === task.nome
|
|
2358
|
+
&& extrairPerfilCompatibilidade(route.corpo, "publico") === "publico");
|
|
2359
|
+
const superficiesPublicasAssociadas = superficies.filter((item) =>
|
|
2360
|
+
localizarCampo(item.superficie, "task", "tarefa")?.valor === task.nome
|
|
2361
|
+
&& superficieEhPublica(item.superficie, item.tipo));
|
|
2362
|
+
const perfilTask = coletarPerfilSegurancaDeclarado(task.corpo, task.effects, undefined, `task "${task.nome}"`);
|
|
2363
|
+
|
|
2364
|
+
if (taskEhSensivel(task)) {
|
|
2365
|
+
motivos.add("criticidade operacional alta/critica ou efeito sensivel");
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
for (const route of rotasPublicasAssociadas) {
|
|
2369
|
+
motivos.add(`route publica "${route.nome}"`);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
for (const item of superficiesPublicasAssociadas) {
|
|
2373
|
+
motivos.add(`superficie publica ${item.tipo} "${item.superficie.nome ?? item.tipo}"`);
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
const motivosOrdenados = [...motivos];
|
|
2377
|
+
if (motivosOrdenados.length === 0) {
|
|
2378
|
+
continue;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
const perfisPublicos = [
|
|
2382
|
+
...rotasPublicasAssociadas.map((route) => coletarPerfilSegurancaDeclarado(route.corpo, localizarBloco(route.corpo, "effects"), undefined, `route "${route.nome}"`)),
|
|
2383
|
+
...superficiesPublicasAssociadas.map((item) => coletarPerfilSegurancaDeclarado(item.superficie, localizarBloco(item.superficie, "effects"), undefined, `superficie ${item.tipo} "${item.superficie.nome ?? item.tipo}"`)),
|
|
2384
|
+
];
|
|
2385
|
+
const perfilPublico: PerfilSegurancaDeclarado = {
|
|
2386
|
+
auth: { ...perfilTask.auth, explicita: perfilTask.auth.explicita || perfisPublicos.some((perfil) => perfil.auth.explicita) },
|
|
2387
|
+
authz: {
|
|
2388
|
+
...perfilTask.authz,
|
|
2389
|
+
explicita: perfilTask.authz.explicita || perfisPublicos.some((perfil) => perfil.authz.explicita),
|
|
2390
|
+
papeis: [...new Set([perfilTask.authz.papeis, ...perfisPublicos.map((perfil) => perfil.authz.papeis)].flat())],
|
|
2391
|
+
escopos: [...new Set([perfilTask.authz.escopos, ...perfisPublicos.map((perfil) => perfil.authz.escopos)].flat())],
|
|
2392
|
+
},
|
|
2393
|
+
dados: {
|
|
2394
|
+
...perfilTask.dados,
|
|
2395
|
+
explicita: perfilTask.dados.explicita || perfisPublicos.some((perfil) => perfil.dados.explicita),
|
|
2396
|
+
campos: [...perfilTask.dados.campos, ...perfisPublicos.flatMap((perfil) => perfil.dados.campos)],
|
|
2397
|
+
},
|
|
2398
|
+
audit: { ...perfilTask.audit, explicita: perfilTask.audit.explicita || perfisPublicos.some((perfil) => perfil.audit.explicita) },
|
|
2399
|
+
segredos: {
|
|
2400
|
+
...perfilTask.segredos,
|
|
2401
|
+
explicita: perfilTask.segredos.explicita || perfisPublicos.some((perfil) => perfil.segredos.explicita),
|
|
2402
|
+
itens: [...perfilTask.segredos.itens, ...perfisPublicos.flatMap((perfil) => perfil.segredos.itens)],
|
|
2403
|
+
},
|
|
2404
|
+
forbidden: {
|
|
2405
|
+
...perfilTask.forbidden,
|
|
2406
|
+
explicita: perfilTask.forbidden.explicita || perfisPublicos.some((perfil) => perfil.forbidden.explicita),
|
|
2407
|
+
regras: [...new Set([perfilTask.forbidden.regras, ...perfisPublicos.map((perfil) => perfil.forbidden.regras)].flat())],
|
|
2408
|
+
},
|
|
2409
|
+
efeitoPrivilegiado: perfilTask.efeitoPrivilegiado || perfisPublicos.some((perfil) => perfil.efeitoPrivilegiado),
|
|
2410
|
+
dadosSensiveis: perfilTask.dadosSensiveis || perfisPublicos.some((perfil) => perfil.dadosSensiveis),
|
|
2411
|
+
exigeSegredos: perfilTask.exigeSegredos || perfisPublicos.some((perfil) => perfil.exigeSegredos),
|
|
2412
|
+
};
|
|
2413
|
+
|
|
2414
|
+
if (rotasPublicasAssociadas.length > 0 || superficiesPublicasAssociadas.length > 0) {
|
|
2415
|
+
emitirGuardrailsSeguranca(
|
|
2416
|
+
`Task "${task.nome}" exposta publicamente`,
|
|
2417
|
+
task.intervalo,
|
|
2418
|
+
perfilPublico,
|
|
2419
|
+
diagnosticos,
|
|
2420
|
+
{ publico: true, sensivel: false },
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
const resumoMotivos = motivosOrdenados.join(", ");
|
|
2425
|
+
if (!task.execucao) {
|
|
2426
|
+
diagnosticos.push(
|
|
2427
|
+
criarDiagnostico(
|
|
2428
|
+
"SEM071",
|
|
2429
|
+
`Task "${task.nome}" exige execucao explicita para producao por causa de ${resumoMotivos}, mas ainda opera com execucao implicita.`,
|
|
2430
|
+
"aviso",
|
|
2431
|
+
task.intervalo,
|
|
2432
|
+
"Declare timeout, retry, compensacao, idempotencia e criticidade_operacional no bloco execucao da task.",
|
|
2433
|
+
),
|
|
2434
|
+
);
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
if (!taskTemRastreabilidade(task)) {
|
|
2438
|
+
diagnosticos.push(
|
|
2439
|
+
criarDiagnostico(
|
|
2440
|
+
"SEM072",
|
|
2441
|
+
`Task "${task.nome}" exige rastreabilidade forte por causa de ${resumoMotivos}, mas ainda nao declara impl nem vinculos.`,
|
|
2442
|
+
"aviso",
|
|
2443
|
+
task.intervalo,
|
|
2444
|
+
"Adicione impl e/ou vinculos para apontar arquivo, simbolo, recurso ou superficie real do codigo vivo.",
|
|
2445
|
+
),
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
for (const item of superficies) {
|
|
2451
|
+
if (!superficieEhPublica(item.superficie, item.tipo)) {
|
|
2452
|
+
continue;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
const execucao = localizarBloco(item.superficie, "execucao");
|
|
2456
|
+
if (!execucao) {
|
|
2457
|
+
const nomeSuperficie = item.superficie.nome ?? item.tipo;
|
|
2458
|
+
diagnosticos.push(
|
|
2459
|
+
criarDiagnostico(
|
|
2460
|
+
"SEM073",
|
|
2461
|
+
`Superficie publica ${item.tipo} "${nomeSuperficie}" deveria declarar execucao explicita para producao, mas ainda depende do padrao implicito.`,
|
|
2462
|
+
"aviso",
|
|
2463
|
+
item.superficie.intervalo,
|
|
2464
|
+
`Declare timeout, retry, compensacao e criticidade_operacional no proprio bloco ${item.tipo}.`,
|
|
2465
|
+
),
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
function campoStatusTexto(campo: CampoAst): boolean {
|
|
2472
|
+
return (campo.nome === "status" || campo.nome === "estado") && campo.valor === "Texto";
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
function moduloTemStateComTransicao(modulo: ModuloAst): boolean {
|
|
2476
|
+
return modulo.states.some((state) => (localizarBloco(state.corpo, "transitions")?.linhas.length ?? 0) > 0);
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
function validarStatusTextoComState(modulo: ModuloAst, diagnosticos: Diagnostico[]): void {
|
|
2480
|
+
if (!moduloTemStateComTransicao(modulo)) {
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
for (const entity of modulo.entities) {
|
|
2485
|
+
const fields = localizarBloco(entity.corpo, "fields");
|
|
2486
|
+
for (const campo of fields?.campos ?? []) {
|
|
2487
|
+
if (!campoStatusTexto(campo)) {
|
|
2488
|
+
continue;
|
|
2489
|
+
}
|
|
2490
|
+
diagnosticos.push(
|
|
2491
|
+
criarDiagnostico(
|
|
2492
|
+
"SEM100",
|
|
2493
|
+
`Entity "${entity.nome}" declara "${campo.nome}: Texto" enquanto o modulo tem state com transitions.`,
|
|
2494
|
+
"aviso",
|
|
2495
|
+
campo.intervalo,
|
|
2496
|
+
"Prefira enum/status de dominio para impedir que a IA invente estados fora do ciclo declarado.",
|
|
2497
|
+
),
|
|
2498
|
+
);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
export function criarContextoLocal(modulo: ModuloAst): ContextoSemantico {
|
|
2504
|
+
const simbolos = new Map<string, SimboloSemantico>();
|
|
2505
|
+
const tiposConhecidos = new Set(TIPOS_PRIMITIVOS);
|
|
2506
|
+
const tasksConhecidas = new Set<string>();
|
|
2507
|
+
const tarefasDetalhadas = new Map<string, ResumoTaskSemantico>();
|
|
2508
|
+
const statesConhecidos = new Map<string, { transicoes: Set<string> }>();
|
|
2509
|
+
const enumsConhecidos = new Map<string, Set<string>>();
|
|
2510
|
+
|
|
2511
|
+
const registrar = (nome: string, categoria: SimboloSemantico["categoria"]): void => {
|
|
2512
|
+
if (simbolos.has(nome)) {
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
simbolos.set(nome, { nome, categoria });
|
|
2516
|
+
if (categoria === "task") {
|
|
2517
|
+
tasksConhecidas.add(nome);
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
if (categoria !== "database") {
|
|
2521
|
+
tiposConhecidos.add(nome);
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
|
|
2525
|
+
for (const type of modulo.types) {
|
|
2526
|
+
registrar(type.nome, "tipo");
|
|
2527
|
+
}
|
|
2528
|
+
for (const entity of modulo.entities) {
|
|
2529
|
+
registrar(entity.nome, "entity");
|
|
2530
|
+
}
|
|
2531
|
+
for (const enumeracao of modulo.enums) {
|
|
2532
|
+
registrar(enumeracao.nome, "enum");
|
|
2533
|
+
enumsConhecidos.set(enumeracao.nome, new Set(enumeracao.valores));
|
|
2534
|
+
}
|
|
2535
|
+
for (const task of modulo.tasks) {
|
|
2536
|
+
registrar(task.nome, "task");
|
|
2537
|
+
tarefasDetalhadas.set(task.nome, coletarResumoTask(task));
|
|
2538
|
+
}
|
|
2539
|
+
for (const flow of modulo.flows) {
|
|
2540
|
+
registrar(flow.nome, "flow");
|
|
2541
|
+
}
|
|
2542
|
+
for (const route of modulo.routes) {
|
|
2543
|
+
registrar(route.nome, "route");
|
|
2544
|
+
}
|
|
2545
|
+
for (const worker of modulo.workers) {
|
|
2546
|
+
if (worker.nome) {
|
|
2547
|
+
registrar(worker.nome, "worker");
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
for (const evento of modulo.eventos) {
|
|
2551
|
+
if (evento.nome) {
|
|
2552
|
+
registrar(evento.nome, "evento");
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
for (const fila of modulo.filas) {
|
|
2556
|
+
if (fila.nome) {
|
|
2557
|
+
registrar(fila.nome, "fila");
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
for (const cron of modulo.crons) {
|
|
2561
|
+
if (cron.nome) {
|
|
2562
|
+
registrar(cron.nome, "cron");
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
for (const webhook of modulo.webhooks) {
|
|
2566
|
+
if (webhook.nome) {
|
|
2567
|
+
registrar(webhook.nome, "webhook");
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
for (const cache of modulo.caches) {
|
|
2571
|
+
if (cache.nome) {
|
|
2572
|
+
registrar(cache.nome, "cache");
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
for (const storage of modulo.storages) {
|
|
2576
|
+
if (storage.nome) {
|
|
2577
|
+
registrar(storage.nome, "storage");
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
for (const policy of modulo.policies) {
|
|
2581
|
+
if (policy.nome) {
|
|
2582
|
+
registrar(policy.nome, "policy");
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
for (const database of modulo.databases) {
|
|
2586
|
+
if (database.nome) {
|
|
2587
|
+
registrar(database.nome, "database");
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
for (const state of modulo.states) {
|
|
2591
|
+
if (state.nome) {
|
|
2592
|
+
registrar(state.nome, "state");
|
|
2593
|
+
const transicoes = new Set(
|
|
2594
|
+
(localizarBloco(state.corpo, "transitions")?.linhas ?? [])
|
|
2595
|
+
.map((linha) => parsearTransicaoEstado(linha.conteudo))
|
|
2596
|
+
.filter((linha): linha is NonNullable<typeof linha> => Boolean(linha))
|
|
2597
|
+
.map((linha) => serializarTransicao(linha.origem, linha.destino)),
|
|
2598
|
+
);
|
|
2599
|
+
statesConhecidos.set(state.nome, { transicoes });
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
return {
|
|
2604
|
+
modulo: modulo.nome,
|
|
2605
|
+
simbolos,
|
|
2606
|
+
tiposConhecidos,
|
|
2607
|
+
tasksConhecidas,
|
|
2608
|
+
tarefasDetalhadas,
|
|
2609
|
+
statesConhecidos,
|
|
2610
|
+
modulosImportados: [],
|
|
2611
|
+
interoperabilidades: modulo.uses
|
|
2612
|
+
.filter(ehUseInterop)
|
|
2613
|
+
.map((use) => ({ origem: use.origem, caminho: use.caminho })),
|
|
2614
|
+
enumsConhecidos,
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
function validarTask(
|
|
2619
|
+
task: TaskAst,
|
|
2620
|
+
tiposConhecidos: Set<string>,
|
|
2621
|
+
statesConhecidos: Map<string, { transicoes: Set<string> }>,
|
|
2622
|
+
diagnosticos: Diagnostico[],
|
|
2623
|
+
): void {
|
|
2624
|
+
if (!task.input) {
|
|
2625
|
+
diagnosticos.push(
|
|
2626
|
+
criarDiagnostico(
|
|
2627
|
+
"SEM003",
|
|
2628
|
+
`Task "${task.nome}" precisa declarar input.`,
|
|
2629
|
+
"erro",
|
|
2630
|
+
task.intervalo,
|
|
2631
|
+
"Toda task precisa declarar as entradas de forma explicita.",
|
|
2632
|
+
),
|
|
2633
|
+
);
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
if (!task.output) {
|
|
2637
|
+
diagnosticos.push(
|
|
2638
|
+
criarDiagnostico(
|
|
2639
|
+
"SEM004",
|
|
2640
|
+
`Task "${task.nome}" precisa declarar output.`,
|
|
2641
|
+
"erro",
|
|
2642
|
+
task.intervalo,
|
|
2643
|
+
"Toda task precisa declarar a saida esperada.",
|
|
2644
|
+
),
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
if (!task.guarantees) {
|
|
2649
|
+
diagnosticos.push(
|
|
2650
|
+
criarDiagnostico(
|
|
2651
|
+
"SEM005",
|
|
2652
|
+
`Task "${task.nome}" precisa declarar guarantees.`,
|
|
2653
|
+
"erro",
|
|
2654
|
+
task.intervalo,
|
|
2655
|
+
"A proposta da Sema e falhar cedo quando a pos-condicao nao esta explicita.",
|
|
2656
|
+
),
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
if (task.input) {
|
|
2661
|
+
validarCamposDeTipos(task.input.campos, tiposConhecidos, diagnosticos, `input da task ${task.nome}`);
|
|
2662
|
+
}
|
|
2663
|
+
if (task.output) {
|
|
2664
|
+
validarCamposDeTipos(task.output.campos, tiposConhecidos, diagnosticos, `output da task ${task.nome}`);
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
const entradasConhecidas = new Set(task.input?.campos.map((campo) => campo.nome) ?? []);
|
|
2668
|
+
const saidasConhecidas = new Set(task.output?.campos.map((campo) => campo.nome) ?? []);
|
|
2669
|
+
|
|
2670
|
+
if (task.state) {
|
|
2671
|
+
for (const campo of task.output?.campos ?? []) {
|
|
2672
|
+
if (!campoStatusTexto(campo)) {
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
diagnosticos.push(
|
|
2676
|
+
criarDiagnostico(
|
|
2677
|
+
"SEM100",
|
|
2678
|
+
`Task "${task.nome}" declara output "${campo.nome}: Texto" enquanto governa state.`,
|
|
2679
|
+
"aviso",
|
|
2680
|
+
campo.intervalo,
|
|
2681
|
+
"Use um enum/status de dominio no output para manter a transicao rastreavel para IA.",
|
|
2682
|
+
),
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
if (task.rules) {
|
|
2688
|
+
validarExpressoesDeclaradas(task.rules.linhas, diagnosticos, {
|
|
2689
|
+
codigoErroSintaxe: "SEM021",
|
|
2690
|
+
codigoErroReferencia: "SEM022",
|
|
2691
|
+
nomeBloco: `rules da task ${task.nome}`,
|
|
2692
|
+
simbolosPermitidos: entradasConhecidas,
|
|
2693
|
+
dicaSintaxe: "Use expressoes como \"campo existe\", \"campo > 0\", \"campo em [A, B]\" ou \"campo deve_ser predicado\".",
|
|
2694
|
+
dicaReferencia: "No MVP atual, rules devem referenciar apenas campos do input.",
|
|
2695
|
+
dicaReferenciaPersonalizada: (raiz) => (
|
|
2696
|
+
saidasConhecidas.has(raiz)
|
|
2697
|
+
? `\"${raiz}\" parece vir do output. Rules devem validar entrada; se a intencao era afirmar pos-condicao, mova isso para guarantees.`
|
|
2698
|
+
: undefined
|
|
2699
|
+
),
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
if (task.effects) {
|
|
2704
|
+
validarEfeitosDeclarados(task.effects.linhas, diagnosticos, `effects da task ${task.nome}`);
|
|
2705
|
+
}
|
|
2706
|
+
validarVinculos(task.vinculos, diagnosticos, `task ${task.nome}`);
|
|
2707
|
+
validarExecucao(task, diagnosticos);
|
|
2708
|
+
|
|
2709
|
+
const perfilSeguranca = coletarPerfilSegurancaDeclarado(
|
|
2710
|
+
task.corpo,
|
|
2711
|
+
task.effects,
|
|
2712
|
+
diagnosticos,
|
|
2713
|
+
`task "${task.nome}"`,
|
|
2714
|
+
);
|
|
2715
|
+
emitirGuardrailsSeguranca(
|
|
2716
|
+
`Task "${task.nome}"`,
|
|
2717
|
+
task.intervalo,
|
|
2718
|
+
perfilSeguranca,
|
|
2719
|
+
diagnosticos,
|
|
2720
|
+
{
|
|
2721
|
+
publico: false,
|
|
2722
|
+
sensivel: taskEhSensivel(task) || perfilSeguranca.efeitoPrivilegiado || perfilSeguranca.dadosSensiveis || perfilSeguranca.exigeSegredos,
|
|
2723
|
+
},
|
|
2724
|
+
);
|
|
2725
|
+
|
|
2726
|
+
validarImplementacoesTask(task, diagnosticos);
|
|
2727
|
+
|
|
2728
|
+
if (task.tests) {
|
|
2729
|
+
for (const bloco of task.tests.blocos) {
|
|
2730
|
+
if (bloco.tipo !== "caso_teste") {
|
|
2731
|
+
continue;
|
|
2732
|
+
}
|
|
2733
|
+
validarCasoTeste(task, bloco, diagnosticos);
|
|
2734
|
+
validarTesteSemanticoForte(task, bloco, diagnosticos);
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
if (task.guarantees && task.output) {
|
|
2739
|
+
validarExpressoesDeclaradas(task.guarantees.linhas, diagnosticos, {
|
|
2740
|
+
codigoErroSintaxe: "SEM030",
|
|
2741
|
+
codigoErroReferencia: "SEM031",
|
|
2742
|
+
nomeBloco: `guarantees da task ${task.nome}`,
|
|
2743
|
+
simbolosPermitidos: saidasConhecidas,
|
|
2744
|
+
dicaSintaxe: "Use expressoes como \"saida existe\", \"saida == valor\" ou \"saida em [A, B]\" nas guarantees.",
|
|
2745
|
+
dicaReferencia: "No MVP atual, guarantees devem referenciar campos do output ou marcadores semanticos permitidos.",
|
|
2746
|
+
aceitarMarcadoresSemanticos: true,
|
|
2747
|
+
dicaReferenciaPersonalizada: (raiz) => (
|
|
2748
|
+
entradasConhecidas.has(raiz)
|
|
2749
|
+
? `\"${raiz}\" parece vir do input. Guarantees devem afirmar output, estado ou marcadores semanticos; se a intencao era validar entrada, mova isso para rules.`
|
|
2750
|
+
: undefined
|
|
2751
|
+
),
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
validarErroOperacional(task, diagnosticos);
|
|
2756
|
+
|
|
2757
|
+
const blocoInternoTests = localizarBloco(task.corpo, "tests");
|
|
2758
|
+
if (blocoInternoTests && blocoInternoTests.blocos.length === 0) {
|
|
2759
|
+
diagnosticos.push(
|
|
2760
|
+
criarDiagnostico(
|
|
2761
|
+
"SEM007",
|
|
2762
|
+
`Task "${task.nome}" declarou tests sem casos.`,
|
|
2763
|
+
"erro",
|
|
2764
|
+
blocoInternoTests.intervalo,
|
|
2765
|
+
"Adicione ao menos um bloco caso dentro de tests.",
|
|
2766
|
+
),
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
validarVinculoEstadoDaTask(task, statesConhecidos, diagnosticos);
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
export function validarTesteSemanticoForte(task: TaskAst, caso: BlocoCasoTesteAst, diagnosticos: Diagnostico[]): void {
|
|
2774
|
+
if (!taskEhSensivel(task) || !caso.expect) {
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
const possuiErroEsperado = Boolean(
|
|
2779
|
+
caso.error
|
|
2780
|
+
&& (caso.error.campos.length > 0 || caso.error.linhas.length > 0 || caso.error.blocos.length > 0),
|
|
2781
|
+
);
|
|
2782
|
+
if (possuiErroEsperado) {
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
const camposExpect = caso.expect.campos.filter((campo) => campo.nome !== "");
|
|
2787
|
+
const somenteSucesso = camposExpect.length === 1
|
|
2788
|
+
&& camposExpect[0]?.nome === "sucesso"
|
|
2789
|
+
&& caso.expect.linhas.length === 0
|
|
2790
|
+
&& caso.expect.blocos.length === 0;
|
|
2791
|
+
|
|
2792
|
+
if (!somenteSucesso) {
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
diagnosticos.push(
|
|
2797
|
+
criarDiagnostico(
|
|
2798
|
+
"SEM102",
|
|
2799
|
+
`Caso de teste "${caso.nome}" da task sensivel "${task.nome}" valida apenas sucesso.`,
|
|
2800
|
+
"aviso",
|
|
2801
|
+
caso.expect.intervalo,
|
|
2802
|
+
"Inclua algum output, erro esperado ou garantia observavel para a IA nao tratar 'sucesso' como contrato completo.",
|
|
2803
|
+
),
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
export function emitirDiagnosticosContratoFrouxo(task: TaskAst, diagnosticos: Diagnostico[]): void {
|
|
2808
|
+
for (const bloco of task.tests?.blocos ?? []) {
|
|
2809
|
+
if (bloco.tipo === "caso_teste") {
|
|
2810
|
+
validarTesteSemanticoForte(task, bloco, diagnosticos);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
function validarCasoTeste(task: TaskAst, caso: BlocoCasoTesteAst, diagnosticos: Diagnostico[]): void {
|
|
2816
|
+
if (!caso.given) {
|
|
2817
|
+
diagnosticos.push(
|
|
2818
|
+
criarDiagnostico(
|
|
2819
|
+
"SEM008",
|
|
2820
|
+
`Caso de teste "${caso.nome}" da task "${task.nome}" precisa declarar given.`,
|
|
2821
|
+
"erro",
|
|
2822
|
+
caso.intervalo,
|
|
2823
|
+
),
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
if (!caso.expect) {
|
|
2827
|
+
diagnosticos.push(
|
|
2828
|
+
criarDiagnostico(
|
|
2829
|
+
"SEM009",
|
|
2830
|
+
`Caso de teste "${caso.nome}" da task "${task.nome}" precisa declarar expect.`,
|
|
2831
|
+
"erro",
|
|
2832
|
+
caso.intervalo,
|
|
2833
|
+
),
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
export function analisarSemantica(modulo: ModuloAst, opcoes: OpcoesAnaliseSemantica = {}): ResultadoSemantico {
|
|
2839
|
+
const diagnosticos: Diagnostico[] = [];
|
|
2840
|
+
const simbolos = new Map<string, SimboloSemantico>();
|
|
2841
|
+
const tiposConhecidos = new Set(TIPOS_PRIMITIVOS);
|
|
2842
|
+
const tasksConhecidas = new Set<string>();
|
|
2843
|
+
const tarefasDetalhadas = new Map<string, ResumoTaskSemantico>();
|
|
2844
|
+
const statesConhecidos = new Map<string, { transicoes: Set<string> }>();
|
|
2845
|
+
const modulosImportados: string[] = [];
|
|
2846
|
+
const interoperabilidades: InteropSemantico[] = [];
|
|
2847
|
+
const enumsConhecidos = new Map<string, Set<string>>();
|
|
2848
|
+
|
|
2849
|
+
for (const use of modulo.uses) {
|
|
2850
|
+
if (use.origem !== "sema") {
|
|
2851
|
+
if (!PADRAO_CAMINHO_INTEROP.test(use.caminho)) {
|
|
2852
|
+
diagnosticos.push(
|
|
2853
|
+
criarDiagnostico(
|
|
2854
|
+
"SEM058",
|
|
2855
|
+
`Interop externa "${use.origem} ${use.caminho}" e invalida no modulo "${modulo.nome}".`,
|
|
2856
|
+
"erro",
|
|
2857
|
+
use.intervalo,
|
|
2858
|
+
"Use um identificador de modulo externo como pacote.servico, app.modulo ou dominio.executor.",
|
|
2859
|
+
),
|
|
2860
|
+
);
|
|
2861
|
+
continue;
|
|
2862
|
+
}
|
|
2863
|
+
interoperabilidades.push({ origem: use.origem, caminho: use.caminho });
|
|
2864
|
+
continue;
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
const resolucaoUse = resolverUseSema(modulo.nome, use.caminho, opcoes.contextosModulos);
|
|
2868
|
+
const contextoImportado = resolucaoUse.caminhoResolvido
|
|
2869
|
+
? opcoes.contextosModulos?.get(resolucaoUse.caminhoResolvido)
|
|
2870
|
+
: undefined;
|
|
2871
|
+
if (!contextoImportado) {
|
|
2872
|
+
const candidatosEncontrados = resolucaoUse.candidatosRelativos
|
|
2873
|
+
.filter((candidato) => opcoes.contextosModulos?.has(candidato))
|
|
2874
|
+
.slice(0, 4);
|
|
2875
|
+
const sugestoesModulos = descreverSugestoes(opcoes.contextosModulos?.keys() ?? [], "Modulos disponiveis neste contexto");
|
|
2876
|
+
diagnosticos.push(
|
|
2877
|
+
criarDiagnostico(
|
|
2878
|
+
"SEM019",
|
|
2879
|
+
`Modulo "${modulo.nome}" usa "${use.caminho}", mas esse modulo nao foi encontrado no projeto atual.`,
|
|
2880
|
+
"erro",
|
|
2881
|
+
use.intervalo,
|
|
2882
|
+
candidatosEncontrados.length > 0
|
|
2883
|
+
? `Se a intencao era um import relativo ao namespace atual, tente um caminho como ${candidatosEncontrados.join(", ")}.`
|
|
2884
|
+
: (sugestoesModulos ?? "Garanta que o arquivo .sema importado esteja presente no mesmo conjunto de compilacao."),
|
|
2885
|
+
),
|
|
2886
|
+
);
|
|
2887
|
+
continue;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
modulosImportados.push(resolucaoUse.caminhoResolvido ?? use.caminho);
|
|
2891
|
+
for (const tipo of contextoImportado.tiposConhecidos) {
|
|
2892
|
+
tiposConhecidos.add(tipo);
|
|
2893
|
+
}
|
|
2894
|
+
for (const task of contextoImportado.tasksConhecidas) {
|
|
2895
|
+
tasksConhecidas.add(task);
|
|
2896
|
+
}
|
|
2897
|
+
for (const [nomeTask, detalhesTask] of contextoImportado.tarefasDetalhadas) {
|
|
2898
|
+
tarefasDetalhadas.set(nomeTask, {
|
|
2899
|
+
input: detalhesTask.input.map((campo) => ({ ...campo, modificadores: [...campo.modificadores] })),
|
|
2900
|
+
output: detalhesTask.output.map((campo) => ({ ...campo, modificadores: [...campo.modificadores] })),
|
|
2901
|
+
errors: detalhesTask.errors.map((erro) => ({ ...erro })),
|
|
2902
|
+
guarantees: [...detalhesTask.guarantees],
|
|
2903
|
+
implementacoes: detalhesTask.implementacoes.map((impl) => ({ ...impl })),
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
for (const [nomeState, metadadosState] of contextoImportado.statesConhecidos) {
|
|
2907
|
+
statesConhecidos.set(nomeState, { transicoes: new Set(metadadosState.transicoes) });
|
|
2908
|
+
}
|
|
2909
|
+
for (const [nomeEnum, valores] of contextoImportado.enumsConhecidos) {
|
|
2910
|
+
enumsConhecidos.set(nomeEnum, new Set(valores));
|
|
2911
|
+
}
|
|
2912
|
+
for (const interop of contextoImportado.interoperabilidades) {
|
|
2913
|
+
interoperabilidades.push({ ...interop });
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
const registrar = (
|
|
2918
|
+
nome: string,
|
|
2919
|
+
categoria: SimboloSemantico["categoria"],
|
|
2920
|
+
intervalo?: TypeAst["intervalo"] | EntityAst["intervalo"] | EnumAst["intervalo"],
|
|
2921
|
+
): void => {
|
|
2922
|
+
if (simbolos.has(nome)) {
|
|
2923
|
+
diagnosticos.push(diagnosticoDuplicado(nome, categoria, intervalo));
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
simbolos.set(nome, { nome, categoria });
|
|
2927
|
+
if (categoria === "task") {
|
|
2928
|
+
tasksConhecidas.add(nome);
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
if (categoria !== "database") {
|
|
2932
|
+
tiposConhecidos.add(nome);
|
|
2933
|
+
}
|
|
2934
|
+
};
|
|
2935
|
+
|
|
2936
|
+
for (const type of modulo.types) {
|
|
2937
|
+
registrar(type.nome, "tipo", type.intervalo);
|
|
2938
|
+
}
|
|
2939
|
+
for (const entity of modulo.entities) {
|
|
2940
|
+
registrar(entity.nome, "entity", entity.intervalo);
|
|
2941
|
+
}
|
|
2942
|
+
for (const enumeracao of modulo.enums) {
|
|
2943
|
+
registrar(enumeracao.nome, "enum", enumeracao.intervalo);
|
|
2944
|
+
enumsConhecidos.set(enumeracao.nome, new Set(enumeracao.valores));
|
|
2945
|
+
}
|
|
2946
|
+
for (const task of modulo.tasks) {
|
|
2947
|
+
registrar(task.nome, "task", task.intervalo);
|
|
2948
|
+
tarefasDetalhadas.set(task.nome, coletarResumoTask(task));
|
|
2949
|
+
}
|
|
2950
|
+
for (const flow of modulo.flows) {
|
|
2951
|
+
registrar(flow.nome, "flow", flow.intervalo);
|
|
2952
|
+
}
|
|
2953
|
+
for (const route of modulo.routes) {
|
|
2954
|
+
registrar(route.nome, "route", route.intervalo);
|
|
2955
|
+
}
|
|
2956
|
+
for (const worker of modulo.workers) {
|
|
2957
|
+
if (worker.nome) {
|
|
2958
|
+
registrar(worker.nome, "worker", worker.intervalo);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
for (const evento of modulo.eventos) {
|
|
2962
|
+
if (evento.nome) {
|
|
2963
|
+
registrar(evento.nome, "evento", evento.intervalo);
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
for (const fila of modulo.filas) {
|
|
2967
|
+
if (fila.nome) {
|
|
2968
|
+
registrar(fila.nome, "fila", fila.intervalo);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
for (const cron of modulo.crons) {
|
|
2972
|
+
if (cron.nome) {
|
|
2973
|
+
registrar(cron.nome, "cron", cron.intervalo);
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
for (const webhook of modulo.webhooks) {
|
|
2977
|
+
if (webhook.nome) {
|
|
2978
|
+
registrar(webhook.nome, "webhook", webhook.intervalo);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
for (const cache of modulo.caches) {
|
|
2982
|
+
if (cache.nome) {
|
|
2983
|
+
registrar(cache.nome, "cache", cache.intervalo);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
for (const storage of modulo.storages) {
|
|
2987
|
+
if (storage.nome) {
|
|
2988
|
+
registrar(storage.nome, "storage", storage.intervalo);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
for (const policy of modulo.policies) {
|
|
2992
|
+
if (policy.nome) {
|
|
2993
|
+
registrar(policy.nome, "policy", policy.intervalo);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
for (const database of modulo.databases) {
|
|
2997
|
+
if (database.nome) {
|
|
2998
|
+
registrar(database.nome, "database", database.intervalo);
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
for (const state of modulo.states) {
|
|
3002
|
+
if (state.nome) {
|
|
3003
|
+
registrar(state.nome, "state", state.intervalo);
|
|
3004
|
+
const transicoes = new Set(
|
|
3005
|
+
(localizarBloco(state.corpo, "transitions")?.linhas ?? [])
|
|
3006
|
+
.map((linha) => parsearTransicaoEstado(linha.conteudo))
|
|
3007
|
+
.filter((linha): linha is NonNullable<typeof linha> => Boolean(linha))
|
|
3008
|
+
.map((linha) => serializarTransicao(linha.origem, linha.destino)),
|
|
3009
|
+
);
|
|
3010
|
+
statesConhecidos.set(state.nome, { transicoes });
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
for (const type of modulo.types) {
|
|
3015
|
+
validarCamposDeTipos(type.corpo.campos, tiposConhecidos, diagnosticos, `type ${type.nome}`);
|
|
3016
|
+
const fields = localizarBloco(type.corpo, "fields");
|
|
3017
|
+
if (fields) {
|
|
3018
|
+
validarCamposDeTipos(fields.campos, tiposConhecidos, diagnosticos, `fields do type ${type.nome}`);
|
|
3019
|
+
}
|
|
3020
|
+
validarInvariantesDeCampos(type.corpo, `type ${type.nome}`, diagnosticos);
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
for (const entity of modulo.entities) {
|
|
3024
|
+
const fields = localizarBloco(entity.corpo, "fields");
|
|
3025
|
+
if (!fields || fields.campos.length === 0) {
|
|
3026
|
+
diagnosticos.push(
|
|
3027
|
+
criarDiagnostico(
|
|
3028
|
+
"SEM010",
|
|
3029
|
+
`Entity "${entity.nome}" precisa declarar fields.`,
|
|
3030
|
+
"erro",
|
|
3031
|
+
entity.intervalo,
|
|
3032
|
+
"Adicione um bloco fields com os campos da entidade.",
|
|
3033
|
+
),
|
|
3034
|
+
);
|
|
3035
|
+
} else {
|
|
3036
|
+
validarCamposDeTipos(fields.campos, tiposConhecidos, diagnosticos, `entity ${entity.nome}`);
|
|
3037
|
+
}
|
|
3038
|
+
validarInvariantesDeCampos(entity.corpo, `entity ${entity.nome}`, diagnosticos);
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
for (const task of modulo.tasks) {
|
|
3042
|
+
validarTask(task, tiposConhecidos, statesConhecidos, diagnosticos);
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
for (const flow of modulo.flows) {
|
|
3046
|
+
validarFlow(flow, tasksConhecidas, tarefasDetalhadas, diagnosticos);
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
for (const route of modulo.routes) {
|
|
3050
|
+
validarRoute(route, tasksConhecidas, tarefasDetalhadas, diagnosticos);
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
validarVinculos(modulo.vinculos, diagnosticos, `modulo ${modulo.nome}`);
|
|
3054
|
+
for (const worker of modulo.workers) {
|
|
3055
|
+
validarSuperficie(worker, "worker", tasksConhecidas, tiposConhecidos, diagnosticos);
|
|
3056
|
+
}
|
|
3057
|
+
for (const evento of modulo.eventos) {
|
|
3058
|
+
validarSuperficie(evento, "evento", tasksConhecidas, tiposConhecidos, diagnosticos);
|
|
3059
|
+
}
|
|
3060
|
+
for (const fila of modulo.filas) {
|
|
3061
|
+
validarSuperficie(fila, "fila", tasksConhecidas, tiposConhecidos, diagnosticos);
|
|
3062
|
+
}
|
|
3063
|
+
for (const cron of modulo.crons) {
|
|
3064
|
+
validarSuperficie(cron, "cron", tasksConhecidas, tiposConhecidos, diagnosticos);
|
|
3065
|
+
}
|
|
3066
|
+
for (const webhook of modulo.webhooks) {
|
|
3067
|
+
validarSuperficie(webhook, "webhook", tasksConhecidas, tiposConhecidos, diagnosticos);
|
|
3068
|
+
}
|
|
3069
|
+
for (const cache of modulo.caches) {
|
|
3070
|
+
validarSuperficie(cache, "cache", tasksConhecidas, tiposConhecidos, diagnosticos);
|
|
3071
|
+
}
|
|
3072
|
+
for (const storage of modulo.storages) {
|
|
3073
|
+
validarSuperficie(storage, "storage", tasksConhecidas, tiposConhecidos, diagnosticos);
|
|
3074
|
+
}
|
|
3075
|
+
for (const policy of modulo.policies) {
|
|
3076
|
+
validarSuperficie(policy, "policy", tasksConhecidas, tiposConhecidos, diagnosticos);
|
|
3077
|
+
}
|
|
3078
|
+
for (const database of modulo.databases) {
|
|
3079
|
+
validarDatabase(database, diagnosticos);
|
|
3080
|
+
}
|
|
3081
|
+
validarGuardrailsSeguranca(modulo, diagnosticos);
|
|
3082
|
+
|
|
3083
|
+
const assinaturasRoute = new Map<string, RouteAst>();
|
|
3084
|
+
for (const route of modulo.routes) {
|
|
3085
|
+
const metodo = (localizarCampo(route.corpo, "metodo")?.valor ?? "").toUpperCase();
|
|
3086
|
+
const caminho = recomporCaminhoRoute(localizarCampo(route.corpo, "caminho")) ?? "";
|
|
3087
|
+
if (!metodo || !caminho) {
|
|
3088
|
+
continue;
|
|
3089
|
+
}
|
|
3090
|
+
const chave = `${metodo} ${caminho}`;
|
|
3091
|
+
const existente = assinaturasRoute.get(chave);
|
|
3092
|
+
if (existente) {
|
|
3093
|
+
diagnosticos.push(
|
|
3094
|
+
criarDiagnostico(
|
|
3095
|
+
"SEM055",
|
|
3096
|
+
`Route "${route.nome}" reutiliza a assinatura publica "${chave}", ja declarada por "${existente.nome}".`,
|
|
3097
|
+
"erro",
|
|
3098
|
+
route.intervalo,
|
|
3099
|
+
"Cada combinacao de metodo e caminho deve ser unica no mesmo modulo.",
|
|
3100
|
+
),
|
|
3101
|
+
);
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
assinaturasRoute.set(chave, route);
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
for (const state of modulo.states) {
|
|
3108
|
+
validarState(state, tiposConhecidos, enumsConhecidos, diagnosticos);
|
|
3109
|
+
}
|
|
3110
|
+
validarStatusTextoComState(modulo, diagnosticos);
|
|
3111
|
+
|
|
3112
|
+
return {
|
|
3113
|
+
contexto: {
|
|
3114
|
+
modulo: modulo.nome,
|
|
3115
|
+
simbolos,
|
|
3116
|
+
tiposConhecidos,
|
|
3117
|
+
tasksConhecidas,
|
|
3118
|
+
tarefasDetalhadas,
|
|
3119
|
+
statesConhecidos,
|
|
3120
|
+
modulosImportados,
|
|
3121
|
+
interoperabilidades,
|
|
3122
|
+
enumsConhecidos,
|
|
3123
|
+
},
|
|
3124
|
+
diagnosticos,
|
|
3125
|
+
};
|
|
3126
|
+
}
|