@semacode/cli 1.5.14 → 1.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +76 -43
  3. package/package.json +26 -35
  4. package/semacode-cli-1.3.1.tgz +0 -0
  5. package/src/angular-consumer-standalone.ts +312 -0
  6. package/src/cpp-symbols.ts +82 -0
  7. package/src/docs.ts +535 -0
  8. package/src/dotnet-http.ts +355 -0
  9. package/src/drift.ts +4933 -0
  10. package/src/go-http.ts +118 -0
  11. package/src/importador.ts +3891 -0
  12. package/src/index.ts +5641 -0
  13. package/src/java-http.ts +247 -0
  14. package/src/lua-symbols.ts +114 -0
  15. package/src/php-symbols.ts +462 -0
  16. package/src/projeto.ts +862 -0
  17. package/src/python-http.ts +258 -0
  18. package/src/rust-http.ts +125 -0
  19. package/src/tipos.ts +24 -0
  20. package/src/typescript-http.ts +1076 -0
  21. package/tsconfig.json +20 -0
  22. package/AGENTS.md +0 -272
  23. package/LICENSE +0 -22
  24. package/SEMA_BRIEF.curto.txt +0 -9
  25. package/SEMA_BRIEF.md +0 -63
  26. package/SEMA_BRIEF.micro.txt +0 -7
  27. package/SEMA_INDEX.json +0 -799
  28. package/dist/angular-consumer-standalone.d.ts +0 -6
  29. package/dist/angular-consumer-standalone.js.map +0 -1
  30. package/dist/cpp-symbols.d.ts +0 -10
  31. package/dist/cpp-symbols.js.map +0 -1
  32. package/dist/docs.d.ts +0 -56
  33. package/dist/docs.js.map +0 -1
  34. package/dist/dotnet-http.d.ts +0 -23
  35. package/dist/dotnet-http.js.map +0 -1
  36. package/dist/drift.d.ts +0 -225
  37. package/dist/drift.js.map +0 -1
  38. package/dist/go-http.d.ts +0 -23
  39. package/dist/go-http.js.map +0 -1
  40. package/dist/importador.d.ts +0 -31
  41. package/dist/importador.js.map +0 -1
  42. package/dist/index.d.ts +0 -2
  43. package/dist/index.js.map +0 -1
  44. package/dist/java-http.d.ts +0 -23
  45. package/dist/java-http.js.map +0 -1
  46. package/dist/lua-symbols.d.ts +0 -10
  47. package/dist/lua-symbols.js.map +0 -1
  48. package/dist/php-symbols.d.ts +0 -24
  49. package/dist/php-symbols.js.map +0 -1
  50. package/dist/projeto.d.ts +0 -53
  51. package/dist/projeto.js.map +0 -1
  52. package/dist/python-http.d.ts +0 -23
  53. package/dist/python-http.js.map +0 -1
  54. package/dist/rust-http.d.ts +0 -23
  55. package/dist/rust-http.js.map +0 -1
  56. package/dist/tipos.d.ts +0 -3
  57. package/dist/tipos.js.map +0 -1
  58. package/dist/typescript-http.d.ts +0 -35
  59. package/dist/typescript-http.js.map +0 -1
  60. package/docs/AGENT_STARTER.md +0 -102
  61. package/docs/cli.md +0 -117
  62. package/docs/como-ensinar-a-sema-para-ia.md +0 -149
  63. package/docs/deploy.md +0 -70
  64. package/docs/documentacao.md +0 -63
  65. package/docs/env.md +0 -56
  66. package/docs/extensao-vscode.md +0 -45
  67. package/docs/fluxo-pratico-ia-sema.md +0 -177
  68. package/docs/instalacao-e-primeiro-uso.md +0 -112
  69. package/docs/integracao-com-ia.md +0 -101
  70. package/docs/mcp.md +0 -53
  71. package/docs/pagamento-ponta-a-ponta.md +0 -155
  72. package/docs/persistencia-vendor-first.md +0 -145
  73. package/docs/prompt-base-ia-sema.md +0 -104
  74. package/docs/rollback.md +0 -47
  75. package/docs/sintaxe.md +0 -410
  76. package/exemplos/agendamento.sema +0 -106
  77. package/exemplos/assinatura.sema +0 -136
  78. package/exemplos/auditoria.sema +0 -88
  79. package/exemplos/autenticacao.sema +0 -125
  80. package/exemplos/automacao.sema +0 -107
  81. package/exemplos/cadastro_usuario.sema +0 -54
  82. package/exemplos/calculadora.sema +0 -78
  83. package/exemplos/crud_simples.sema +0 -89
  84. package/exemplos/estoque.sema +0 -126
  85. package/exemplos/exportacao.sema +0 -94
  86. package/exemplos/fila.sema +0 -131
  87. package/exemplos/integracao_externa.sema +0 -94
  88. package/exemplos/multi_tenant.sema +0 -140
  89. package/exemplos/notificacao.sema +0 -98
  90. package/exemplos/operacao_estrategia.sema +0 -402
  91. package/exemplos/pagamento.sema +0 -222
  92. package/exemplos/pagamento_dominio.sema +0 -35
  93. package/exemplos/pedido.sema +0 -119
  94. package/exemplos/permissao.sema +0 -121
  95. package/exemplos/persistencia_vendor_first.sema +0 -86
  96. package/exemplos/relatorio.sema +0 -93
  97. package/exemplos/testes_embutidos.sema +0 -45
  98. package/exemplos/tratamento_erro.sema +0 -157
  99. package/exemplos/upload_arquivo.sema +0 -93
  100. package/exemplos/webhook.sema +0 -96
  101. package/llms-full.txt +0 -34
  102. package/llms.txt +0 -17
  103. package/node_modules/@sema/gerador-css/dist/index.d.ts +0 -3
  104. package/node_modules/@sema/gerador-css/dist/index.js +0 -592
  105. package/node_modules/@sema/gerador-css/dist/index.js.map +0 -1
  106. package/node_modules/@sema/gerador-css/package.json +0 -7
  107. package/node_modules/@sema/gerador-dart/dist/index.d.ts +0 -3
  108. package/node_modules/@sema/gerador-dart/dist/index.js +0 -44
  109. package/node_modules/@sema/gerador-dart/dist/index.js.map +0 -1
  110. package/node_modules/@sema/gerador-dart/package.json +0 -7
  111. package/node_modules/@sema/gerador-html/dist/index.d.ts +0 -3
  112. package/node_modules/@sema/gerador-html/dist/index.js +0 -163
  113. package/node_modules/@sema/gerador-html/dist/index.js.map +0 -1
  114. package/node_modules/@sema/gerador-html/package.json +0 -7
  115. package/node_modules/@sema/gerador-javascript/dist/index.d.ts +0 -3
  116. package/node_modules/@sema/gerador-javascript/dist/index.js +0 -421
  117. package/node_modules/@sema/gerador-javascript/dist/index.js.map +0 -1
  118. package/node_modules/@sema/gerador-javascript/package.json +0 -7
  119. package/node_modules/@sema/gerador-lua/dist/index.d.ts +0 -3
  120. package/node_modules/@sema/gerador-lua/dist/index.js +0 -328
  121. package/node_modules/@sema/gerador-lua/dist/index.js.map +0 -1
  122. package/node_modules/@sema/gerador-lua/package.json +0 -7
  123. package/node_modules/@sema/gerador-python/dist/index.d.ts +0 -6
  124. package/node_modules/@sema/gerador-python/dist/index.js +0 -729
  125. package/node_modules/@sema/gerador-python/dist/index.js.map +0 -1
  126. package/node_modules/@sema/gerador-python/package.json +0 -7
  127. package/node_modules/@sema/gerador-typescript/dist/index.d.ts +0 -6
  128. package/node_modules/@sema/gerador-typescript/dist/index.js +0 -793
  129. package/node_modules/@sema/gerador-typescript/dist/index.js.map +0 -1
  130. package/node_modules/@sema/gerador-typescript/package.json +0 -7
  131. package/node_modules/@sema/nucleo/dist/ast/tipos.d.ts +0 -125
  132. package/node_modules/@sema/nucleo/dist/ast/tipos.js +0 -2
  133. package/node_modules/@sema/nucleo/dist/ast/tipos.js.map +0 -1
  134. package/node_modules/@sema/nucleo/dist/diagnosticos/index.d.ts +0 -21
  135. package/node_modules/@sema/nucleo/dist/diagnosticos/index.js +0 -12
  136. package/node_modules/@sema/nucleo/dist/diagnosticos/index.js.map +0 -1
  137. package/node_modules/@sema/nucleo/dist/formatador/index.d.ts +0 -9
  138. package/node_modules/@sema/nucleo/dist/formatador/index.js +0 -487
  139. package/node_modules/@sema/nucleo/dist/formatador/index.js.map +0 -1
  140. package/node_modules/@sema/nucleo/dist/index.d.ts +0 -35
  141. package/node_modules/@sema/nucleo/dist/index.js +0 -96
  142. package/node_modules/@sema/nucleo/dist/index.js.map +0 -1
  143. package/node_modules/@sema/nucleo/dist/ir/conversor.d.ts +0 -5
  144. package/node_modules/@sema/nucleo/dist/ir/conversor.js +0 -1058
  145. package/node_modules/@sema/nucleo/dist/ir/conversor.js.map +0 -1
  146. package/node_modules/@sema/nucleo/dist/ir/modelos.d.ts +0 -377
  147. package/node_modules/@sema/nucleo/dist/ir/modelos.js +0 -2
  148. package/node_modules/@sema/nucleo/dist/ir/modelos.js.map +0 -1
  149. package/node_modules/@sema/nucleo/dist/lexer/lexer.d.ts +0 -7
  150. package/node_modules/@sema/nucleo/dist/lexer/lexer.js +0 -122
  151. package/node_modules/@sema/nucleo/dist/lexer/lexer.js.map +0 -1
  152. package/node_modules/@sema/nucleo/dist/lexer/tokens.d.ts +0 -8
  153. package/node_modules/@sema/nucleo/dist/lexer/tokens.js +0 -82
  154. package/node_modules/@sema/nucleo/dist/lexer/tokens.js.map +0 -1
  155. package/node_modules/@sema/nucleo/dist/parser/parser.d.ts +0 -9
  156. package/node_modules/@sema/nucleo/dist/parser/parser.js +0 -807
  157. package/node_modules/@sema/nucleo/dist/parser/parser.js.map +0 -1
  158. package/node_modules/@sema/nucleo/dist/persistencia/contratos.d.ts +0 -39
  159. package/node_modules/@sema/nucleo/dist/persistencia/contratos.js +0 -294
  160. package/node_modules/@sema/nucleo/dist/persistencia/contratos.js.map +0 -1
  161. package/node_modules/@sema/nucleo/dist/semantico/analisador.d.ts +0 -58
  162. package/node_modules/@sema/nucleo/dist/semantico/analisador.js +0 -1912
  163. package/node_modules/@sema/nucleo/dist/semantico/analisador.js.map +0 -1
  164. package/node_modules/@sema/nucleo/dist/semantico/estruturas.d.ts +0 -104
  165. package/node_modules/@sema/nucleo/dist/semantico/estruturas.js +0 -445
  166. package/node_modules/@sema/nucleo/dist/semantico/estruturas.js.map +0 -1
  167. package/node_modules/@sema/nucleo/dist/semantico/seguranca.d.ts +0 -91
  168. package/node_modules/@sema/nucleo/dist/semantico/seguranca.js +0 -258
  169. package/node_modules/@sema/nucleo/dist/semantico/seguranca.js.map +0 -1
  170. package/node_modules/@sema/nucleo/dist/util/arquivos.d.ts +0 -2
  171. package/node_modules/@sema/nucleo/dist/util/arquivos.js +0 -25
  172. package/node_modules/@sema/nucleo/dist/util/arquivos.js.map +0 -1
  173. package/node_modules/@sema/nucleo/package.json +0 -7
  174. package/node_modules/@sema/padroes/dist/index.d.ts +0 -25
  175. package/node_modules/@sema/padroes/dist/index.js +0 -316
  176. package/node_modules/@sema/padroes/dist/index.js.map +0 -1
  177. package/node_modules/@sema/padroes/package.json +0 -7
package/src/drift.ts ADDED
@@ -0,0 +1,4933 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import ts from "typescript";
4
+ import type {
5
+ EngineBanco,
6
+ IrBancoDados,
7
+ IrFlow,
8
+ IrModulo,
9
+ IrRecursoPersistencia,
10
+ IrRoute,
11
+ IrSuperficie,
12
+ IrTask,
13
+ IrVinculo,
14
+ NivelConfiancaSemantica,
15
+ NivelRiscoSemantico,
16
+ TipoRecursoPersistencia,
17
+ } from "@sema/nucleo";
18
+ import type { ContextoProjetoCarregado } from "./projeto.js";
19
+ import type { FonteLegado } from "./tipos.js";
20
+ import { coletarSuperficiesAngularStandaloneConsumer } from "./angular-consumer-standalone.js";
21
+ import { extrairSimbolosCpp } from "./cpp-symbols.js";
22
+ import { extrairRotasDotnet, extrairSimbolosDotnet } from "./dotnet-http.js";
23
+ import { extrairRotasGo, extrairSimbolosGo } from "./go-http.js";
24
+ import { extrairRotasJava, extrairSimbolosJava } from "./java-http.js";
25
+ import { extrairSimbolosLua } from "./lua-symbols.js";
26
+ import { extrairRotasPhp, extrairSimbolosPhp } from "./php-symbols.js";
27
+ import { contarIndentacaoPython, extrairRotasFlaskDecoradas, normalizarCaminhoFlask } from "./python-http.js";
28
+ import { extrairRotasRust, extrairSimbolosRust } from "./rust-http.js";
29
+ import { extrairRotasTypeScriptHttp } from "./typescript-http.js";
30
+
31
+ interface SimboloResolvido {
32
+ origem: "ts" | "py" | "dart" | "lua" | "php" | "cs" | "java" | "go" | "rust" | "cpp";
33
+ caminho: string;
34
+ arquivo: string;
35
+ simbolo: string;
36
+ }
37
+
38
+ type ConsumerFramework = "nextjs-consumer" | "react-vite-consumer" | "angular-consumer" | "flutter-consumer";
39
+
40
+ interface RotaResolvida {
41
+ origem: "nestjs" | "fastapi" | "flask" | "nextjs" | ConsumerFramework | "firebase" | "php" | "dotnet" | "java" | "go" | "rust";
42
+ metodo: string;
43
+ caminho: string;
44
+ arquivo: string;
45
+ simbolo: string;
46
+ }
47
+
48
+ interface RegistroConsumerSurfaceDrift {
49
+ rota: string;
50
+ arquivo: string;
51
+ tipoArquivo: string;
52
+ }
53
+
54
+ interface RegistroConsumerBridgeDrift {
55
+ caminho: string;
56
+ arquivo: string;
57
+ simbolo: string;
58
+ }
59
+
60
+ export type EscopoDriftReal = "arquivo" | "modulo" | "projeto";
61
+
62
+ export interface OpcoesDriftLegado {
63
+ escopo?: EscopoDriftReal;
64
+ ignorarWorktrees?: boolean;
65
+ ignorarConsumidoresLaterais?: boolean;
66
+ }
67
+
68
+ export interface DiagnosticoDrift {
69
+ tipo: "impl_quebrado" | "task_sem_impl" | "rota_divergente" | "recurso_divergente" | "vinculo_quebrado" | "seguranca_frouxa";
70
+ modulo: string;
71
+ task?: string;
72
+ route?: string;
73
+ mensagem: string;
74
+ }
75
+
76
+ interface RegistroImplDrift {
77
+ modulo: string;
78
+ task: string;
79
+ origem: "ts" | "py" | "dart" | "lua" | "php" | "cs" | "java" | "go" | "rust" | "cpp";
80
+ caminho: string;
81
+ arquivo?: string;
82
+ simbolo?: string;
83
+ caminhoResolvido?: string;
84
+ status: "resolvido" | "quebrado";
85
+ candidatos?: SimboloCandidatoDrift[];
86
+ }
87
+
88
+ interface RegistroRotaDivergente {
89
+ modulo: string;
90
+ route: string;
91
+ metodo?: string;
92
+ caminho?: string;
93
+ motivo: string;
94
+ }
95
+
96
+ type OrigemRecursoDrift = "firebase" | EngineBanco | "arquivo";
97
+ type TipoRecursoDrift = "colecao" | TipoRecursoPersistencia | "arquivo_local";
98
+ type CategoriaPersistenciaDrift = "relacional" | "documental" | "chave_valor" | "local_arquivo" | "desconhecida";
99
+
100
+ interface RecursoResolvido {
101
+ origem: OrigemRecursoDrift;
102
+ nome: string;
103
+ arquivo: string;
104
+ simbolo?: string;
105
+ tipo: TipoRecursoDrift;
106
+ }
107
+
108
+ interface RegistroRecursoDrift {
109
+ modulo: string;
110
+ task: string;
111
+ categoria: "persistencia";
112
+ alvo: string;
113
+ arquivo: string;
114
+ origem: OrigemRecursoDrift;
115
+ tipo: TipoRecursoDrift;
116
+ status: "resolvido" | "divergente";
117
+ }
118
+
119
+ interface RecursoEsperadoDrift {
120
+ categoria: "persistencia";
121
+ alvo: string;
122
+ origem?: OrigemRecursoDrift;
123
+ tiposAceitos: TipoRecursoDrift[];
124
+ nomes: string[];
125
+ }
126
+
127
+ interface RegistroColunaPersistenciaDrift {
128
+ origem: OrigemRecursoDrift;
129
+ categoriaPersistencia: CategoriaPersistenciaDrift;
130
+ recurso: string;
131
+ coluna: string;
132
+ arquivo: string;
133
+ }
134
+
135
+ interface RegistroRepositorioPersistenciaDrift {
136
+ origem: OrigemRecursoDrift;
137
+ categoriaPersistencia: CategoriaPersistenciaDrift;
138
+ recurso: string;
139
+ arquivo: string;
140
+ }
141
+
142
+ interface SimboloCandidatoDrift {
143
+ origem: "ts" | "py" | "dart" | "lua" | "php" | "cs" | "java" | "go" | "rust" | "cpp";
144
+ caminho: string;
145
+ arquivo: string;
146
+ simbolo: string;
147
+ confianca: "alta" | "media";
148
+ motivo: string;
149
+ }
150
+
151
+ type AncoragemVinculoTaskDrift = "propria" | "herdada_modulo" | "ausente";
152
+
153
+ interface ResumoTaskDrift {
154
+ modulo: string;
155
+ task: string;
156
+ impls: number;
157
+ implsValidos: number;
158
+ implsQuebrados: number;
159
+ semImplementacao: boolean;
160
+ scoreSemantico: number;
161
+ confiancaVinculo: NivelConfiancaSemantica;
162
+ riscoOperacional: NivelRiscoSemantico;
163
+ lacunas: string[];
164
+ ancoragemVinculo: AncoragemVinculoTaskDrift;
165
+ arquivosReferenciados: string[];
166
+ arquivosAncoraHerdados: string[];
167
+ arquivosProvaveisEditar: string[];
168
+ simbolosReferenciados: string[];
169
+ candidatosImpl: SimboloCandidatoDrift[];
170
+ checksSugeridos: string[];
171
+ }
172
+
173
+ interface RegistroVinculoDrift {
174
+ modulo: string;
175
+ donoTipo: "modulo" | "task" | "flow" | "route" | "superficie";
176
+ dono: string;
177
+ tipo: string;
178
+ valor: string;
179
+ arquivo?: string;
180
+ simbolo?: string;
181
+ status: "resolvido" | "parcial" | "nao_encontrado";
182
+ confianca: NivelConfiancaSemantica;
183
+ }
184
+
185
+ export interface RegistroPersistenciaRealDrift {
186
+ modulo: string;
187
+ task: string;
188
+ alvo: string;
189
+ engine: OrigemRecursoDrift | "desconhecido";
190
+ categoriaPersistencia: CategoriaPersistenciaDrift;
191
+ tipo: TipoRecursoDrift;
192
+ status: "materializado" | "parcial" | "divergente";
193
+ arquivos: string[];
194
+ colunas: string[];
195
+ repositorios: string[];
196
+ compatibilidade: "nativo" | "adaptado" | "parcial" | "invalido" | "desconhecida";
197
+ motivoCompatibilidade?: string;
198
+ }
199
+
200
+ export interface ConfiguracaoEscopoDriftAplicada {
201
+ escopo: EscopoDriftReal;
202
+ ignorarWorktrees: boolean;
203
+ ignorarConsumidoresLaterais: boolean;
204
+ termosEscopo: string[];
205
+ }
206
+
207
+ export interface RegistroImpactoSemanticoArquivo {
208
+ arquivo: string;
209
+ tipo: "contrato" | "persistencia" | "repositorio" | "rota" | "worker" | "ui" | "teste" | "codigo";
210
+ prioridade: "alta" | "media" | "baixa";
211
+ linhas: number[];
212
+ motivos: string[];
213
+ }
214
+
215
+ export interface ResultadoImpactoSemantico {
216
+ comando: "impacto";
217
+ sucesso: boolean;
218
+ escopo: EscopoDriftReal;
219
+ alvoSemantico: string;
220
+ mudancaProposta: string;
221
+ contratosAfetados: string[];
222
+ tasksAfetadas: string[];
223
+ routesAfetadas: string[];
224
+ superficiesAfetadas: string[];
225
+ persistenciaAfetada: string[];
226
+ arquivos: RegistroImpactoSemanticoArquivo[];
227
+ ordemOperacional: string[];
228
+ validacoes: string[];
229
+ }
230
+
231
+ export interface SugestaoRenomeacaoSemantica {
232
+ arquivo: string;
233
+ linha: number;
234
+ atual: string;
235
+ sugerido: string;
236
+ contexto: string;
237
+ }
238
+
239
+ export interface ResultadoRenomeacaoSemantica {
240
+ comando: "renomear-semantico";
241
+ sucesso: boolean;
242
+ escopo: EscopoDriftReal;
243
+ de: string;
244
+ para: string;
245
+ arquivos: RegistroImpactoSemanticoArquivo[];
246
+ sugestoes: SugestaoRenomeacaoSemantica[];
247
+ ordemOperacional: string[];
248
+ validacoes: string[];
249
+ }
250
+
251
+ export interface ResultadoDrift {
252
+ comando: "drift";
253
+ sucesso: boolean;
254
+ escopo_aplicado: ConfiguracaoEscopoDriftAplicada;
255
+ consumerFramework: ConsumerFramework | null;
256
+ appRoutes: string[];
257
+ consumerSurfaces: RegistroConsumerSurfaceDrift[];
258
+ consumerBridges: RegistroConsumerBridgeDrift[];
259
+ modulos: Array<{
260
+ caminho: string;
261
+ modulo: string | null;
262
+ tasks: number;
263
+ routes: number;
264
+ }>;
265
+ tasks: ResumoTaskDrift[];
266
+ impls_validos: RegistroImplDrift[];
267
+ impls_quebrados: RegistroImplDrift[];
268
+ vinculos_validos: RegistroVinculoDrift[];
269
+ vinculos_quebrados: RegistroVinculoDrift[];
270
+ rotas_divergentes: RegistroRotaDivergente[];
271
+ recursos_validos: RegistroRecursoDrift[];
272
+ recursos_divergentes: RegistroRecursoDrift[];
273
+ persistencia_real: RegistroPersistenciaRealDrift[];
274
+ resumo_operacional: {
275
+ scoreMedio: number;
276
+ confiancaGeral: NivelConfiancaSemantica;
277
+ riscosPrincipais: string[];
278
+ oQueTocar: string[];
279
+ oQueValidar: string[];
280
+ oQueEstaFrouxo: string[];
281
+ oQueFoiInferido: string[];
282
+ };
283
+ diagnosticos: DiagnosticoDrift[];
284
+ }
285
+
286
+ const DIRETORIOS_IGNORADOS_BASE = new Set([
287
+ ".git",
288
+ ".hg",
289
+ ".svn",
290
+ "node_modules",
291
+ "dist",
292
+ "build",
293
+ ".next",
294
+ ".nuxt",
295
+ ".dart_tool",
296
+ "__pycache__",
297
+ ".venv",
298
+ "venv",
299
+ "coverage",
300
+ ".tmp",
301
+ "generated",
302
+ ]);
303
+
304
+ const DIRETORIOS_WORKTREE = [
305
+ ".claude",
306
+ "worktrees",
307
+ ];
308
+
309
+ const DIRETORIOS_CONSUMIDOR_LATERAL = [
310
+ "showcase",
311
+ "showcases",
312
+ "storybook",
313
+ "stories",
314
+ "playground",
315
+ "sandbox",
316
+ "fixture",
317
+ "fixtures",
318
+ "demo",
319
+ "demos",
320
+ "sample",
321
+ "samples",
322
+ "mini-web",
323
+ ];
324
+
325
+ const TERMOS_ESCopo_IGNORADOS = new Set([
326
+ "api",
327
+ "app",
328
+ "apps",
329
+ "base",
330
+ "codigo",
331
+ "config",
332
+ "controller",
333
+ "controllers",
334
+ "data",
335
+ "drift",
336
+ "flow",
337
+ "int",
338
+ "module",
339
+ "modulo",
340
+ "publico",
341
+ "route",
342
+ "routes",
343
+ "schema",
344
+ "sema",
345
+ "service",
346
+ "services",
347
+ "src",
348
+ "task",
349
+ "tasks",
350
+ "tests",
351
+ "ui",
352
+ "web",
353
+ ]);
354
+
355
+ let diretoriosIgnoradosAtivos = new Set(DIRETORIOS_IGNORADOS_BASE);
356
+
357
+ function normalizarFragmentoArquivo(valor: string): string {
358
+ return valor.replace(/\\/g, "/").replace(/^\.?\//, "").trim().toLowerCase();
359
+ }
360
+
361
+ function normalizarEscopoDrift(valor?: string): EscopoDriftReal {
362
+ if (valor === "arquivo" || valor === "modulo" || valor === "projeto") {
363
+ return valor;
364
+ }
365
+ return "modulo";
366
+ }
367
+
368
+ function resolverOpcoesDrift(opcoes?: OpcoesDriftLegado): Required<OpcoesDriftLegado> {
369
+ return {
370
+ escopo: normalizarEscopoDrift(opcoes?.escopo),
371
+ ignorarWorktrees: opcoes?.ignorarWorktrees !== false,
372
+ ignorarConsumidoresLaterais: opcoes?.ignorarConsumidoresLaterais !== false,
373
+ };
374
+ }
375
+
376
+ function resolverDiretoriosIgnoradosAtivos(opcoes?: OpcoesDriftLegado): Set<string> {
377
+ const resolvidas = resolverOpcoesDrift(opcoes);
378
+ const diretorios = new Set(DIRETORIOS_IGNORADOS_BASE);
379
+ if (resolvidas.ignorarWorktrees) {
380
+ for (const diretorio of DIRETORIOS_WORKTREE) {
381
+ diretorios.add(diretorio);
382
+ }
383
+ }
384
+ if (resolvidas.ignorarConsumidoresLaterais) {
385
+ for (const diretorio of DIRETORIOS_CONSUMIDOR_LATERAL) {
386
+ diretorios.add(diretorio);
387
+ }
388
+ }
389
+ return diretorios;
390
+ }
391
+
392
+ function quebrarTermosEscopo(valor: string): string[] {
393
+ return paraIdentificadorModulo(valor)
394
+ .split("_")
395
+ .map((item) => item.trim())
396
+ .filter((item) => item.length >= 3 && !TERMOS_ESCopo_IGNORADOS.has(item));
397
+ }
398
+
399
+ function quebrarTermosModuloEscopo(nomeModulo: string): string[] {
400
+ const segmentos = nomeModulo
401
+ .split(".")
402
+ .map((item) => item.trim())
403
+ .filter(Boolean);
404
+ const relevantes = segmentos.length > 1 ? segmentos.slice(1) : segmentos;
405
+ return relevantes.flatMap((segmento) => quebrarTermosEscopo(segmento));
406
+ }
407
+
408
+ function extrairTermosEscopoDrift(contexto: ContextoProjetoCarregado, escopo: EscopoDriftReal): string[] {
409
+ if (escopo === "projeto") {
410
+ return [];
411
+ }
412
+
413
+ const termos = new Set<string>();
414
+ const termosRaizProjeto = new Set(quebrarTermosEscopo(path.basename(contexto.baseProjeto)));
415
+ if (escopo === "arquivo" || path.extname(contexto.entradaResolvida)) {
416
+ termos.add(paraIdentificadorModulo(path.basename(contexto.entradaResolvida, path.extname(contexto.entradaResolvida))));
417
+ }
418
+
419
+ for (const modulo of contexto.modulosSelecionados) {
420
+ const ir = modulo.resultado.ir;
421
+ if (!ir) {
422
+ continue;
423
+ }
424
+ for (const termo of quebrarTermosModuloEscopo(ir.nome)) {
425
+ termos.add(termo);
426
+ }
427
+ for (const task of ir.tasks) {
428
+ for (const termo of quebrarTermosEscopo(task.nome)) {
429
+ termos.add(termo);
430
+ }
431
+ }
432
+ for (const route of ir.routes) {
433
+ for (const termo of quebrarTermosEscopo(route.nome)) {
434
+ termos.add(termo);
435
+ }
436
+ if (route.caminho) {
437
+ for (const termo of quebrarTermosEscopo(route.caminho)) {
438
+ termos.add(termo);
439
+ }
440
+ }
441
+ }
442
+ }
443
+
444
+ return [...termos]
445
+ .filter((termo) => Boolean(termo) && !termosRaizProjeto.has(termo))
446
+ .sort((a, b) => a.localeCompare(b, "pt-BR"));
447
+ }
448
+
449
+ function categorizarPersistenciaPorOrigem(origem?: OrigemRecursoDrift): CategoriaPersistenciaDrift {
450
+ switch (origem) {
451
+ case "postgres":
452
+ case "mysql":
453
+ case "sqlite":
454
+ return "relacional";
455
+ case "mongodb":
456
+ case "firebase":
457
+ return "documental";
458
+ case "redis":
459
+ return "chave_valor";
460
+ case "arquivo":
461
+ return "local_arquivo";
462
+ default:
463
+ return "desconhecida";
464
+ }
465
+ }
466
+
467
+ function caminhoTemSegmentoIgnorado(arquivo: string, segmentos: string[]): boolean {
468
+ const partes = normalizarFragmentoArquivo(arquivo).split("/").filter(Boolean);
469
+ return partes.some((parte) => segmentos.includes(parte));
470
+ }
471
+
472
+ function normalizarCaminhoComparacao(caminhoArquivo: string): string {
473
+ return path.resolve(caminhoArquivo).replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
474
+ }
475
+
476
+ function caminhoEstaDentroDe(base: string, alvo: string): boolean {
477
+ const baseNormalizada = normalizarCaminhoComparacao(base);
478
+ const alvoNormalizado = normalizarCaminhoComparacao(alvo);
479
+ return alvoNormalizado === baseNormalizada || alvoNormalizado.startsWith(`${baseNormalizada}/`);
480
+ }
481
+
482
+ function resolverRaizEscopoReal(contexto: ContextoProjetoCarregado): string {
483
+ const entrada = path.resolve(contexto.entradaResolvida);
484
+ return path.extname(entrada) ? path.dirname(entrada) : entrada;
485
+ }
486
+
487
+ function resolverRaizesExplicitasConfiguradas(contexto: ContextoProjetoCarregado): string[] {
488
+ const configCarregada = contexto.configCarregada;
489
+ if (!configCarregada) {
490
+ return [];
491
+ }
492
+
493
+ const origensDeclaradas = configCarregada.config.origens ?? (configCarregada.config.origem ? [configCarregada.config.origem] : []);
494
+ return [...new Set([
495
+ ...(configCarregada.config.diretoriosCodigo ?? []).map((diretorio) => path.resolve(configCarregada.baseDiretorio, diretorio)),
496
+ ...origensDeclaradas.map((origem) => path.resolve(configCarregada.baseDiretorio, origem)),
497
+ ])].sort((a, b) => a.localeCompare(b, "pt-BR"));
498
+ }
499
+
500
+ function resolverRaizesIgnoradasPermitidas(
501
+ contexto: ContextoProjetoCarregado,
502
+ segmentosIgnorados: string[],
503
+ ): string[] {
504
+ return [...new Set([
505
+ path.resolve(contexto.baseProjeto),
506
+ resolverRaizEscopoReal(contexto),
507
+ ...resolverRaizesExplicitasConfiguradas(contexto),
508
+ ])]
509
+ .filter((raiz) => caminhoTemSegmentoIgnorado(raiz, segmentosIgnorados))
510
+ .sort((a, b) => a.localeCompare(b, "pt-BR"));
511
+ }
512
+
513
+ function caminhoIgnoradoForaDoEscopoReal(
514
+ caminhoArquivo: string,
515
+ segmentosIgnorados: string[],
516
+ raizesPermitidas: string[],
517
+ ): boolean {
518
+ if (!caminhoTemSegmentoIgnorado(caminhoArquivo, segmentosIgnorados)) {
519
+ return false;
520
+ }
521
+ if (raizesPermitidas.length === 0) {
522
+ return true;
523
+ }
524
+ return !raizesPermitidas.some((raiz) => caminhoEstaDentroDe(raiz, caminhoArquivo));
525
+ }
526
+
527
+ function filtrarCaminhosEscopoReal(
528
+ caminhos: string[],
529
+ contexto: ContextoProjetoCarregado,
530
+ configuracao: Pick<ConfiguracaoEscopoDriftAplicada, "ignorarWorktrees" | "ignorarConsumidoresLaterais">,
531
+ ): string[] {
532
+ const raizesWorktreePermitidas = resolverRaizesIgnoradasPermitidas(contexto, DIRETORIOS_WORKTREE);
533
+ const raizesConsumidorPermitidas = resolverRaizesIgnoradasPermitidas(contexto, DIRETORIOS_CONSUMIDOR_LATERAL);
534
+ return caminhos.filter((caminho) => {
535
+ if (configuracao.ignorarWorktrees && caminhoIgnoradoForaDoEscopoReal(caminho, DIRETORIOS_WORKTREE, raizesWorktreePermitidas)) {
536
+ return false;
537
+ }
538
+ if (configuracao.ignorarConsumidoresLaterais
539
+ && caminhoIgnoradoForaDoEscopoReal(caminho, DIRETORIOS_CONSUMIDOR_LATERAL, raizesConsumidorPermitidas)) {
540
+ return false;
541
+ }
542
+ return true;
543
+ });
544
+ }
545
+
546
+ function resolverDiretoriosCodigoEscopoReal(
547
+ contexto: ContextoProjetoCarregado,
548
+ configuracao: Pick<ConfiguracaoEscopoDriftAplicada, "ignorarWorktrees" | "ignorarConsumidoresLaterais">,
549
+ ): string[] {
550
+ return filtrarCaminhosEscopoReal(contexto.diretoriosCodigo, contexto, configuracao);
551
+ }
552
+
553
+ function textoCombinaEscopo(texto: string, termos: string[]): boolean {
554
+ if (termos.length === 0) {
555
+ return true;
556
+ }
557
+ const normalizado = paraIdentificadorModulo(texto);
558
+ return termos.some((termo) => normalizado.includes(termo));
559
+ }
560
+
561
+ interface ContextoRelevanciaConsumerDrift {
562
+ arquivosAncora: string[];
563
+ rotasAncora: string[];
564
+ }
565
+
566
+ function construirContextoRelevanciaConsumer(
567
+ contexto: ContextoProjetoCarregado,
568
+ tasksResumo: ResumoTaskDrift[],
569
+ vinculosValidos: RegistroVinculoDrift[],
570
+ ): ContextoRelevanciaConsumerDrift {
571
+ const arquivosAncora = new Set<string>();
572
+ const rotasAncora = new Set<string>();
573
+
574
+ for (const modulo of contexto.modulosSelecionados) {
575
+ const ir = modulo.resultado.ir;
576
+ if (!ir) {
577
+ continue;
578
+ }
579
+ for (const route of ir.routes) {
580
+ if (route.caminho) {
581
+ rotasAncora.add(normalizarCaminhoRota(route.caminho));
582
+ }
583
+ }
584
+ }
585
+
586
+ for (const task of tasksResumo) {
587
+ for (const arquivo of [...task.arquivosReferenciados, ...task.arquivosProvaveisEditar]) {
588
+ arquivosAncora.add(arquivo);
589
+ }
590
+ }
591
+
592
+ for (const vinculo of vinculosValidos) {
593
+ if (vinculo.arquivo) {
594
+ arquivosAncora.add(vinculo.arquivo);
595
+ }
596
+ if (vinculo.tipo === "superficie" || vinculo.tipo === "rota") {
597
+ rotasAncora.add(normalizarCaminhoRota(vinculo.valor));
598
+ }
599
+ }
600
+
601
+ return {
602
+ arquivosAncora: [...arquivosAncora].sort((a, b) => a.localeCompare(b, "pt-BR")),
603
+ rotasAncora: [...rotasAncora].sort((a, b) => a.localeCompare(b, "pt-BR")),
604
+ };
605
+ }
606
+
607
+ function pontuarTextoEscopo(texto: string, termos: string[]): number {
608
+ if (termos.length === 0) {
609
+ return 0;
610
+ }
611
+ const normalizado = paraIdentificadorModulo(texto);
612
+ const segmentos = new Set(normalizado.split("_").filter(Boolean));
613
+ let score = 0;
614
+ for (const termo of termos) {
615
+ if (segmentos.has(termo)) {
616
+ score += 4;
617
+ continue;
618
+ }
619
+ if (normalizado.includes(termo)) {
620
+ score += 2;
621
+ }
622
+ }
623
+ return Math.min(score, 8);
624
+ }
625
+
626
+ function pontuarProximidadeArquivoConsumer(arquivo: string, arquivosAncora: string[]): number {
627
+ if (arquivosAncora.length === 0) {
628
+ return 0;
629
+ }
630
+
631
+ const alvo = normalizarFragmentoArquivo(arquivo);
632
+ const diretorioAlvo = path.posix.dirname(alvo);
633
+ let scoreMaximo = 0;
634
+
635
+ for (const ancora of arquivosAncora) {
636
+ const ancoraNormalizada = normalizarFragmentoArquivo(ancora);
637
+ const diretorioAncora = path.posix.dirname(ancoraNormalizada);
638
+
639
+ if (alvo === ancoraNormalizada) {
640
+ return 8;
641
+ }
642
+ if (diretorioAlvo === diretorioAncora) {
643
+ scoreMaximo = Math.max(scoreMaximo, 6);
644
+ continue;
645
+ }
646
+
647
+ const ultimoDiretorioAlvo = diretorioAlvo.split("/").filter(Boolean).at(-1);
648
+ const ultimoDiretorioAncora = diretorioAncora.split("/").filter(Boolean).at(-1);
649
+ if (ultimoDiretorioAlvo && ultimoDiretorioAlvo === ultimoDiretorioAncora) {
650
+ scoreMaximo = Math.max(scoreMaximo, 4);
651
+ }
652
+ }
653
+
654
+ return scoreMaximo;
655
+ }
656
+
657
+ function pontuarProximidadeRotaConsumer(rota: string, rotasAncora: string[]): number {
658
+ if (rotasAncora.length === 0) {
659
+ return 0;
660
+ }
661
+
662
+ const rotaNormalizada = normalizarCaminhoRota(rota);
663
+ const segmentosRota = rotaNormalizada.split("/").filter(Boolean);
664
+ let scoreMaximo = 0;
665
+
666
+ for (const ancora of rotasAncora) {
667
+ const rotaAncora = normalizarCaminhoRota(ancora);
668
+ if (rotaNormalizada === rotaAncora) {
669
+ return 8;
670
+ }
671
+
672
+ const segmentosAncora = rotaAncora.split("/").filter(Boolean);
673
+ if (segmentosRota[0] && segmentosRota[0] === segmentosAncora[0]) {
674
+ scoreMaximo = Math.max(scoreMaximo, 4);
675
+ }
676
+ }
677
+
678
+ return scoreMaximo;
679
+ }
680
+
681
+ function filtrarConsumerSurfacesPorEscopo(
682
+ consumerSurfaces: RegistroConsumerSurfaceDrift[],
683
+ consumerBridges: RegistroConsumerBridgeDrift[],
684
+ contexto: ContextoProjetoCarregado,
685
+ configuracao: ConfiguracaoEscopoDriftAplicada,
686
+ relevancia?: ContextoRelevanciaConsumerDrift,
687
+ ): {
688
+ consumerSurfaces: RegistroConsumerSurfaceDrift[];
689
+ consumerBridges: RegistroConsumerBridgeDrift[];
690
+ } {
691
+ const raizesWorktreePermitidas = resolverRaizesIgnoradasPermitidas(contexto, DIRETORIOS_WORKTREE);
692
+ const raizesConsumidorPermitidas = resolverRaizesIgnoradasPermitidas(contexto, DIRETORIOS_CONSUMIDOR_LATERAL);
693
+ const limiar = configuracao.escopo === "arquivo" ? 5 : 4;
694
+ const manterSurface = (surface: RegistroConsumerSurfaceDrift) => {
695
+ if (configuracao.ignorarWorktrees
696
+ && caminhoIgnoradoForaDoEscopoReal(surface.arquivo, DIRETORIOS_WORKTREE, raizesWorktreePermitidas)) {
697
+ return false;
698
+ }
699
+ if (configuracao.ignorarConsumidoresLaterais
700
+ && caminhoIgnoradoForaDoEscopoReal(surface.arquivo, DIRETORIOS_CONSUMIDOR_LATERAL, raizesConsumidorPermitidas)) {
701
+ return false;
702
+ }
703
+ if (configuracao.escopo === "projeto") {
704
+ return true;
705
+ }
706
+
707
+ const score = pontuarTextoEscopo(`${surface.rota} ${surface.arquivo} ${surface.tipoArquivo}`, configuracao.termosEscopo)
708
+ + pontuarProximidadeArquivoConsumer(surface.arquivo, relevancia?.arquivosAncora ?? [])
709
+ + pontuarProximidadeRotaConsumer(surface.rota, relevancia?.rotasAncora ?? []);
710
+ return score >= limiar;
711
+ };
712
+
713
+ const manterBridge = (bridge: RegistroConsumerBridgeDrift) => {
714
+ if (configuracao.ignorarWorktrees
715
+ && caminhoIgnoradoForaDoEscopoReal(bridge.arquivo, DIRETORIOS_WORKTREE, raizesWorktreePermitidas)) {
716
+ return false;
717
+ }
718
+ if (configuracao.ignorarConsumidoresLaterais
719
+ && caminhoIgnoradoForaDoEscopoReal(bridge.arquivo, DIRETORIOS_CONSUMIDOR_LATERAL, raizesConsumidorPermitidas)) {
720
+ return false;
721
+ }
722
+ if (configuracao.escopo === "projeto") {
723
+ return true;
724
+ }
725
+
726
+ const score = pontuarTextoEscopo(`${bridge.caminho} ${bridge.arquivo} ${bridge.simbolo}`, configuracao.termosEscopo)
727
+ + pontuarProximidadeArquivoConsumer(bridge.arquivo, relevancia?.arquivosAncora ?? []);
728
+ return score >= limiar;
729
+ };
730
+
731
+ return {
732
+ consumerSurfaces: consumerSurfaces.filter(manterSurface),
733
+ consumerBridges: consumerBridges.filter(manterBridge),
734
+ };
735
+ }
736
+
737
+ const NOMES_RECURSO_IGNORADOS = new Set([
738
+ "all",
739
+ "and",
740
+ "as",
741
+ "by",
742
+ "create",
743
+ "delete",
744
+ "from",
745
+ "group",
746
+ "inner",
747
+ "into",
748
+ "join",
749
+ "left",
750
+ "limit",
751
+ "offset",
752
+ "on",
753
+ "or",
754
+ "order",
755
+ "outer",
756
+ "returning",
757
+ "right",
758
+ "select",
759
+ "set",
760
+ "table",
761
+ "update",
762
+ "values",
763
+ "view",
764
+ "where",
765
+ ]);
766
+
767
+ const OPERACOES_REDIS_KEYSPACE = [
768
+ "append",
769
+ "decr",
770
+ "del",
771
+ "expire",
772
+ "expireat",
773
+ "get",
774
+ "getdel",
775
+ "getex",
776
+ "getrange",
777
+ "hdel",
778
+ "hexists",
779
+ "hget",
780
+ "hgetall",
781
+ "hincrby",
782
+ "hkeys",
783
+ "hlen",
784
+ "hmget",
785
+ "hmset",
786
+ "hrandfield",
787
+ "hscan",
788
+ "hset",
789
+ "hsetnx",
790
+ "hvals",
791
+ "incr",
792
+ "incrby",
793
+ "lindex",
794
+ "llen",
795
+ "lpop",
796
+ "lpush",
797
+ "lrange",
798
+ "lrem",
799
+ "lset",
800
+ "rpop",
801
+ "rpush",
802
+ "sadd",
803
+ "scard",
804
+ "set",
805
+ "setex",
806
+ "setnx",
807
+ "smembers",
808
+ "spop",
809
+ "srem",
810
+ "ttl",
811
+ "type",
812
+ "zadd",
813
+ "zcard",
814
+ "zrange",
815
+ "zrem",
816
+ ];
817
+
818
+ const OPERACOES_REDIS_STREAM = [
819
+ "xadd",
820
+ "xdel",
821
+ "xgroupcreate",
822
+ "xgroupdestroy",
823
+ "xlen",
824
+ "xrange",
825
+ "xread",
826
+ "xreadgroup",
827
+ "xrevrange",
828
+ "xtrim",
829
+ ];
830
+
831
+ function limparLiteralRecurso(valor: string): string {
832
+ return valor
833
+ .trim()
834
+ .replace(/^["'`]+|["'`]+$/g, "")
835
+ .replace(/\$\{[^}]+\}/g, "")
836
+ .replace(/\{[^}]+\}/g, "")
837
+ .replace(/%[sdifjo]/gi, "")
838
+ .trim();
839
+ }
840
+
841
+ function fecharPrefixoRecurso(valor: string): string {
842
+ return valor.replace(/[:/_\-.]+$/g, "").trim();
843
+ }
844
+
845
+ function normalizarNomeRecursoDrift(valor: string): string {
846
+ return fecharPrefixoRecurso(limparLiteralRecurso(valor))
847
+ .normalize("NFD")
848
+ .replace(/[\u0300-\u036f]/g, "")
849
+ .toLowerCase()
850
+ .replace(/["'`]/g, "")
851
+ .replace(/\s+/g, "");
852
+ }
853
+
854
+ function variantesNomeRecursoDrift(valor: string): string[] {
855
+ const base = fecharPrefixoRecurso(limparLiteralRecurso(valor));
856
+ if (!base) {
857
+ return [];
858
+ }
859
+
860
+ const variantes = new Set<string>();
861
+ const registrar = (candidato?: string) => {
862
+ if (!candidato) {
863
+ return;
864
+ }
865
+ const normalizado = normalizarNomeRecursoDrift(candidato);
866
+ if (normalizado) {
867
+ variantes.add(normalizado);
868
+ }
869
+ };
870
+
871
+ registrar(base);
872
+ registrar(base.replace(/[.:/_-]+/g, "_"));
873
+ registrar(base.replace(/[.:/_-]+/g, ""));
874
+
875
+ const partes = base.split(/[.:/_-]+/).filter(Boolean);
876
+ if (partes.length > 1) {
877
+ registrar(partes.join("_"));
878
+ registrar(partes.join(""));
879
+ }
880
+
881
+ const singular = base.replace(/s$/i, "");
882
+ if (singular && singular !== base) {
883
+ registrar(singular);
884
+ } else if (!/s$/i.test(base)) {
885
+ registrar(`${base}s`);
886
+ }
887
+
888
+ return [...variantes];
889
+ }
890
+
891
+ function recursoEhIgnorado(nome: string): boolean {
892
+ const normalizado = normalizarNomeRecursoDrift(nome);
893
+ if (!normalizado || normalizado.length < 2) {
894
+ return true;
895
+ }
896
+ return NOMES_RECURSO_IGNORADOS.has(normalizado);
897
+ }
898
+
899
+ function registrarRecursoDrift(
900
+ recursos: Map<string, RecursoResolvido>,
901
+ origem: OrigemRecursoDrift,
902
+ tipo: TipoRecursoDrift,
903
+ nome: string,
904
+ arquivo: string,
905
+ simbolo?: string,
906
+ ): void {
907
+ const nomeLimpo = fecharPrefixoRecurso(limparLiteralRecurso(nome));
908
+ if (!nomeLimpo || recursoEhIgnorado(nomeLimpo)) {
909
+ return;
910
+ }
911
+
912
+ const chave = `${origem}:${tipo}:${normalizarNomeRecursoDrift(nomeLimpo)}:${arquivo}:${simbolo ?? ""}`;
913
+ if (!recursos.has(chave)) {
914
+ recursos.set(chave, {
915
+ origem,
916
+ nome: nomeLimpo,
917
+ arquivo,
918
+ simbolo,
919
+ tipo,
920
+ });
921
+ }
922
+ }
923
+
924
+ function inferirMotoresRelacionais(codigo: string, arquivo: string): EngineBanco[] {
925
+ const motores = new Set<EngineBanco>();
926
+ const caminho = normalizarFragmentoArquivo(arquivo);
927
+ if (
928
+ /\b(?:from|require)\s*\(?["'`]pg["'`]/i.test(codigo)
929
+ || /\bpostgres(?:ql)?\b/i.test(codigo)
930
+ || /\bon\s+conflict\b/i.test(codigo)
931
+ || /\breturning\b/i.test(codigo)
932
+ || /\bjsonb\b/i.test(codigo)
933
+ || /\bilike\b/i.test(codigo)
934
+ || /(?:^|\/)(?:postgres|pgsql)(?:\/|[-_.])/i.test(caminho)
935
+ ) {
936
+ motores.add("postgres");
937
+ }
938
+ if (
939
+ /\b(?:from|require)\s*\(?["'`](?:mysql2?(?:\/promise)?|mysql)["'`]/i.test(codigo)
940
+ || /\bon\s+duplicate\s+key\b/i.test(codigo)
941
+ || /\bauto_increment\b/i.test(codigo)
942
+ || /\binnodb\b/i.test(codigo)
943
+ || /\bunsigned\b/i.test(codigo)
944
+ || /(?:^|\/)mysql(?:\/|[-_.])/i.test(caminho)
945
+ ) {
946
+ motores.add("mysql");
947
+ }
948
+ if (
949
+ /\b(?:from|require)\s*\(?["'`](?:sqlite3|better-sqlite3|bun:sqlite|sqlite)["'`]/i.test(codigo)
950
+ || /\bpragma\b/i.test(codigo)
951
+ || /\bwithout\s+rowid\b/i.test(codigo)
952
+ || /\bsqlite\b/i.test(codigo)
953
+ || /(?:^|\/)sqlite(?:\/|[-_.])/i.test(caminho)
954
+ ) {
955
+ motores.add("sqlite");
956
+ }
957
+
958
+ const temSqlGenerico = /\b(?:select\b[\s\S]*?\bfrom\b|insert\s+into|update\s+[A-Za-z_][\w$.-]*\s+set|delete\s+from|create\s+(?:table|view)|alter\s+table|drop\s+(?:table|view)|join\s+[A-Za-z_][\w$.-]*)/i.test(codigo)
959
+ || /\.(?:from|into|table)\s*\(\s*["'`]/i.test(codigo)
960
+ || /\b(?:knex|db|trx)\s*\(\s*["'`][A-Za-z_][^"'`]+["'`]\s*\)/i.test(codigo)
961
+ || /\bprisma\.[A-Za-z_]\w*\.(?:find\w+|create|update|delete|upsert|aggregate|count)\b/i.test(codigo);
962
+ if (temSqlGenerico && motores.size === 0) {
963
+ motores.add("postgres");
964
+ motores.add("mysql");
965
+ motores.add("sqlite");
966
+ }
967
+
968
+ return [...motores];
969
+ }
970
+
971
+ function extrairRecursosSql(arquivo: string, codigo: string): RecursoResolvido[] {
972
+ const recursos = new Map<string, RecursoResolvido>();
973
+ const motores = inferirMotoresRelacionais(codigo, arquivo);
974
+ if (motores.length === 0) {
975
+ return [];
976
+ }
977
+
978
+ const registrarParaMotores = (tipo: TipoRecursoDrift, nome: string) => {
979
+ for (const motor of motores) {
980
+ registrarRecursoDrift(recursos, motor, tipo, nome, arquivo);
981
+ }
982
+ };
983
+
984
+ const registrarTextoSql = (texto: string) => {
985
+ if (!/\b(?:select\b[\s\S]*?\bfrom\b|insert\s+into|update\s+[A-Za-z_][\w$.-]*\s+set|delete\s+from|create\s+(?:table|view)|alter\s+table|drop\s+(?:table|view)|join\s+[A-Za-z_][\w$.-]*|create\s+(?:unique\s+)?index)\b/i.test(texto)) {
986
+ return;
987
+ }
988
+
989
+ for (const match of texto.matchAll(/\bcreate\s+(?:or\s+replace\s+)?(table|view)\s+(?:if\s+not\s+exists\s+)?["'`]?([A-Za-z_][\w$.-]*)["'`]?/gi)) {
990
+ registrarParaMotores(match[1]!.toLowerCase() as TipoRecursoDrift, match[2]!);
991
+ }
992
+
993
+ for (const match of texto.matchAll(/\bcreate\s+(?:unique\s+)?index\s+(?:if\s+not\s+exists\s+)?["'`]?([A-Za-z_][\w$.-]*)["'`]?/gi)) {
994
+ registrarParaMotores("index", match[1]!);
995
+ }
996
+
997
+ for (const match of texto.matchAll(/\b(?:insert\s+into|update|from|join|delete\s+from|truncate\s+table)\s+["'`]?([A-Za-z_][\w$.-]*)["'`]?/gi)) {
998
+ registrarParaMotores("table", match[1]!);
999
+ }
1000
+ };
1001
+
1002
+ if (/\.(?:sql|psql|ddl)$/i.test(arquivo)) {
1003
+ registrarTextoSql(codigo);
1004
+ } else {
1005
+ for (const literal of codigo.matchAll(/(["'`])([\s\S]*?)\1/g)) {
1006
+ registrarTextoSql(literal[2] ?? "");
1007
+ }
1008
+ }
1009
+
1010
+ for (const match of codigo.matchAll(/\.(?:from|into|table)\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/gi)) {
1011
+ registrarParaMotores("table", match[1]!);
1012
+ }
1013
+
1014
+ for (const match of codigo.matchAll(/\b(?:knex|db|trx)\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/gi)) {
1015
+ registrarParaMotores("table", match[1]!);
1016
+ }
1017
+
1018
+ for (const match of codigo.matchAll(/\bprisma\.([A-Za-z_]\w*)\.(?:find\w+|create|update|delete|upsert|aggregate|count)\b/gi)) {
1019
+ registrarParaMotores("table", match[1]!);
1020
+ }
1021
+
1022
+ return [...recursos.values()];
1023
+ }
1024
+
1025
+ function extrairRecursosMongoDb(arquivo: string, codigo: string): RecursoResolvido[] {
1026
+ const recursos = new Map<string, RecursoResolvido>();
1027
+ const contextoMongo = /\b(?:mongodb|mongoose|mongoclient|objectid)\b/i.test(codigo)
1028
+ || /\bdb\.collection\s*\(/i.test(codigo)
1029
+ || /(?:^|\/)mongo(?:db)?(?:\/|[-_.])/i.test(normalizarFragmentoArquivo(arquivo));
1030
+ if (!contextoMongo) {
1031
+ return [];
1032
+ }
1033
+
1034
+ for (const match of codigo.matchAll(/\b(?:db\.)?collection\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/gi)) {
1035
+ registrarRecursoDrift(recursos, "mongodb", "collection", match[1]!, arquivo);
1036
+ }
1037
+
1038
+ for (const match of codigo.matchAll(/\bgetCollection\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/gi)) {
1039
+ registrarRecursoDrift(recursos, "mongodb", "collection", match[1]!, arquivo);
1040
+ }
1041
+
1042
+ for (const match of codigo.matchAll(/\bmongoose\.model\s*\(\s*["'`]([^"'`]+)["'`](?:\s*,[\s\S]*?,\s*["'`]([^"'`]+)["'`])?/gi)) {
1043
+ registrarRecursoDrift(recursos, "mongodb", "document", match[1]!, arquivo);
1044
+ if (match[2]) {
1045
+ registrarRecursoDrift(recursos, "mongodb", "collection", match[2], arquivo);
1046
+ }
1047
+ }
1048
+
1049
+ for (const match of codigo.matchAll(/\bdb\.([A-Za-z_]\w*)\.(?:find|findOne|aggregate|insertOne|insertMany|updateOne|updateMany|deleteOne|deleteMany|countDocuments)\b/gi)) {
1050
+ registrarRecursoDrift(recursos, "mongodb", "collection", match[1]!, arquivo);
1051
+ }
1052
+
1053
+ return [...recursos.values()];
1054
+ }
1055
+
1056
+ function extrairRecursosRedis(arquivo: string, codigo: string): RecursoResolvido[] {
1057
+ const recursos = new Map<string, RecursoResolvido>();
1058
+ const contextoRedis = /\b(?:from|require)\s*\(?["'`](?:redis|ioredis)["'`]/i.test(codigo)
1059
+ || /\bcreateClient\s*\(/i.test(codigo)
1060
+ || /\bx(?:add|read|readgroup|groupcreate|groupdestroy)\s*\(/i.test(codigo)
1061
+ || /(?:^|\/)redis(?:\/|[-_.])/i.test(normalizarFragmentoArquivo(arquivo));
1062
+ if (!contextoRedis) {
1063
+ return [];
1064
+ }
1065
+
1066
+ const operacoesKeyspace = OPERACOES_REDIS_KEYSPACE.join("|");
1067
+ const operacoesStream = OPERACOES_REDIS_STREAM.join("|");
1068
+ const padraoKeyspace = new RegExp(`\\b(?:${operacoesKeyspace})\\s*\\(\\s*['"\\\`]([^'"\\\`]+)['"\\\`]`, "gi");
1069
+ const padraoStream = new RegExp(`\\b(?:${operacoesStream})\\s*\\(\\s*['"\\\`]([^'"\\\`]+)['"\\\`]`, "gi");
1070
+
1071
+ for (const match of codigo.matchAll(padraoKeyspace)) {
1072
+ registrarRecursoDrift(recursos, "redis", "keyspace", match[1]!, arquivo);
1073
+ }
1074
+
1075
+ for (const match of codigo.matchAll(padraoStream)) {
1076
+ registrarRecursoDrift(recursos, "redis", "stream", match[1]!, arquivo);
1077
+ }
1078
+
1079
+ return [...recursos.values()];
1080
+ }
1081
+
1082
+ function extrairRecursosArquivoLocal(arquivo: string, codigo: string): RecursoResolvido[] {
1083
+ const recursos = new Map<string, RecursoResolvido>();
1084
+ const contextoArquivoLocal = /\b(?:json|jsonl|ndjson)\b/i.test(codigo)
1085
+ || /\b(?:read_text|write_text|readFile(?:Sync)?|writeFile(?:Sync)?|open)\b/i.test(codigo)
1086
+ || /\.(?:json|jsonl|ndjson|db|sqlite|sqlite3)\b/i.test(codigo)
1087
+ || /@capacitor\/preferences|Preferences\.(?:get|set|remove)\s*\(/i.test(codigo)
1088
+ || /\b(?:localStorage|sessionStorage)\.(?:getItem|setItem|removeItem)\s*\(/i.test(codigo)
1089
+ || /(?:repository|repositories|repositorio|repo|store|storage|persist|cache)/i.test(normalizarFragmentoArquivo(arquivo));
1090
+ if (!contextoArquivoLocal) {
1091
+ return [];
1092
+ }
1093
+
1094
+ for (const match of codigo.matchAll(/["'`]([^"'`]+\.(?:json|jsonl|ndjson|db|sqlite|sqlite3))["'`]/gi)) {
1095
+ const literal = match[1] ?? "";
1096
+ const nomeBase = path.basename(literal, path.extname(literal));
1097
+ registrarRecursoDrift(recursos, "arquivo", "arquivo_local", nomeBase, arquivo);
1098
+ }
1099
+
1100
+ const nomeArquivo = path.basename(arquivo).replace(/\.(?:ts|tsx|js|jsx|mjs|cjs|py|dart|lua|cs|java|go|rs|cpp|cc|cxx|hpp|h)$/i, "");
1101
+ const nomeStore = nomeArquivo
1102
+ .replace(/(?:[_.-]?(?:repository|repositories|repo|store|storage|persistencia|persistence))$/i, "")
1103
+ .trim();
1104
+ if (nomeStore && /(?:repository|repositories|repositorio|repo|store|storage|persist|cache)/i.test(nomeArquivo)) {
1105
+ registrarRecursoDrift(recursos, "arquivo", "arquivo_local", nomeStore, arquivo);
1106
+ }
1107
+
1108
+ for (const match of codigo.matchAll(/Preferences\.(?:get|set|remove)\s*\(\s*\{[\s\S]{0,160}?key\s*:\s*["'`]([^"'`]+)["'`]/gi)) {
1109
+ registrarRecursoDrift(recursos, "arquivo", "arquivo_local", match[1]!, arquivo);
1110
+ }
1111
+
1112
+ for (const match of codigo.matchAll(/\b(?:localStorage|sessionStorage)\.(?:getItem|setItem|removeItem)\s*\(\s*["'`]([^"'`]+)["'`]/gi)) {
1113
+ registrarRecursoDrift(recursos, "arquivo", "arquivo_local", match[1]!, arquivo);
1114
+ }
1115
+
1116
+ return [...recursos.values()];
1117
+ }
1118
+
1119
+ function extrairRecursosPersistenciaCodigoVivo(arquivo: string, codigo: string): RecursoResolvido[] {
1120
+ const recursos = new Map<string, RecursoResolvido>();
1121
+
1122
+ for (const recurso of extrairColecoesFirebase(arquivo, codigo)) {
1123
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
1124
+ }
1125
+ for (const recurso of extrairRecursosSql(arquivo, codigo)) {
1126
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
1127
+ }
1128
+ for (const recurso of extrairRecursosMongoDb(arquivo, codigo)) {
1129
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
1130
+ }
1131
+ for (const recurso of extrairRecursosRedis(arquivo, codigo)) {
1132
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
1133
+ }
1134
+ for (const recurso of extrairRecursosArquivoLocal(arquivo, codigo)) {
1135
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
1136
+ }
1137
+
1138
+ return [...recursos.values()];
1139
+ }
1140
+
1141
+ function extrairRecursosPrisma(arquivo: string, codigo: string): RecursoResolvido[] {
1142
+ const recursos = new Map<string, RecursoResolvido>();
1143
+ const provider = codigo.match(/\bprovider\s*=\s*["'`](postgresql|mysql|sqlite)["'`]/i)?.[1]?.toLowerCase();
1144
+ const origem = provider === "postgresql"
1145
+ ? "postgres"
1146
+ : provider === "mysql"
1147
+ ? "mysql"
1148
+ : provider === "sqlite"
1149
+ ? "sqlite"
1150
+ : undefined;
1151
+ if (!origem) {
1152
+ return [];
1153
+ }
1154
+
1155
+ for (const match of codigo.matchAll(/\bmodel\s+([A-Za-z_]\w*)\s*\{([\s\S]*?)\n\}/g)) {
1156
+ const nomeModelo = match[1]!;
1157
+ const corpo = match[2] ?? "";
1158
+ const tabelaMapeada = corpo.match(/@@map\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/)?.[1];
1159
+ registrarRecursoDrift(recursos, origem, "table", tabelaMapeada ?? nomeModelo, arquivo);
1160
+ if (tabelaMapeada) {
1161
+ registrarRecursoDrift(recursos, origem, "table", nomeModelo, arquivo);
1162
+ }
1163
+ }
1164
+
1165
+ return [...recursos.values()];
1166
+ }
1167
+
1168
+ function escolherArquivoPorVinculo(arquivos: string[], valor: string): { arquivo?: string; confianca: NivelConfiancaSemantica; status: RegistroVinculoDrift["status"] } {
1169
+ const normalizado = normalizarFragmentoArquivo(valor);
1170
+ const exato = arquivos.find((arquivo) => normalizarFragmentoArquivo(arquivo) === normalizado);
1171
+ if (exato) {
1172
+ return { arquivo: exato, confianca: "alta", status: "resolvido" };
1173
+ }
1174
+
1175
+ const porSufixo = arquivos.find((arquivo) => normalizarFragmentoArquivo(arquivo).endsWith(normalizado));
1176
+ if (porSufixo) {
1177
+ return { arquivo: porSufixo, confianca: "media", status: "parcial" };
1178
+ }
1179
+
1180
+ return { confianca: "baixa", status: "nao_encontrado" };
1181
+ }
1182
+
1183
+ function escolherSimboloPorVinculo(
1184
+ simbolos: SimboloResolvido[],
1185
+ mapaImpl: Map<string, SimboloResolvido>,
1186
+ valor: string,
1187
+ ): { simbolo?: SimboloResolvido; confianca: NivelConfiancaSemantica; status: RegistroVinculoDrift["status"] } {
1188
+ const exato = mapaImpl.get(valor);
1189
+ if (exato) {
1190
+ return { simbolo: exato, confianca: "alta", status: "resolvido" };
1191
+ }
1192
+
1193
+ const ultimoSegmento = valor.split(".").at(-1)?.toLowerCase();
1194
+ const aproximado = simbolos.find((simbolo) =>
1195
+ simbolo.caminho.toLowerCase() === valor.toLowerCase()
1196
+ || simbolo.simbolo.toLowerCase() === ultimoSegmento
1197
+ || simbolo.caminho.toLowerCase().endsWith(`.${ultimoSegmento}`));
1198
+
1199
+ if (aproximado) {
1200
+ return { simbolo: aproximado, confianca: "media", status: "parcial" };
1201
+ }
1202
+
1203
+ return { confianca: "baixa", status: "nao_encontrado" };
1204
+ }
1205
+
1206
+ function resolverArquivoOuSimboloAncora(
1207
+ vinculos: IrVinculo[],
1208
+ simbolos: SimboloResolvido[],
1209
+ mapaImpl: Map<string, SimboloResolvido>,
1210
+ arquivos: string[],
1211
+ ): { arquivo?: string; simbolo?: string; confianca: NivelConfiancaSemantica } | undefined {
1212
+ for (const vinculo of vinculos) {
1213
+ if (vinculo.simbolo) {
1214
+ const resolucaoSimbolo = escolherSimboloPorVinculo(simbolos, mapaImpl, vinculo.simbolo);
1215
+ if (resolucaoSimbolo.status !== "nao_encontrado") {
1216
+ return {
1217
+ arquivo: resolucaoSimbolo.simbolo?.arquivo,
1218
+ simbolo: resolucaoSimbolo.simbolo?.simbolo,
1219
+ confianca: resolucaoSimbolo.confianca,
1220
+ };
1221
+ }
1222
+ }
1223
+
1224
+ if (vinculo.arquivo) {
1225
+ const resolucaoArquivo = escolherArquivoPorVinculo(arquivos, vinculo.arquivo);
1226
+ if (resolucaoArquivo.status !== "nao_encontrado") {
1227
+ return {
1228
+ arquivo: resolucaoArquivo.arquivo,
1229
+ confianca: resolucaoArquivo.confianca,
1230
+ };
1231
+ }
1232
+ }
1233
+ }
1234
+
1235
+ return undefined;
1236
+ }
1237
+
1238
+ function encontrarAncoraSuperficie(
1239
+ ir: IrModulo,
1240
+ superficie: IrSuperficie,
1241
+ simbolos: SimboloResolvido[],
1242
+ mapaImpl: Map<string, SimboloResolvido>,
1243
+ arquivos: string[],
1244
+ ): { arquivo?: string; simbolo?: string; confianca: NivelConfiancaSemantica } | undefined {
1245
+ const ancoraDireta = resolverArquivoOuSimboloAncora(superficie.vinculos, simbolos, mapaImpl, arquivos);
1246
+ if (ancoraDireta) {
1247
+ return ancoraDireta;
1248
+ }
1249
+
1250
+ for (const impl of superficie.implementacoesExternas) {
1251
+ const resolvido = mapaImpl.get(impl.caminho);
1252
+ if (resolvido) {
1253
+ return {
1254
+ arquivo: resolvido.arquivo,
1255
+ simbolo: resolvido.simbolo,
1256
+ confianca: "alta",
1257
+ };
1258
+ }
1259
+ }
1260
+
1261
+ if (!superficie.task) {
1262
+ return undefined;
1263
+ }
1264
+
1265
+ const taskAssociada = ir.tasks.find((task) => task.nome === superficie.task);
1266
+ if (!taskAssociada) {
1267
+ return undefined;
1268
+ }
1269
+
1270
+ const ancoraTask = resolverArquivoOuSimboloAncora(taskAssociada.vinculos, simbolos, mapaImpl, arquivos);
1271
+ if (ancoraTask) {
1272
+ return ancoraTask;
1273
+ }
1274
+
1275
+ for (const impl of taskAssociada.implementacoesExternas) {
1276
+ const resolvido = mapaImpl.get(impl.caminho);
1277
+ if (resolvido) {
1278
+ return {
1279
+ arquivo: resolvido.arquivo,
1280
+ simbolo: resolvido.simbolo,
1281
+ confianca: "alta",
1282
+ };
1283
+ }
1284
+ }
1285
+
1286
+ return undefined;
1287
+ }
1288
+
1289
+ function calcularRiscoOperacional(task: IrTask): NivelRiscoSemantico {
1290
+ const dadosSensiveis = Boolean(
1291
+ task.dados.classificacaoPadrao && ["pii", "financeiro", "credencial", "segredo"].includes(task.dados.classificacaoPadrao)
1292
+ || task.dados.campos.some((campo) => ["pii", "financeiro", "credencial", "segredo"].includes(campo.classificacao))
1293
+ );
1294
+ const efeitoPrivilegiado = task.efeitosEstruturados.some((efeito) =>
1295
+ ["db.read", "db.write", "queue.publish", "queue.consume", "fs.read", "fs.write", "network.egress", "secret.read", "shell.exec"].includes(efeito.categoria)
1296
+ || ["alta", "critica"].includes(efeito.criticidade ?? ""),
1297
+ );
1298
+ if (
1299
+ task.execucao.criticidadeOperacional === "alta"
1300
+ || task.execucao.criticidadeOperacional === "critica"
1301
+ || dadosSensiveis
1302
+ || efeitoPrivilegiado
1303
+ || task.efeitosEstruturados.some((efeito) => efeito.categoria === "persistencia" || efeito.criticidade === "critica")
1304
+ ) {
1305
+ return "alto";
1306
+ }
1307
+
1308
+ if (task.efeitosEstruturados.length > 0 || task.vinculos.length > 0 || task.errosDetalhados.length > 0) {
1309
+ return "medio";
1310
+ }
1311
+
1312
+ return "baixo";
1313
+ }
1314
+
1315
+ function calcularConfiancaTask(
1316
+ task: IrTask,
1317
+ implsValidos: number,
1318
+ implsQuebrados: number,
1319
+ vinculosValidos: number,
1320
+ vinculosQuebrados: number,
1321
+ ): NivelConfiancaSemantica {
1322
+ if ((implsValidos > 0 || vinculosValidos > 0) && implsQuebrados === 0 && vinculosQuebrados === 0) {
1323
+ return "alta";
1324
+ }
1325
+ if (implsValidos > 0 || vinculosValidos > 0 || task.implementacoesExternas.length > 0 || task.vinculos.length > 0) {
1326
+ return "media";
1327
+ }
1328
+ return "baixa";
1329
+ }
1330
+
1331
+ function calcularScoreTask(
1332
+ task: IrTask,
1333
+ implsValidos: number,
1334
+ implsQuebrados: number,
1335
+ vinculosValidos: number,
1336
+ vinculosQuebrados: number,
1337
+ semImplementacao: boolean,
1338
+ ): number {
1339
+ let score = 45;
1340
+ if (!semImplementacao && task.implementacoesExternas.length > 0) {
1341
+ score += 15;
1342
+ }
1343
+ score += Math.min(implsValidos * 10, 20);
1344
+ score -= Math.min(implsQuebrados * 20, 30);
1345
+ score += Math.min(vinculosValidos * 5, 15);
1346
+ score -= Math.min(vinculosQuebrados * 10, 20);
1347
+ if (task.guarantees.length > 0) {
1348
+ score += 5;
1349
+ }
1350
+ if (task.execucao.explicita) {
1351
+ score += 5;
1352
+ }
1353
+ return Math.max(0, Math.min(100, score));
1354
+ }
1355
+
1356
+ function resumirLacunasTask(
1357
+ task: IrTask,
1358
+ semImplementacao: boolean,
1359
+ implsQuebrados: number,
1360
+ vinculosQuebrados: number,
1361
+ guardrails: {
1362
+ publica: boolean;
1363
+ sensivel: boolean;
1364
+ auth: boolean;
1365
+ authz: boolean;
1366
+ dados: boolean;
1367
+ audit: boolean;
1368
+ segredos: boolean;
1369
+ forbidden: boolean;
1370
+ dadosSensiveis: boolean;
1371
+ efeitoPrivilegiado: boolean;
1372
+ exigeSegredos: boolean;
1373
+ },
1374
+ ): string[] {
1375
+ const lacunas: string[] = [];
1376
+ if (semImplementacao) {
1377
+ lacunas.push("sem_impl");
1378
+ }
1379
+ if (implsQuebrados > 0) {
1380
+ lacunas.push("impl_quebrado");
1381
+ }
1382
+ if (task.vinculos.length === 0) {
1383
+ lacunas.push("sem_vinculos");
1384
+ }
1385
+ if (vinculosQuebrados > 0) {
1386
+ lacunas.push("vinculo_quebrado");
1387
+ }
1388
+ if (!task.execucao.explicita) {
1389
+ lacunas.push("execucao_implicita");
1390
+ }
1391
+ if (guardrails.publica && !task.execucao.explicita) {
1392
+ lacunas.push("superficie_publica_sem_execucao");
1393
+ }
1394
+ if (guardrails.sensivel && !task.execucao.explicita) {
1395
+ lacunas.push("execucao_critica_sem_bloco");
1396
+ }
1397
+ if ((guardrails.publica || guardrails.sensivel) && semImplementacao && task.vinculos.length === 0) {
1398
+ lacunas.push("rastreabilidade_fraca");
1399
+ }
1400
+ if (guardrails.publica && !guardrails.auth) {
1401
+ lacunas.push("auth_ausente");
1402
+ }
1403
+ if ((guardrails.publica || guardrails.sensivel || guardrails.efeitoPrivilegiado || guardrails.dadosSensiveis) && !guardrails.authz) {
1404
+ lacunas.push("authz_frouxa");
1405
+ }
1406
+ if ((guardrails.publica || guardrails.sensivel || guardrails.efeitoPrivilegiado) && !guardrails.dados) {
1407
+ lacunas.push("dados_nao_classificados");
1408
+ }
1409
+ if ((guardrails.publica || guardrails.sensivel || guardrails.efeitoPrivilegiado || guardrails.dadosSensiveis) && !guardrails.audit) {
1410
+ lacunas.push("audit_ausente");
1411
+ }
1412
+ if (guardrails.exigeSegredos && !guardrails.segredos) {
1413
+ lacunas.push("segredo_sem_governanca");
1414
+ }
1415
+ if ((guardrails.efeitoPrivilegiado || guardrails.dadosSensiveis) && !guardrails.forbidden) {
1416
+ lacunas.push("proibicoes_ausentes");
1417
+ }
1418
+ return lacunas;
1419
+ }
1420
+
1421
+ function resumirOperacional(resultado: Omit<ResultadoDrift, "comando" | "sucesso">): ResultadoDrift["resumo_operacional"] {
1422
+ const scoreMedio = resultado.tasks.length > 0
1423
+ ? Math.round(resultado.tasks.reduce((total, task) => total + task.scoreSemantico, 0) / resultado.tasks.length)
1424
+ : 0;
1425
+ const confiancaGeral: NivelConfiancaSemantica = scoreMedio >= 80 ? "alta" : scoreMedio >= 55 ? "media" : "baixa";
1426
+ const riscosPrincipais = [...new Set([
1427
+ ...resultado.tasks.filter((task) => task.riscoOperacional !== "baixo").map((task) => `${task.task}:${task.riscoOperacional}`),
1428
+ ...resultado.persistencia_real
1429
+ .filter((item) => item.status !== "materializado")
1430
+ .map((item) => `${item.task}:${item.alvo}:persistencia_${item.status}`),
1431
+ ])];
1432
+ const oQueTocar = [...new Set([
1433
+ ...resultado.tasks.flatMap((task) => task.arquivosProvaveisEditar),
1434
+ ...resultado.persistencia_real.flatMap((item) => [...item.arquivos, ...item.repositorios]),
1435
+ ])].slice(0, 20);
1436
+ const oQueValidar = [...new Set([
1437
+ ...resultado.tasks.flatMap((task) => task.checksSugeridos),
1438
+ ...resultado.persistencia_real
1439
+ .filter((item) => item.status !== "materializado")
1440
+ .map((item) => `validar persistencia real de ${item.task} em ${item.alvo}`),
1441
+ ])];
1442
+ const oQueEstaFrouxo = [...new Set([
1443
+ ...resultado.tasks.flatMap((task) => task.lacunas),
1444
+ ...resultado.persistencia_real
1445
+ .filter((item) => item.status !== "materializado" || item.compatibilidade === "desconhecida" || item.compatibilidade === "invalido")
1446
+ .map((item) => `persistencia:${item.alvo}:${item.status}:${item.compatibilidade}`),
1447
+ ])];
1448
+ const oQueFoiInferido = [
1449
+ ...new Set([
1450
+ ...resultado.impls_quebrados.flatMap((impl) => impl.candidatos?.map((candidato) => candidato.caminho) ?? []),
1451
+ ...resultado.vinculos_quebrados.filter((vinculo) => vinculo.status === "parcial").map((vinculo) => `${vinculo.dono}:${vinculo.valor}`),
1452
+ ...resultado.persistencia_real
1453
+ .filter((item) => item.compatibilidade === "desconhecida")
1454
+ .map((item) => `${item.task}:${item.alvo}:compatibilidade_nao_confirmada`),
1455
+ ]),
1456
+ ];
1457
+
1458
+ return {
1459
+ scoreMedio,
1460
+ confiancaGeral,
1461
+ riscosPrincipais,
1462
+ oQueTocar,
1463
+ oQueValidar,
1464
+ oQueEstaFrouxo,
1465
+ oQueFoiInferido,
1466
+ };
1467
+ }
1468
+
1469
+ function paraIdentificadorModulo(valor: string): string {
1470
+ return valor
1471
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
1472
+ .replace(/[^A-Za-z0-9_]+/g, "_")
1473
+ .replace(/_{2,}/g, "_")
1474
+ .replace(/^_+|_+$/g, "")
1475
+ .toLowerCase();
1476
+ }
1477
+
1478
+ function extrairTextoLiteral(expr?: ts.Expression): string | undefined {
1479
+ if (!expr) {
1480
+ return undefined;
1481
+ }
1482
+ if (ts.isStringLiteralLike(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
1483
+ return expr.text;
1484
+ }
1485
+ if (ts.isNumericLiteral(expr)) {
1486
+ return expr.text;
1487
+ }
1488
+ return undefined;
1489
+ }
1490
+
1491
+ function listarDecoradores(node: ts.Node): readonly ts.Decorator[] {
1492
+ return ts.canHaveDecorators(node) ? ts.getDecorators(node) ?? [] : [];
1493
+ }
1494
+
1495
+ function lerDecorator(node: ts.Node, nomes: string[]): { nome: string; argumentos: ts.NodeArray<ts.Expression> } | undefined {
1496
+ for (const decorator of listarDecoradores(node)) {
1497
+ const expressao = decorator.expression;
1498
+ if (ts.isCallExpression(expressao)) {
1499
+ const alvo = expressao.expression;
1500
+ if (ts.isIdentifier(alvo) && nomes.includes(alvo.text)) {
1501
+ return { nome: alvo.text, argumentos: expressao.arguments };
1502
+ }
1503
+ } else if (ts.isIdentifier(expressao) && nomes.includes(expressao.text)) {
1504
+ return { nome: expressao.text, argumentos: ts.factory.createNodeArray() };
1505
+ }
1506
+ }
1507
+ return undefined;
1508
+ }
1509
+
1510
+ function juntarCaminhoHttp(base: string | undefined, sufixo: string | undefined): string {
1511
+ const partes = [base ?? "", sufixo ?? ""]
1512
+ .map((item) => item.trim())
1513
+ .filter(Boolean)
1514
+ .map((item) => item.replace(/^\/+|\/+$/g, ""));
1515
+
1516
+ const caminho = `/${partes.join("/")}`.replace(/\/+/g, "/");
1517
+ return caminho === "//" ? "/" : caminho;
1518
+ }
1519
+
1520
+ async function listarArquivosRecursivos(diretorio: string, extensoes: string[]): Promise<string[]> {
1521
+ let entradas;
1522
+ try {
1523
+ entradas = await readdir(diretorio, { withFileTypes: true, encoding: "utf8" });
1524
+ } catch {
1525
+ return [];
1526
+ }
1527
+
1528
+ const encontrados: string[] = [];
1529
+ for (const entrada of entradas) {
1530
+ if (diretoriosIgnoradosAtivos.has(entrada.name.toLowerCase())) {
1531
+ continue;
1532
+ }
1533
+ const caminhoAtual = path.join(diretorio, entrada.name);
1534
+ if (entrada.isDirectory()) {
1535
+ encontrados.push(...await listarArquivosRecursivos(caminhoAtual, extensoes));
1536
+ continue;
1537
+ }
1538
+ if (extensoes.some((extensao) => entrada.name.endsWith(extensao))) {
1539
+ encontrados.push(caminhoAtual);
1540
+ }
1541
+ }
1542
+
1543
+ return encontrados.sort((a, b) => a.localeCompare(b, "pt-BR"));
1544
+ }
1545
+
1546
+ const EXTENSOES_ARQUIVOS_CODIGO_DRIFT = [
1547
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
1548
+ ".py", ".dart", ".lua", ".php", ".cs", ".java", ".go", ".rs", ".cpp", ".cc", ".cxx", ".hpp", ".h",
1549
+ ".sql", ".psql", ".ddl", ".prisma", ".json",
1550
+ ];
1551
+
1552
+ async function indexarArquivosCodigo(diretorios: string[]): Promise<string[]> {
1553
+ const arquivos = new Set<string>();
1554
+ for (const diretorio of diretorios) {
1555
+ for (const arquivo of await listarArquivosRecursivos(diretorio, EXTENSOES_ARQUIVOS_CODIGO_DRIFT)) {
1556
+ arquivos.add(arquivo);
1557
+ }
1558
+ }
1559
+ return [...arquivos].sort((a, b) => a.localeCompare(b, "pt-BR"));
1560
+ }
1561
+
1562
+ function caminhosSimbolicos(baseDiretorio: string, arquivo: string): string[] {
1563
+ const relativo = path.relative(baseDiretorio, arquivo).replace(/\.[^.]+$/, "");
1564
+ const semPrefixo = relativo
1565
+ .split(path.sep)
1566
+ .map((segmento) => paraIdentificadorModulo(segmento))
1567
+ .filter(Boolean)
1568
+ .join(".");
1569
+ const prefixo = paraIdentificadorModulo(path.basename(baseDiretorio));
1570
+ const comPrefixo = prefixo ? [prefixo, semPrefixo].filter(Boolean).join(".") : semPrefixo;
1571
+ return [...new Set([semPrefixo, comPrefixo].filter(Boolean))];
1572
+ }
1573
+
1574
+ function registrarSimboloTypeScript(
1575
+ simbolos: Map<string, SimboloResolvido>,
1576
+ basesSimbolicas: string[],
1577
+ arquivo: string,
1578
+ nome: string,
1579
+ nomeClasse?: string,
1580
+ ): void {
1581
+ for (const baseSimbolica of basesSimbolicas) {
1582
+ const caminho = nomeClasse
1583
+ ? `${baseSimbolica}.${nomeClasse}.${nome}`
1584
+ : `${baseSimbolica}.${nome}`;
1585
+ simbolos.set(caminho, {
1586
+ origem: "ts",
1587
+ caminho,
1588
+ arquivo,
1589
+ simbolo: nomeClasse ? `${nomeClasse}.${nome}` : nome,
1590
+ });
1591
+ }
1592
+ }
1593
+
1594
+ function desembrulharExpressaoTypeScript(expr: ts.Expression): ts.Expression {
1595
+ let atual = expr;
1596
+ while (true) {
1597
+ if (ts.isParenthesizedExpression(atual) || ts.isAsExpression(atual) || ts.isSatisfiesExpression(atual) || ts.isTypeAssertionExpression(atual)) {
1598
+ atual = atual.expression;
1599
+ continue;
1600
+ }
1601
+ if (ts.isAwaitExpression(atual)) {
1602
+ atual = atual.expression;
1603
+ continue;
1604
+ }
1605
+ return atual;
1606
+ }
1607
+ }
1608
+
1609
+ function extrairNomePropriedadeTypeScript(nome: ts.PropertyName, sourceFile: ts.SourceFile): string | undefined {
1610
+ if (ts.isIdentifier(nome) || ts.isStringLiteralLike(nome) || ts.isNumericLiteral(nome)) {
1611
+ return nome.text;
1612
+ }
1613
+ if (ts.isComputedPropertyName(nome) && ts.isStringLiteralLike(nome.expression)) {
1614
+ return nome.expression.text;
1615
+ }
1616
+ const texto = nome.getText(sourceFile).trim();
1617
+ return texto.length > 0 ? texto : undefined;
1618
+ }
1619
+
1620
+ function extrairNomeClassePrototypeTypeScript(expr: ts.Expression, sourceFile: ts.SourceFile): string | undefined {
1621
+ const alvo = desembrulharExpressaoTypeScript(expr);
1622
+ if (ts.isPropertyAccessExpression(alvo) && alvo.name.text === "prototype") {
1623
+ return alvo.expression.getText(sourceFile).trim() || undefined;
1624
+ }
1625
+ return undefined;
1626
+ }
1627
+
1628
+ function registrarMetodoTypeScriptProtoOuObjeto(
1629
+ simbolos: Map<string, SimboloResolvido>,
1630
+ basesSimbolicas: string[],
1631
+ arquivo: string,
1632
+ nomeMetodo: string,
1633
+ nomeClasse?: string,
1634
+ ): void {
1635
+ if (!nomeMetodo) {
1636
+ return;
1637
+ }
1638
+ if (nomeClasse) {
1639
+ registrarSimboloTypeScript(simbolos, basesSimbolicas, arquivo, nomeMetodo, nomeClasse);
1640
+ }
1641
+ registrarSimboloTypeScript(simbolos, basesSimbolicas, arquivo, nomeMetodo);
1642
+ }
1643
+
1644
+ function registrarMetodosObjectAssignTypeScript(
1645
+ simbolos: Map<string, SimboloResolvido>,
1646
+ basesSimbolicas: string[],
1647
+ arquivo: string,
1648
+ objeto: ts.ObjectLiteralExpression,
1649
+ sourceFile: ts.SourceFile,
1650
+ nomeClasse?: string,
1651
+ ): void {
1652
+ for (const propriedade of objeto.properties) {
1653
+ if (ts.isMethodDeclaration(propriedade) && propriedade.name) {
1654
+ const nomeMetodo = extrairNomePropriedadeTypeScript(propriedade.name, sourceFile);
1655
+ if (nomeMetodo) {
1656
+ registrarMetodoTypeScriptProtoOuObjeto(simbolos, basesSimbolicas, arquivo, nomeMetodo, nomeClasse);
1657
+ }
1658
+ continue;
1659
+ }
1660
+
1661
+ if (!ts.isPropertyAssignment(propriedade)) {
1662
+ continue;
1663
+ }
1664
+
1665
+ const nomeMetodo = extrairNomePropriedadeTypeScript(propriedade.name, sourceFile);
1666
+ const valor = desembrulharExpressaoTypeScript(propriedade.initializer);
1667
+ if (nomeMetodo && (ts.isFunctionExpression(valor) || ts.isArrowFunction(valor))) {
1668
+ registrarMetodoTypeScriptProtoOuObjeto(simbolos, basesSimbolicas, arquivo, nomeMetodo, nomeClasse);
1669
+ }
1670
+ }
1671
+ }
1672
+
1673
+ function registrarAtribuicaoPrototypeTypeScript(
1674
+ simbolos: Map<string, SimboloResolvido>,
1675
+ basesSimbolicas: string[],
1676
+ arquivo: string,
1677
+ sourceFile: ts.SourceFile,
1678
+ esquerda: ts.Expression,
1679
+ direita: ts.Expression,
1680
+ ): void {
1681
+ const alvo = desembrulharExpressaoTypeScript(esquerda);
1682
+ const valor = desembrulharExpressaoTypeScript(direita);
1683
+ if (!ts.isPropertyAccessExpression(alvo) || !ts.isPropertyAccessExpression(alvo.expression)) {
1684
+ return;
1685
+ }
1686
+ if (alvo.expression.name.text !== "prototype") {
1687
+ return;
1688
+ }
1689
+ if (!ts.isFunctionExpression(valor) && !ts.isArrowFunction(valor)) {
1690
+ return;
1691
+ }
1692
+
1693
+ const nomeClasse = alvo.expression.expression.getText(sourceFile).trim();
1694
+ const nomeMetodo = alvo.name.getText(sourceFile).trim();
1695
+ registrarMetodoTypeScriptProtoOuObjeto(simbolos, basesSimbolicas, arquivo, nomeMetodo, nomeClasse || undefined);
1696
+ }
1697
+
1698
+ function normalizarRelacaoConsumer(relacaoArquivo: string): string {
1699
+ return relacaoArquivo.replace(/\\/g, "/");
1700
+ }
1701
+
1702
+ function normalizarSegmentoRotaConsumer(segmento: string): string {
1703
+ const opcionalCatchAll = segmento.match(/^\[\[\.\.\.([A-Za-z_]\w*)\]\]$/);
1704
+ if (opcionalCatchAll) {
1705
+ return `{${opcionalCatchAll[1]}}`;
1706
+ }
1707
+ const catchAll = segmento.match(/^\[\.\.\.([A-Za-z_]\w*)\]$/);
1708
+ if (catchAll) {
1709
+ return `{${catchAll[1]}}`;
1710
+ }
1711
+ const dinamico = segmento.match(/^\[([A-Za-z_]\w*)\]$/);
1712
+ if (dinamico) {
1713
+ return `{${dinamico[1]}}`;
1714
+ }
1715
+ return segmento;
1716
+ }
1717
+
1718
+ function montarRotaConsumer(partes: string[]): string {
1719
+ const filtradas = partes
1720
+ .filter((segmento) => segmento && segmento !== "index" && !/^\(.*\)$/.test(segmento) && !segmento.startsWith("@"))
1721
+ .map(normalizarSegmentoRotaConsumer);
1722
+ return filtradas.length > 0 ? `/${filtradas.join("/")}`.replace(/\/+/g, "/") : "/";
1723
+ }
1724
+
1725
+ function arquivoEhBridgeNextJsConsumer(relacaoArquivo: string): boolean {
1726
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1727
+ return /(?:^|\/)(?:src\/)?lib\/(?:sema_consumer_bridge|sema\/.+)\.(?:ts|tsx|js|jsx)$/i.test(relacao);
1728
+ }
1729
+
1730
+ function arquivoEhBridgeReactViteConsumer(relacaoArquivo: string): boolean {
1731
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1732
+ return /(?:^|\/)(?:src\/)?lib\/(?:sema_consumer_bridge|sema\/.+)\.(?:ts|tsx|js|jsx)$/i.test(relacao);
1733
+ }
1734
+
1735
+ function arquivoEhBridgeAngularConsumer(relacaoArquivo: string): boolean {
1736
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1737
+ return /(?:^|\/)(?:src\/)?app\/(?:sema_consumer_bridge|sema\/.+)\.(?:ts|js)$/i.test(relacao);
1738
+ }
1739
+
1740
+ function arquivoEhSuperficieNextJsConsumer(relacaoArquivo: string): boolean {
1741
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1742
+ return /(?:^|\/)(?:src\/)?app\/(?:(?!api\/).)*?(?:page|layout|loading|error)\.(?:ts|tsx|js|jsx)$/i.test(relacao);
1743
+ }
1744
+
1745
+ function arquivoEhSuperficieReactViteConsumer(relacaoArquivo: string): boolean {
1746
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1747
+ return /^(?:src\/)?pages\/.+\.(?:ts|tsx|js|jsx)$/i.test(relacao)
1748
+ || /^(?:src\/)?App\.(?:ts|tsx|js|jsx)$/i.test(relacao);
1749
+ }
1750
+
1751
+ function arquivoEhRotasReactViteConsumer(relacaoArquivo: string, codigo?: string): boolean {
1752
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1753
+ return /(?:^|\/)(?:src\/)?(?:app\/)?(?:router|routes)\.(?:ts|tsx|js|jsx)$/i.test(relacao)
1754
+ || /from\s+["']react-router-dom["']|createBrowserRouter|RouterProvider|useRoutes\s*\(|<Routes\b|<Route\b/.test(codigo ?? "");
1755
+ }
1756
+
1757
+ function arquivoEhRotasAngularConsumer(relacaoArquivo: string): boolean {
1758
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1759
+ return /(?:^|\/)(?:src\/)?app(?:\/.+)?\/[^/]+\.routes\.(?:ts|js)$/i.test(relacao);
1760
+ }
1761
+
1762
+ function arquivoEhRotasAngularConsumerRaiz(relacaoArquivo: string): boolean {
1763
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1764
+ return /(?:^|\/)(?:src\/)?app\/[^/]+\.routes\.(?:ts|js)$/i.test(relacao);
1765
+ }
1766
+
1767
+ function arquivoEhBridgeFlutterConsumer(relacaoArquivo: string): boolean {
1768
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1769
+ return /(?:^|\/)(?:lib\/)?(?:sema_consumer_bridge|api\/sema_contract_bridge|sema\/.+)\.dart$/i.test(relacao);
1770
+ }
1771
+
1772
+ function arquivoEhSuperficieFlutterConsumer(relacaoArquivo: string): boolean {
1773
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1774
+ return /(?:^|\/)(?:lib\/)?(?:screens|pages)\/.+\.dart$/i.test(relacao)
1775
+ || /(?:^|\/)(?:lib\/)?main\.dart$/i.test(relacao);
1776
+ }
1777
+
1778
+ function arquivoEhRotasFlutterConsumer(relacaoArquivo: string, codigo?: string): boolean {
1779
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1780
+ return /(?:^|\/)(?:lib\/)?(?:router|app_router|routes)\.dart$/i.test(relacao)
1781
+ || /MaterialApp(?:\.router)?\s*\(|CupertinoApp(?:\.router)?\s*\(|GoRouter\s*\(/.test(codigo ?? "");
1782
+ }
1783
+
1784
+ function inferirRotaNextJsConsumer(relacaoArquivo: string): RegistroConsumerSurfaceDrift | undefined {
1785
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1786
+ const segmentos = relacao.split("/");
1787
+ const indiceSrcApp = segmentos.findIndex((segmento, indice) => segmento === "src" && segmentos[indice + 1] === "app");
1788
+ const indiceApp = segmentos.findIndex((segmento) => segmento === "app");
1789
+ const inicioApp = indiceSrcApp >= 0 ? indiceSrcApp + 2 : indiceApp >= 0 ? indiceApp + 1 : -1;
1790
+ if (inicioApp < 0) {
1791
+ return undefined;
1792
+ }
1793
+
1794
+ const arquivoFinal = segmentos.at(-1) ?? "";
1795
+ const tipoArquivo = arquivoFinal.match(/^(page|layout|loading|error)\.(?:ts|tsx|js|jsx)$/)?.[1] as RegistroConsumerSurfaceDrift["tipoArquivo"] | undefined;
1796
+ if (!tipoArquivo) {
1797
+ return undefined;
1798
+ }
1799
+
1800
+ const caminhoAteArquivo = segmentos.slice(inicioApp, -1);
1801
+ if (caminhoAteArquivo[0] === "api") {
1802
+ return undefined;
1803
+ }
1804
+
1805
+ return {
1806
+ rota: montarRotaConsumer(caminhoAteArquivo),
1807
+ arquivo: relacaoArquivo,
1808
+ tipoArquivo,
1809
+ };
1810
+ }
1811
+
1812
+ function inferirRotaReactViteConsumer(relacaoArquivo: string): RegistroConsumerSurfaceDrift | undefined {
1813
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1814
+ if (/(?:^|\/)(?:src\/)?App\.(?:ts|tsx|js|jsx)$/i.test(relacao)) {
1815
+ return {
1816
+ rota: "/",
1817
+ arquivo: relacaoArquivo,
1818
+ tipoArquivo: "app",
1819
+ };
1820
+ }
1821
+
1822
+ const segmentos = relacao.split("/");
1823
+ const indiceSrcPages = segmentos.findIndex((segmento, indice) => segmento === "src" && segmentos[indice + 1] === "pages");
1824
+ const indicePages = segmentos.findIndex((segmento) => segmento === "pages");
1825
+ const inicioPages = indiceSrcPages >= 0 ? indiceSrcPages + 2 : indicePages >= 0 ? indicePages + 1 : -1;
1826
+ if (inicioPages < 0) {
1827
+ return undefined;
1828
+ }
1829
+
1830
+ const arquivoFinal = segmentos.at(-1) ?? "";
1831
+ const nomeBase = arquivoFinal.replace(/\.(?:ts|tsx|js|jsx)$/i, "");
1832
+ return {
1833
+ rota: montarRotaConsumer([...segmentos.slice(inicioPages, -1), nomeBase]),
1834
+ arquivo: relacaoArquivo,
1835
+ tipoArquivo: "page",
1836
+ };
1837
+ }
1838
+
1839
+ function inferirRotaFlutterConsumer(relacaoArquivo: string): RegistroConsumerSurfaceDrift | undefined {
1840
+ const relacao = normalizarRelacaoConsumer(relacaoArquivo);
1841
+ if (!arquivoEhSuperficieFlutterConsumer(relacao)) {
1842
+ return undefined;
1843
+ }
1844
+ if (/(?:^|\/)(?:lib\/)?main\.dart$/i.test(relacao)) {
1845
+ return {
1846
+ rota: "/",
1847
+ arquivo: relacaoArquivo,
1848
+ tipoArquivo: "app",
1849
+ };
1850
+ }
1851
+
1852
+ const segmentos = relacao.split("/");
1853
+ const indiceLibScreens = segmentos.findIndex((segmento, indice) => segmento === "lib" && ["screens", "pages"].includes(segmentos[indice + 1] ?? ""));
1854
+ const indiceScreens = segmentos.findIndex((segmento) => segmento === "screens" || segmento === "pages");
1855
+ const inicio = indiceLibScreens >= 0 ? indiceLibScreens + 2 : indiceScreens >= 0 ? indiceScreens + 1 : -1;
1856
+ if (inicio < 0) {
1857
+ return undefined;
1858
+ }
1859
+
1860
+ const arquivoFinal = segmentos.at(-1) ?? "";
1861
+ const nomeBase = arquivoFinal
1862
+ .replace(/\.(?:dart)$/i, "")
1863
+ .replace(/_(screen|page)$/i, "");
1864
+ return {
1865
+ rota: montarRotaConsumer([...segmentos.slice(inicio, -1), nomeBase]),
1866
+ arquivo: relacaoArquivo,
1867
+ tipoArquivo: "screen",
1868
+ };
1869
+ }
1870
+
1871
+ interface RotaReactViteConsumerDrift {
1872
+ rota: string;
1873
+ arquivoRotas: string;
1874
+ arquivoComponente?: string;
1875
+ }
1876
+
1877
+ interface RotaFlutterConsumerDrift {
1878
+ rota: string;
1879
+ arquivoRotas: string;
1880
+ }
1881
+
1882
+ interface RotaAngularConsumerDrift {
1883
+ rota: string;
1884
+ arquivoRotas: string;
1885
+ componente?: string;
1886
+ arquivoComponente?: string;
1887
+ arquivoRotasFilhas?: string;
1888
+ }
1889
+
1890
+ function normalizarRotaDeclaradaConsumer(caminhoCru: string, prefixo = "/"): string {
1891
+ const partesPrefixo = prefixo.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
1892
+ const partesCaminho = (caminhoCru ?? "").trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
1893
+ return montarRotaConsumer([...partesPrefixo, ...partesCaminho]);
1894
+ }
1895
+
1896
+ function resolverImportRelativoConsumer(relacaoArquivoBase: string, especificador: string): string | undefined {
1897
+ if (!especificador.startsWith(".")) {
1898
+ return undefined;
1899
+ }
1900
+ const baseDir = path.posix.dirname(normalizarRelacaoConsumer(relacaoArquivoBase));
1901
+ for (const sufixo of ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js"]) {
1902
+ const candidato = path.posix.normalize(path.posix.join(baseDir, `${especificador}${sufixo}`));
1903
+ if (/\.(?:ts|tsx|js|jsx)$/i.test(candidato)) {
1904
+ return candidato;
1905
+ }
1906
+ }
1907
+ return undefined;
1908
+ }
1909
+
1910
+ function extrairImportsTypeScriptConsumer(relacaoArquivo: string, codigo: string): Map<string, string> {
1911
+ const imports = new Map<string, string>();
1912
+ for (const match of codigo.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*["']([^"']+)["']/g)) {
1913
+ const arquivoImportado = resolverImportRelativoConsumer(relacaoArquivo, match[2]);
1914
+ if (!arquivoImportado) {
1915
+ continue;
1916
+ }
1917
+ for (const bruto of match[1].split(",")) {
1918
+ const local = bruto.trim().split(/\s+as\s+/i).at(-1)?.trim();
1919
+ if (local) {
1920
+ imports.set(local, arquivoImportado);
1921
+ }
1922
+ }
1923
+ }
1924
+ for (const match of codigo.matchAll(/import\s+([A-Za-z_]\w*)\s+from\s*["']([^"']+)["']/g)) {
1925
+ const arquivoImportado = resolverImportRelativoConsumer(relacaoArquivo, match[2]);
1926
+ const local = match[1]?.trim();
1927
+ if (arquivoImportado && local) {
1928
+ imports.set(local, arquivoImportado);
1929
+ }
1930
+ }
1931
+ return imports;
1932
+ }
1933
+
1934
+ function extrairRotasReactViteConsumer(relacaoArquivo: string, codigo: string): RotaReactViteConsumerDrift[] {
1935
+ const imports = extrairImportsTypeScriptConsumer(relacaoArquivo, codigo);
1936
+ const rotas = new Map<string, RotaReactViteConsumerDrift>();
1937
+ const registrar = (caminhoCru: string, componente?: string) => {
1938
+ const rota = normalizarRotaDeclaradaConsumer(caminhoCru);
1939
+ const chave = `${rota}:${normalizarRelacaoConsumer(relacaoArquivo)}:${componente ?? "router"}`;
1940
+ rotas.set(chave, {
1941
+ rota,
1942
+ arquivoRotas: normalizarRelacaoConsumer(relacaoArquivo),
1943
+ arquivoComponente: componente ? imports.get(componente) : undefined,
1944
+ });
1945
+ };
1946
+
1947
+ for (const match of codigo.matchAll(/(?:path\s*:\s*["'`]([^"'`]*)["'`]|index\s*:\s*true)[\s\S]{0,260}?(?:element\s*:\s*<\s*([A-Za-z_]\w*)|Component\s*:\s*([A-Za-z_]\w*))/g)) {
1948
+ const caminhoCru = match[1] ?? "";
1949
+ const componente = match[2] ?? match[3];
1950
+ registrar(caminhoCru, componente);
1951
+ }
1952
+
1953
+ for (const match of codigo.matchAll(/<Route\b[^>]*?(?:path=["'`]([^"'`]*)["'`][^>]*?)?(index\b)?[^>]*?(?:element=\{\s*<\s*([A-Za-z_]\w*)|Component=\{\s*([A-Za-z_]\w*))/g)) {
1954
+ const caminhoCru = match[2] ? "" : (match[1] ?? "");
1955
+ const componente = match[3] ?? match[4];
1956
+ registrar(caminhoCru, componente);
1957
+ }
1958
+
1959
+ return [...rotas.values()];
1960
+ }
1961
+
1962
+ function normalizarRotaDeclaradaFlutter(caminhoCru: string): string {
1963
+ return montarRotaConsumer((caminhoCru ?? "").trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean));
1964
+ }
1965
+
1966
+ function extrairRotasFlutterConsumer(relacaoArquivo: string, codigo: string): RotaFlutterConsumerDrift[] {
1967
+ const rotas = new Map<string, RotaFlutterConsumerDrift>();
1968
+ const registrar = (caminhoCru: string) => {
1969
+ const rota = normalizarRotaDeclaradaFlutter(caminhoCru);
1970
+ rotas.set(`${rota}:${normalizarRelacaoConsumer(relacaoArquivo)}`, {
1971
+ rota,
1972
+ arquivoRotas: normalizarRelacaoConsumer(relacaoArquivo),
1973
+ });
1974
+ };
1975
+
1976
+ for (const match of codigo.matchAll(/GoRoute\s*\([\s\S]{0,220}?path\s*:\s*["'`]([^"'`]+)["'`]/g)) {
1977
+ registrar(match[1] ?? "");
1978
+ }
1979
+
1980
+ for (const match of codigo.matchAll(/["'`]([^"'`]+)["'`]\s*:\s*\([^)]*\)\s*=>/g)) {
1981
+ registrar(match[1] ?? "");
1982
+ }
1983
+
1984
+ if (/home\s*:\s*(?:const\s+)?[A-Za-z_]\w*\(/.test(codigo)) {
1985
+ registrar("/");
1986
+ }
1987
+
1988
+ return [...rotas.values()];
1989
+ }
1990
+
1991
+ function extrairRotasAngularConsumerDiretas(relacaoArquivo: string, codigo: string, prefixo = "/"): RotaAngularConsumerDrift[] {
1992
+ const imports = extrairImportsTypeScriptConsumer(relacaoArquivo, codigo);
1993
+
1994
+ const rotas: RotaAngularConsumerDrift[] = [];
1995
+ for (const match of codigo.matchAll(/path\s*:\s*["'`]([^"'`]*)["'`][\s\S]{0,320}?component\s*:\s*([A-Za-z_]\w*)/g)) {
1996
+ const caminhoCru = (match[1] ?? "").trim();
1997
+ const componente = match[2];
1998
+ rotas.push({
1999
+ rota: normalizarRotaDeclaradaConsumer(caminhoCru, prefixo),
2000
+ arquivoRotas: normalizarRelacaoConsumer(relacaoArquivo),
2001
+ componente,
2002
+ arquivoComponente: imports.get(componente),
2003
+ });
2004
+ }
2005
+
2006
+ for (const match of codigo.matchAll(/path\s*:\s*["'`]([^"'`]*)["'`][\s\S]{0,320}?loadComponent\s*:\s*\(\s*\)\s*=>\s*import\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
2007
+ const caminhoCru = (match[1] ?? "").trim();
2008
+ const arquivoComponente = resolverImportRelativoConsumer(relacaoArquivo, match[2] ?? "");
2009
+ rotas.push({
2010
+ rota: normalizarRotaDeclaradaConsumer(caminhoCru, prefixo),
2011
+ arquivoRotas: normalizarRelacaoConsumer(relacaoArquivo),
2012
+ arquivoComponente,
2013
+ });
2014
+ }
2015
+
2016
+ for (const match of codigo.matchAll(/path\s*:\s*["'`]([^"'`]*)["'`][\s\S]{0,360}?loadChildren\s*:\s*\(\s*\)\s*=>\s*import\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
2017
+ const caminhoCru = (match[1] ?? "").trim();
2018
+ const arquivoRotasFilhas = resolverImportRelativoConsumer(relacaoArquivo, match[2] ?? "");
2019
+ rotas.push({
2020
+ rota: normalizarRotaDeclaradaConsumer(caminhoCru, prefixo),
2021
+ arquivoRotas: normalizarRelacaoConsumer(relacaoArquivo),
2022
+ arquivoRotasFilhas,
2023
+ });
2024
+ }
2025
+
2026
+ return rotas;
2027
+ }
2028
+
2029
+ async function extrairRotasAngularConsumer(
2030
+ diretorioBase: string,
2031
+ relacaoArquivo: string,
2032
+ prefixo = "/",
2033
+ visitados = new Set<string>(),
2034
+ ): Promise<RotaAngularConsumerDrift[]> {
2035
+ const relacaoNormalizada = normalizarRelacaoConsumer(relacaoArquivo);
2036
+ if (visitados.has(relacaoNormalizada)) {
2037
+ return [];
2038
+ }
2039
+ visitados.add(relacaoNormalizada);
2040
+
2041
+ let codigo = "";
2042
+ try {
2043
+ codigo = await readFile(path.join(diretorioBase, relacaoNormalizada), "utf8");
2044
+ } catch {
2045
+ return [];
2046
+ }
2047
+
2048
+ const rotas = extrairRotasAngularConsumerDiretas(relacaoNormalizada, codigo, prefixo);
2049
+ const filhas: RotaAngularConsumerDrift[] = [];
2050
+ for (const rota of rotas) {
2051
+ if (!rota.arquivoRotasFilhas) {
2052
+ continue;
2053
+ }
2054
+ filhas.push(...await extrairRotasAngularConsumer(diretorioBase, rota.arquivoRotasFilhas, rota.rota, visitados));
2055
+ }
2056
+ return [...rotas, ...filhas];
2057
+ }
2058
+
2059
+ function simboloEhBridgeConsumer(caminho: string, arquivo: string): boolean {
2060
+ return arquivoEhBridgeNextJsConsumer(arquivo)
2061
+ || arquivoEhBridgeReactViteConsumer(arquivo)
2062
+ || arquivoEhBridgeAngularConsumer(arquivo)
2063
+ || arquivoEhBridgeFlutterConsumer(arquivo)
2064
+ || /(?:^|\.)(?:src\.)?lib\.(?:sema_consumer_bridge|sema\.)/i.test(caminho)
2065
+ || /(?:^|\.)(?:src\.)?app\.(?:sema_consumer_bridge|sema\.)/i.test(caminho)
2066
+ || /(?:^|\.)(?:lib\.)?(?:sema_consumer_bridge|api\.sema_contract_bridge|sema\.)/i.test(caminho);
2067
+ }
2068
+
2069
+ function inferirConsumerFrameworkPrincipal(
2070
+ fontesLegado: FonteLegado[],
2071
+ consumerSurfaces: RegistroConsumerSurfaceDrift[],
2072
+ consumerBridges: RegistroConsumerBridgeDrift[],
2073
+ ): ConsumerFramework | null {
2074
+ const arquivos = [
2075
+ ...consumerSurfaces.map((item) => item.arquivo),
2076
+ ...consumerBridges.map((item) => item.arquivo),
2077
+ ].map(normalizarRelacaoConsumer);
2078
+ if (arquivos.some((arquivo) => /(?:^|\/)(?:src\/)?app\/(?:(?!api\/).)*?(?:page|layout|loading|error)\.(?:ts|tsx|js|jsx)$/i.test(arquivo))) {
2079
+ return "nextjs-consumer";
2080
+ }
2081
+ if (arquivos.some((arquivo) =>
2082
+ /^(?:src\/)?pages\/.+\.(?:ts|tsx|js|jsx)$/i.test(arquivo)
2083
+ || /^(?:src\/)?App\.(?:ts|tsx|js|jsx)$/i.test(arquivo)
2084
+ || /(?:^|\/)(?:src\/)?(?:app\/)?(?:router|routes)\.(?:ts|tsx|js|jsx)$/i.test(arquivo))) {
2085
+ return "react-vite-consumer";
2086
+ }
2087
+ if (arquivos.some((arquivo) =>
2088
+ /(?:^|\/)(?:src\/)?app\.component\.(?:ts|js)$/i.test(arquivo)
2089
+ || /(?:^|\/)(?:src\/)?app\/.+\.component\.(?:ts|js)$/i.test(arquivo)
2090
+ || /(?:^|\/)(?:src\/)?components\/.+\.component\.(?:ts|js)$/i.test(arquivo)
2091
+ || arquivoEhRotasAngularConsumer(arquivo))) {
2092
+ return "angular-consumer";
2093
+ }
2094
+ if (arquivos.some((arquivo) =>
2095
+ /(?:^|\/)(?:lib\/)?(?:screens|pages)\/.+\.dart$/i.test(arquivo)
2096
+ || /(?:^|\/)(?:lib\/)?(?:router|app_router|routes|main)\.dart$/i.test(arquivo))) {
2097
+ return "flutter-consumer";
2098
+ }
2099
+ for (const framework of ["nextjs-consumer", "react-vite-consumer", "angular-consumer", "flutter-consumer"] as const) {
2100
+ if (fontesLegado.includes(framework)) {
2101
+ return framework;
2102
+ }
2103
+ }
2104
+ return null;
2105
+ }
2106
+
2107
+ function extrairColecoesFirebase(arquivo: string, codigo: string): RecursoResolvido[] {
2108
+ const recursos = new Map<string, RecursoResolvido>();
2109
+ const registrar = (nome: string) => {
2110
+ if (!nome) {
2111
+ return;
2112
+ }
2113
+ recursos.set(`${nome}:${arquivo}`, {
2114
+ origem: "firebase",
2115
+ nome,
2116
+ arquivo,
2117
+ tipo: "colecao",
2118
+ });
2119
+ };
2120
+
2121
+ for (const match of codigo.matchAll(/\b(?:export\s+)?const\s+\w*COLLECTIONS?\w*\s*=\s*\{([\s\S]*?)\n\}/g)) {
2122
+ const corpo = match[1] ?? "";
2123
+ for (const valor of corpo.matchAll(/:\s*["'`]([^"'`]+)["'`]/g)) {
2124
+ registrar(valor[1]!);
2125
+ }
2126
+ }
2127
+
2128
+ for (const match of codigo.matchAll(/\b(?:db\.)?collection\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
2129
+ registrar(match[1]!);
2130
+ }
2131
+
2132
+ for (const match of codigo.matchAll(/\bdoc\s*\(\s*[^,]+,\s*["'`]([^"'`]+)["'`]/g)) {
2133
+ registrar(match[1]!);
2134
+ }
2135
+
2136
+ return [...recursos.values()];
2137
+ }
2138
+
2139
+ async function indexarTypeScript(diretorios: string[]): Promise<{
2140
+ simbolos: SimboloResolvido[];
2141
+ rotas: RotaResolvida[];
2142
+ recursos: RecursoResolvido[];
2143
+ consumerSurfaces: RegistroConsumerSurfaceDrift[];
2144
+ }> {
2145
+ const simbolos = new Map<string, SimboloResolvido>();
2146
+ const rotas: RotaResolvida[] = [];
2147
+ const recursos = new Map<string, RecursoResolvido>();
2148
+ const consumerSurfaces = new Map<string, RegistroConsumerSurfaceDrift>();
2149
+
2150
+ for (const diretorio of diretorios) {
2151
+ const arquivos = (await listarArquivosRecursivos(diretorio, [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]))
2152
+ .filter((arquivo) =>
2153
+ !arquivo.endsWith(".d.ts")
2154
+ && !arquivo.endsWith(".spec.ts")
2155
+ && !arquivo.endsWith(".test.ts"),
2156
+ );
2157
+ const arquivosRotasAngular = arquivos.filter((arquivo) => arquivoEhRotasAngularConsumer(path.relative(diretorio, arquivo)));
2158
+ const arquivosRotasAngularRaiz = new Set(
2159
+ arquivosRotasAngular
2160
+ .filter((arquivo) => arquivoEhRotasAngularConsumerRaiz(path.relative(diretorio, arquivo)))
2161
+ .map((arquivo) => path.resolve(arquivo)),
2162
+ );
2163
+ const usarApenasRotasAngularRaiz = arquivosRotasAngularRaiz.size > 0;
2164
+ let encontrouSuperficieAngularPorRotas = false;
2165
+
2166
+ for (const arquivo of arquivos) {
2167
+ const codigo = await readFile(arquivo, "utf8");
2168
+ const scriptKind = arquivo.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
2169
+ const sourceFile = ts.createSourceFile(arquivo, codigo, ts.ScriptTarget.Latest, true, scriptKind);
2170
+ const basesSimbolicas = caminhosSimbolicos(diretorio, arquivo);
2171
+ const relacao = path.relative(diretorio, arquivo);
2172
+
2173
+ for (const recurso of extrairRecursosPersistenciaCodigoVivo(arquivo, codigo)) {
2174
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
2175
+ }
2176
+
2177
+ for (const rota of extrairRotasTypeScriptHttp(sourceFile, relacao)) {
2178
+ rotas.push({
2179
+ origem: rota.origem,
2180
+ metodo: rota.metodo,
2181
+ caminho: rota.caminho,
2182
+ arquivo,
2183
+ simbolo: rota.simbolo,
2184
+ });
2185
+ }
2186
+
2187
+ const superficieNextJs = arquivoEhSuperficieNextJsConsumer(relacao)
2188
+ ? inferirRotaNextJsConsumer(relacao)
2189
+ : undefined;
2190
+ if (superficieNextJs) {
2191
+ consumerSurfaces.set(`${superficieNextJs.rota}:${arquivo}:${superficieNextJs.tipoArquivo}`, {
2192
+ rota: superficieNextJs.rota,
2193
+ arquivo,
2194
+ tipoArquivo: superficieNextJs.tipoArquivo,
2195
+ });
2196
+ rotas.push({
2197
+ origem: "nextjs-consumer",
2198
+ metodo: "VIEW",
2199
+ caminho: superficieNextJs.rota,
2200
+ arquivo,
2201
+ simbolo: superficieNextJs.tipoArquivo,
2202
+ });
2203
+ }
2204
+
2205
+ const superficieReact = arquivoEhSuperficieReactViteConsumer(relacao)
2206
+ ? inferirRotaReactViteConsumer(relacao)
2207
+ : undefined;
2208
+ if (superficieReact) {
2209
+ consumerSurfaces.set(`${superficieReact.rota}:${arquivo}:${superficieReact.tipoArquivo}`, {
2210
+ rota: superficieReact.rota,
2211
+ arquivo,
2212
+ tipoArquivo: superficieReact.tipoArquivo,
2213
+ });
2214
+ rotas.push({
2215
+ origem: "react-vite-consumer",
2216
+ metodo: "VIEW",
2217
+ caminho: superficieReact.rota,
2218
+ arquivo,
2219
+ simbolo: superficieReact.tipoArquivo,
2220
+ });
2221
+ }
2222
+
2223
+ if (arquivoEhRotasReactViteConsumer(relacao, codigo)) {
2224
+ for (const rotaReact of extrairRotasReactViteConsumer(relacao, codigo)) {
2225
+ consumerSurfaces.set(`${rotaReact.rota}:${arquivo}:router`, {
2226
+ rota: rotaReact.rota,
2227
+ arquivo,
2228
+ tipoArquivo: "router",
2229
+ });
2230
+ rotas.push({
2231
+ origem: "react-vite-consumer",
2232
+ metodo: "VIEW",
2233
+ caminho: rotaReact.rota,
2234
+ arquivo,
2235
+ simbolo: "router",
2236
+ });
2237
+ if (rotaReact.arquivoComponente) {
2238
+ const arquivoComponente = path.join(diretorio, rotaReact.arquivoComponente);
2239
+ consumerSurfaces.set(`${rotaReact.rota}:${arquivoComponente}:page`, {
2240
+ rota: rotaReact.rota,
2241
+ arquivo: arquivoComponente,
2242
+ tipoArquivo: "page",
2243
+ });
2244
+ }
2245
+ }
2246
+ }
2247
+
2248
+ if (arquivoEhRotasAngularConsumer(relacao) && (!usarApenasRotasAngularRaiz || arquivosRotasAngularRaiz.has(path.resolve(arquivo)))) {
2249
+ encontrouSuperficieAngularPorRotas = true;
2250
+ for (const rotaAngular of await extrairRotasAngularConsumer(diretorio, relacao)) {
2251
+ const arquivoRotasAngular = path.join(diretorio, rotaAngular.arquivoRotas);
2252
+ consumerSurfaces.set(`${rotaAngular.rota}:${arquivoRotasAngular}:routes`, {
2253
+ rota: rotaAngular.rota,
2254
+ arquivo: arquivoRotasAngular,
2255
+ tipoArquivo: "routes",
2256
+ });
2257
+ rotas.push({
2258
+ origem: "angular-consumer",
2259
+ metodo: "VIEW",
2260
+ caminho: rotaAngular.rota,
2261
+ arquivo: arquivoRotasAngular,
2262
+ simbolo: rotaAngular.componente ?? "routes",
2263
+ });
2264
+ if (rotaAngular.arquivoComponente) {
2265
+ const arquivoComponente = path.join(diretorio, rotaAngular.arquivoComponente);
2266
+ consumerSurfaces.set(`${rotaAngular.rota}:${arquivoComponente}:component`, {
2267
+ rota: rotaAngular.rota,
2268
+ arquivo: arquivoComponente,
2269
+ tipoArquivo: "component",
2270
+ });
2271
+ }
2272
+ }
2273
+ }
2274
+
2275
+ for (const node of sourceFile.statements) {
2276
+ if (ts.isFunctionDeclaration(node) && node.name) {
2277
+ registrarSimboloTypeScript(simbolos, basesSimbolicas, arquivo, node.name.text);
2278
+ }
2279
+
2280
+ if (ts.isVariableStatement(node)) {
2281
+ for (const declaracao of node.declarationList.declarations) {
2282
+ if (!ts.isIdentifier(declaracao.name) || !declaracao.initializer) {
2283
+ continue;
2284
+ }
2285
+ if (ts.isArrowFunction(declaracao.initializer) || ts.isFunctionExpression(declaracao.initializer)) {
2286
+ registrarSimboloTypeScript(simbolos, basesSimbolicas, arquivo, declaracao.name.text);
2287
+ }
2288
+ }
2289
+ }
2290
+
2291
+ if (!ts.isClassDeclaration(node) || !node.name) {
2292
+ if (ts.isExpressionStatement(node)) {
2293
+ const expr = desembrulharExpressaoTypeScript(node.expression);
2294
+ if (ts.isCallExpression(expr)
2295
+ && ts.isPropertyAccessExpression(expr.expression)
2296
+ && ts.isIdentifier(expr.expression.expression)
2297
+ && expr.expression.expression.text === "Object"
2298
+ && expr.expression.name.text === "assign") {
2299
+ const nomeClasse = expr.arguments[0]
2300
+ ? extrairNomeClassePrototypeTypeScript(expr.arguments[0], sourceFile)
2301
+ : undefined;
2302
+ for (const argumento of expr.arguments.slice(1)) {
2303
+ const valor = desembrulharExpressaoTypeScript(argumento);
2304
+ if (ts.isObjectLiteralExpression(valor)) {
2305
+ registrarMetodosObjectAssignTypeScript(simbolos, basesSimbolicas, arquivo, valor, sourceFile, nomeClasse);
2306
+ }
2307
+ }
2308
+ } else if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
2309
+ registrarAtribuicaoPrototypeTypeScript(simbolos, basesSimbolicas, arquivo, sourceFile, expr.left, expr.right);
2310
+ }
2311
+ }
2312
+ continue;
2313
+ }
2314
+
2315
+ const controllerDecorator = lerDecorator(node, ["Controller"]);
2316
+ const basePath = extrairTextoLiteral(controllerDecorator?.argumentos[0]);
2317
+
2318
+ for (const member of node.members) {
2319
+ if (!ts.isMethodDeclaration(member) || !member.name) {
2320
+ continue;
2321
+ }
2322
+
2323
+ const nomeMetodo = member.name.getText(sourceFile);
2324
+ if (nomeMetodo === "constructor") {
2325
+ continue;
2326
+ }
2327
+
2328
+ registrarSimboloTypeScript(simbolos, basesSimbolicas, arquivo, nomeMetodo, node.name.text);
2329
+ const metodoEhInterno = member.modifiers?.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword);
2330
+ for (const baseSimbolica of basesSimbolicas) {
2331
+ const caminhoMetodoDireto = `${baseSimbolica}.${nomeMetodo}`;
2332
+ if (!simbolos.has(caminhoMetodoDireto)) {
2333
+ simbolos.set(caminhoMetodoDireto, { origem: "ts", caminho: caminhoMetodoDireto, arquivo, simbolo: nomeMetodo });
2334
+ }
2335
+ }
2336
+
2337
+ if (controllerDecorator && !metodoEhInterno) {
2338
+ const httpDecorator = lerDecorator(member, ["Get", "Post", "Put", "Patch", "Delete"]);
2339
+ if (httpDecorator) {
2340
+ rotas.push({
2341
+ origem: "nestjs",
2342
+ metodo: httpDecorator.nome.toUpperCase(),
2343
+ caminho: juntarCaminhoHttp(basePath, extrairTextoLiteral(httpDecorator.argumentos[0])),
2344
+ arquivo,
2345
+ simbolo: `${node.name.text}.${nomeMetodo}`,
2346
+ });
2347
+ }
2348
+ }
2349
+ }
2350
+ }
2351
+ }
2352
+
2353
+ if (!encontrouSuperficieAngularPorRotas) {
2354
+ for (const superficie of await coletarSuperficiesAngularStandaloneConsumer(diretorio, arquivos)) {
2355
+ const arquivoSuperficie = path.join(diretorio, superficie.arquivo);
2356
+ consumerSurfaces.set(`${superficie.rota}:${arquivoSuperficie}:${superficie.tipoArquivo}`, {
2357
+ rota: superficie.rota,
2358
+ arquivo: arquivoSuperficie,
2359
+ tipoArquivo: superficie.tipoArquivo,
2360
+ });
2361
+ rotas.push({
2362
+ origem: "angular-consumer",
2363
+ metodo: "VIEW",
2364
+ caminho: superficie.rota,
2365
+ arquivo: arquivoSuperficie,
2366
+ simbolo: superficie.tipoArquivo,
2367
+ });
2368
+ }
2369
+ }
2370
+ }
2371
+
2372
+ return {
2373
+ simbolos: [...simbolos.values()],
2374
+ rotas,
2375
+ recursos: [...recursos.values()],
2376
+ consumerSurfaces: [...consumerSurfaces.values()].sort((a, b) =>
2377
+ a.rota.localeCompare(b.rota, "pt-BR")
2378
+ || a.tipoArquivo.localeCompare(b.tipoArquivo, "pt-BR")
2379
+ || a.arquivo.localeCompare(b.arquivo, "pt-BR")),
2380
+ };
2381
+ }
2382
+
2383
+ interface BlocoPython {
2384
+ tipo: "class" | "def";
2385
+ nome: string;
2386
+ indentacao: number;
2387
+ }
2388
+
2389
+ function registrarSimboloPython(
2390
+ simbolos: Map<string, SimboloResolvido>,
2391
+ basesSimbolicas: string[],
2392
+ arquivo: string,
2393
+ nome: string,
2394
+ nomeClasse?: string,
2395
+ ): void {
2396
+ for (const baseSimbolica of basesSimbolicas) {
2397
+ const caminho = nomeClasse
2398
+ ? `${baseSimbolica}.${nomeClasse}.${nome}`
2399
+ : `${baseSimbolica}.${nome}`;
2400
+ simbolos.set(caminho, {
2401
+ origem: "py",
2402
+ caminho,
2403
+ arquivo,
2404
+ simbolo: nomeClasse ? `${nomeClasse}.${nome}` : nome,
2405
+ });
2406
+ }
2407
+ }
2408
+
2409
+ function registrarRotasPython(
2410
+ rotas: RotaResolvida[],
2411
+ decoratorsPendentes: string[],
2412
+ prefixo: string | undefined,
2413
+ arquivo: string,
2414
+ nomeFuncao: string,
2415
+ ): void {
2416
+ for (const decorator of decoratorsPendentes) {
2417
+ const match = decorator.match(/^@(router|app)\.(get|post|put|patch|delete)\((.*)\)\s*$/);
2418
+ if (!match) {
2419
+ continue;
2420
+ }
2421
+ const metodo = match[2]!.toUpperCase();
2422
+ const sufixo = match[3]?.match(/["']([^"']+)["']/)?.[1];
2423
+ rotas.push({
2424
+ origem: "fastapi",
2425
+ metodo,
2426
+ caminho: juntarCaminhoHttp(prefixo, sufixo),
2427
+ arquivo,
2428
+ simbolo: nomeFuncao,
2429
+ });
2430
+ }
2431
+ }
2432
+
2433
+ async function indexarPython(diretorios: string[]): Promise<{ simbolos: SimboloResolvido[]; rotas: RotaResolvida[]; recursos: RecursoResolvido[] }> {
2434
+ const simbolos = new Map<string, SimboloResolvido>();
2435
+ const rotas: RotaResolvida[] = [];
2436
+ const recursos = new Map<string, RecursoResolvido>();
2437
+
2438
+ for (const diretorio of diretorios) {
2439
+ const arquivos = (await listarArquivosRecursivos(diretorio, [".py"]))
2440
+ .filter((arquivo) => !arquivo.endsWith("__init__.py") && !/tests?[\\/]/i.test(arquivo));
2441
+
2442
+ for (const arquivo of arquivos) {
2443
+ const texto = await readFile(arquivo, "utf8");
2444
+ const basesSimbolicas = caminhosSimbolicos(diretorio, arquivo);
2445
+ const prefixo = texto.match(/APIRouter\s*\(\s*prefix\s*=\s*["']([^"']+)["']/)?.[1];
2446
+ for (const recurso of extrairRecursosPersistenciaCodigoVivo(arquivo, texto)) {
2447
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
2448
+ }
2449
+ for (const rota of extrairRotasFlaskDecoradas(texto)) {
2450
+ rotas.push({
2451
+ origem: "flask",
2452
+ metodo: rota.metodo,
2453
+ caminho: rota.caminho,
2454
+ arquivo,
2455
+ simbolo: rota.nomeFuncao,
2456
+ });
2457
+ }
2458
+ const blocos: BlocoPython[] = [];
2459
+ let decoratorsPendentes: string[] = [];
2460
+
2461
+ for (const linha of texto.split(/\r?\n/)) {
2462
+ const trim = linha.trim();
2463
+ if (trim === "" || trim.startsWith("#")) {
2464
+ decoratorsPendentes = [];
2465
+ continue;
2466
+ }
2467
+
2468
+ const indentacao = contarIndentacaoPython(linha);
2469
+ while (blocos.length > 0 && indentacao <= blocos[blocos.length - 1]!.indentacao) {
2470
+ blocos.pop();
2471
+ }
2472
+
2473
+ if (trim.startsWith("@")) {
2474
+ decoratorsPendentes.push(trim);
2475
+ continue;
2476
+ }
2477
+
2478
+ const classe = trim.match(/^class\s+([A-Za-z_]\w*)(?:\([^)]*\))?:\s*(?:#.*)?$/);
2479
+ if (classe) {
2480
+ blocos.push({ tipo: "class", nome: classe[1]!, indentacao });
2481
+ decoratorsPendentes = [];
2482
+ continue;
2483
+ }
2484
+
2485
+ const definicao = trim.match(/^(?:async\s+def|def)\s+([A-Za-z_]\w*)\s*\(/);
2486
+ if (definicao) {
2487
+ const nomeFuncao = definicao[1]!;
2488
+ const existeDefPai = blocos.some((bloco) => bloco.tipo === "def");
2489
+ const classeAtual = [...blocos].reverse().find((bloco) => bloco.tipo === "class");
2490
+
2491
+ if (!existeDefPai && classeAtual) {
2492
+ registrarSimboloPython(simbolos, basesSimbolicas, arquivo, nomeFuncao, classeAtual.nome);
2493
+ } else if (!existeDefPai) {
2494
+ registrarSimboloPython(simbolos, basesSimbolicas, arquivo, nomeFuncao);
2495
+ registrarRotasPython(rotas, decoratorsPendentes, prefixo, arquivo, nomeFuncao);
2496
+ }
2497
+
2498
+ blocos.push({ tipo: "def", nome: nomeFuncao, indentacao });
2499
+ decoratorsPendentes = [];
2500
+ continue;
2501
+ }
2502
+
2503
+ decoratorsPendentes = [];
2504
+ }
2505
+ }
2506
+ }
2507
+
2508
+ return { simbolos: [...simbolos.values()], rotas, recursos: [...recursos.values()] };
2509
+ }
2510
+
2511
+ async function indexarDart(diretorios: string[]): Promise<{
2512
+ simbolos: SimboloResolvido[];
2513
+ rotas: RotaResolvida[];
2514
+ recursos: RecursoResolvido[];
2515
+ consumerSurfaces: RegistroConsumerSurfaceDrift[];
2516
+ }> {
2517
+ const simbolos = new Map<string, SimboloResolvido>();
2518
+ const rotas: RotaResolvida[] = [];
2519
+ const recursos = new Map<string, RecursoResolvido>();
2520
+ const consumerSurfaces = new Map<string, RegistroConsumerSurfaceDrift>();
2521
+
2522
+ for (const diretorio of diretorios) {
2523
+ const arquivos = (await listarArquivosRecursivos(diretorio, [".dart"]))
2524
+ .filter((arquivo) => !arquivo.endsWith(".g.dart") && !arquivo.endsWith(".freezed.dart"));
2525
+
2526
+ for (const arquivo of arquivos) {
2527
+ const texto = await readFile(arquivo, "utf8");
2528
+ const basesSimbolicas = caminhosSimbolicos(diretorio, arquivo);
2529
+ const relacao = path.relative(diretorio, arquivo);
2530
+ for (const recurso of extrairRecursosPersistenciaCodigoVivo(arquivo, texto)) {
2531
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
2532
+ }
2533
+
2534
+ for (const match of texto.matchAll(/(?:Future<[^\n]+>|[\w?<>.,\s]+)\s+(\w+)\(([^)]*)\)\s*(?:async\s*)?\{/g)) {
2535
+ const nome = match[1]!;
2536
+ if (["build", "toString"].includes(nome)) {
2537
+ continue;
2538
+ }
2539
+ for (const baseSimbolica of basesSimbolicas) {
2540
+ const caminho = `${baseSimbolica}.${nome}`;
2541
+ simbolos.set(caminho, { origem: "dart", caminho, arquivo, simbolo: nome });
2542
+ }
2543
+ }
2544
+
2545
+ const superficieFlutter = inferirRotaFlutterConsumer(relacao);
2546
+ if (superficieFlutter) {
2547
+ consumerSurfaces.set(`${superficieFlutter.rota}:${arquivo}:${superficieFlutter.tipoArquivo}`, {
2548
+ rota: superficieFlutter.rota,
2549
+ arquivo,
2550
+ tipoArquivo: superficieFlutter.tipoArquivo,
2551
+ });
2552
+ rotas.push({
2553
+ origem: "flutter-consumer",
2554
+ metodo: "VIEW",
2555
+ caminho: superficieFlutter.rota,
2556
+ arquivo,
2557
+ simbolo: superficieFlutter.tipoArquivo,
2558
+ });
2559
+ }
2560
+
2561
+ if (arquivoEhRotasFlutterConsumer(relacao, texto)) {
2562
+ for (const rotaFlutter of extrairRotasFlutterConsumer(relacao, texto)) {
2563
+ consumerSurfaces.set(`${rotaFlutter.rota}:${arquivo}:router`, {
2564
+ rota: rotaFlutter.rota,
2565
+ arquivo,
2566
+ tipoArquivo: "router",
2567
+ });
2568
+ rotas.push({
2569
+ origem: "flutter-consumer",
2570
+ metodo: "VIEW",
2571
+ caminho: rotaFlutter.rota,
2572
+ arquivo,
2573
+ simbolo: "router",
2574
+ });
2575
+ }
2576
+ }
2577
+ }
2578
+ }
2579
+
2580
+ return {
2581
+ simbolos: [...simbolos.values()],
2582
+ rotas,
2583
+ recursos: [...recursos.values()],
2584
+ consumerSurfaces: [...consumerSurfaces.values()].sort((a, b) =>
2585
+ a.rota.localeCompare(b.rota, "pt-BR")
2586
+ || a.tipoArquivo.localeCompare(b.tipoArquivo, "pt-BR")
2587
+ || a.arquivo.localeCompare(b.arquivo, "pt-BR")),
2588
+ };
2589
+ }
2590
+
2591
+ async function indexarLua(diretorios: string[]): Promise<{ simbolos: SimboloResolvido[]; recursos: RecursoResolvido[] }> {
2592
+ const simbolos = new Map<string, SimboloResolvido>();
2593
+ const recursos = new Map<string, RecursoResolvido>();
2594
+
2595
+ for (const diretorio of diretorios) {
2596
+ const arquivos = (await listarArquivosRecursivos(diretorio, [".lua"]))
2597
+ .filter((arquivo) => !/(^|[\\/])tests?([\\/]|$)|(?:^|[\\/])spec([\\/]|$)|(?:_spec|_test)\.lua$/i.test(arquivo));
2598
+
2599
+ for (const arquivo of arquivos) {
2600
+ const codigo = await readFile(arquivo, "utf8");
2601
+ const basesSimbolicas = caminhosSimbolicos(diretorio, arquivo);
2602
+ for (const recurso of extrairRecursosPersistenciaCodigoVivo(arquivo, codigo)) {
2603
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
2604
+ }
2605
+ for (const simbolo of extrairSimbolosLua(codigo)) {
2606
+ registrarSimboloGenerico(simbolos, "lua", basesSimbolicas, arquivo, simbolo.simbolo);
2607
+ }
2608
+ }
2609
+ }
2610
+
2611
+ return {
2612
+ simbolos: [...simbolos.values()],
2613
+ recursos: [...recursos.values()],
2614
+ };
2615
+ }
2616
+
2617
+ async function indexarPhp(diretorios: string[]): Promise<{ simbolos: SimboloResolvido[]; rotas: RotaResolvida[]; recursos: RecursoResolvido[] }> {
2618
+ const simbolos = new Map<string, SimboloResolvido>();
2619
+ const rotas: RotaResolvida[] = [];
2620
+ const recursos = new Map<string, RecursoResolvido>();
2621
+
2622
+ for (const diretorio of diretorios) {
2623
+ const arquivos = (await listarArquivosRecursivos(diretorio, [".php"]))
2624
+ .filter((arquivo) => !/(^|[\\/])tests?([\\/]|$)|(?:^|[\\/])spec([\\/]|$)|(?:Test|Spec)\.php$/i.test(arquivo));
2625
+
2626
+ for (const arquivo of arquivos) {
2627
+ const codigo = await readFile(arquivo, "utf8");
2628
+ const basesSimbolicas = caminhosSimbolicos(diretorio, arquivo);
2629
+ for (const recurso of extrairRecursosPersistenciaCodigoVivo(arquivo, codigo)) {
2630
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
2631
+ }
2632
+ for (const simbolo of extrairSimbolosPhp(codigo)) {
2633
+ registrarSimboloPhp(simbolos, basesSimbolicas, arquivo, simbolo.simbolo);
2634
+ }
2635
+ for (const rota of extrairRotasPhp(codigo)) {
2636
+ rotas.push({
2637
+ origem: "php",
2638
+ metodo: rota.metodo,
2639
+ caminho: rota.caminho,
2640
+ arquivo,
2641
+ simbolo: rota.simbolo,
2642
+ });
2643
+ }
2644
+ }
2645
+ }
2646
+
2647
+ return { simbolos: [...simbolos.values()], rotas, recursos: [...recursos.values()] };
2648
+ }
2649
+
2650
+ function registrarSimboloGenerico(
2651
+ simbolos: Map<string, SimboloResolvido>,
2652
+ origem: SimboloResolvido["origem"],
2653
+ basesSimbolicas: string[],
2654
+ arquivo: string,
2655
+ simbolo: string,
2656
+ ): void {
2657
+ for (const baseSimbolica of basesSimbolicas) {
2658
+ const caminho = `${baseSimbolica}.${simbolo}`;
2659
+ simbolos.set(caminho, {
2660
+ origem,
2661
+ caminho,
2662
+ arquivo,
2663
+ simbolo,
2664
+ });
2665
+
2666
+ const ultimo = simbolo.split(".").at(-1);
2667
+ if (ultimo) {
2668
+ const caminhoDireto = `${baseSimbolica}.${ultimo}`;
2669
+ if (!simbolos.has(caminhoDireto)) {
2670
+ simbolos.set(caminhoDireto, {
2671
+ origem,
2672
+ caminho: caminhoDireto,
2673
+ arquivo,
2674
+ simbolo: ultimo,
2675
+ });
2676
+ }
2677
+ }
2678
+ }
2679
+ }
2680
+
2681
+ function registrarSimboloPhp(
2682
+ simbolos: Map<string, SimboloResolvido>,
2683
+ basesSimbolicas: string[],
2684
+ arquivo: string,
2685
+ simbolo: string,
2686
+ ): void {
2687
+ registrarSimboloGenerico(simbolos, "php", basesSimbolicas, arquivo, simbolo);
2688
+
2689
+ const partes = simbolo.split(".").filter(Boolean);
2690
+ const aliases = new Set<string>([simbolo]);
2691
+ const ultimo = partes.at(-1);
2692
+ const penultimo = partes.at(-2);
2693
+ if (ultimo) {
2694
+ aliases.add(ultimo);
2695
+ }
2696
+ if (penultimo && ultimo) {
2697
+ aliases.add(`${penultimo}.${ultimo}`);
2698
+ }
2699
+
2700
+ for (const alias of aliases) {
2701
+ if (!alias || simbolos.has(alias)) {
2702
+ continue;
2703
+ }
2704
+ simbolos.set(alias, {
2705
+ origem: "php",
2706
+ caminho: alias,
2707
+ arquivo,
2708
+ simbolo,
2709
+ });
2710
+ }
2711
+ }
2712
+
2713
+ async function indexarDotnet(diretorios: string[]): Promise<{ simbolos: SimboloResolvido[]; rotas: RotaResolvida[]; recursos: RecursoResolvido[] }> {
2714
+ const simbolos = new Map<string, SimboloResolvido>();
2715
+ const rotas: RotaResolvida[] = [];
2716
+ const recursos = new Map<string, RecursoResolvido>();
2717
+
2718
+ for (const diretorio of diretorios) {
2719
+ const arquivos = (await listarArquivosRecursivos(diretorio, [".cs"]))
2720
+ .filter((arquivo) => !/(^|[\\/])(bin|obj|Test[s]?)([\\/]|$)/i.test(arquivo));
2721
+
2722
+ for (const arquivo of arquivos) {
2723
+ const codigo = await readFile(arquivo, "utf8");
2724
+ const basesSimbolicas = caminhosSimbolicos(diretorio, arquivo);
2725
+ for (const recurso of extrairRecursosPersistenciaCodigoVivo(arquivo, codigo)) {
2726
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
2727
+ }
2728
+ for (const simbolo of extrairSimbolosDotnet(codigo)) {
2729
+ registrarSimboloGenerico(simbolos, "cs", basesSimbolicas, arquivo, simbolo.simbolo);
2730
+ }
2731
+ for (const rota of extrairRotasDotnet(codigo)) {
2732
+ rotas.push({
2733
+ origem: "dotnet",
2734
+ metodo: rota.metodo,
2735
+ caminho: rota.caminho,
2736
+ arquivo,
2737
+ simbolo: rota.simbolo,
2738
+ });
2739
+ }
2740
+ }
2741
+ }
2742
+
2743
+ return { simbolos: [...simbolos.values()], rotas, recursos: [...recursos.values()] };
2744
+ }
2745
+
2746
+ async function indexarJava(diretorios: string[]): Promise<{ simbolos: SimboloResolvido[]; rotas: RotaResolvida[]; recursos: RecursoResolvido[] }> {
2747
+ const simbolos = new Map<string, SimboloResolvido>();
2748
+ const rotas: RotaResolvida[] = [];
2749
+ const recursos = new Map<string, RecursoResolvido>();
2750
+
2751
+ for (const diretorio of diretorios) {
2752
+ const arquivos = (await listarArquivosRecursivos(diretorio, [".java"]))
2753
+ .filter((arquivo) => !/(^|[\\/])(target|build|out|Test[s]?)([\\/]|$)/i.test(arquivo));
2754
+
2755
+ for (const arquivo of arquivos) {
2756
+ const codigo = await readFile(arquivo, "utf8");
2757
+ const basesSimbolicas = caminhosSimbolicos(diretorio, arquivo);
2758
+ for (const recurso of extrairRecursosPersistenciaCodigoVivo(arquivo, codigo)) {
2759
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
2760
+ }
2761
+ for (const simbolo of extrairSimbolosJava(codigo)) {
2762
+ registrarSimboloGenerico(simbolos, "java", basesSimbolicas, arquivo, simbolo.simbolo);
2763
+ }
2764
+ for (const rota of extrairRotasJava(codigo)) {
2765
+ rotas.push({
2766
+ origem: "java",
2767
+ metodo: rota.metodo,
2768
+ caminho: rota.caminho,
2769
+ arquivo,
2770
+ simbolo: rota.simbolo,
2771
+ });
2772
+ }
2773
+ }
2774
+ }
2775
+
2776
+ return { simbolos: [...simbolos.values()], rotas, recursos: [...recursos.values()] };
2777
+ }
2778
+
2779
+ async function indexarGo(diretorios: string[]): Promise<{ simbolos: SimboloResolvido[]; rotas: RotaResolvida[]; recursos: RecursoResolvido[] }> {
2780
+ const simbolos = new Map<string, SimboloResolvido>();
2781
+ const rotas: RotaResolvida[] = [];
2782
+ const recursos = new Map<string, RecursoResolvido>();
2783
+
2784
+ for (const diretorio of diretorios) {
2785
+ const arquivos = await listarArquivosRecursivos(diretorio, [".go"]);
2786
+
2787
+ for (const arquivo of arquivos) {
2788
+ const codigo = await readFile(arquivo, "utf8");
2789
+ const basesSimbolicas = caminhosSimbolicos(diretorio, arquivo);
2790
+ for (const recurso of extrairRecursosPersistenciaCodigoVivo(arquivo, codigo)) {
2791
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
2792
+ }
2793
+ for (const simbolo of extrairSimbolosGo(codigo)) {
2794
+ registrarSimboloGenerico(simbolos, "go", basesSimbolicas, arquivo, simbolo.simbolo);
2795
+ }
2796
+ for (const rota of extrairRotasGo(codigo)) {
2797
+ rotas.push({
2798
+ origem: "go",
2799
+ metodo: rota.metodo,
2800
+ caminho: rota.caminho,
2801
+ arquivo,
2802
+ simbolo: rota.simbolo,
2803
+ });
2804
+ }
2805
+ }
2806
+ }
2807
+
2808
+ return { simbolos: [...simbolos.values()], rotas, recursos: [...recursos.values()] };
2809
+ }
2810
+
2811
+ async function indexarRust(diretorios: string[]): Promise<{ simbolos: SimboloResolvido[]; rotas: RotaResolvida[]; recursos: RecursoResolvido[] }> {
2812
+ const simbolos = new Map<string, SimboloResolvido>();
2813
+ const rotas: RotaResolvida[] = [];
2814
+ const recursos = new Map<string, RecursoResolvido>();
2815
+
2816
+ for (const diretorio of diretorios) {
2817
+ const arquivos = await listarArquivosRecursivos(diretorio, [".rs"]);
2818
+
2819
+ for (const arquivo of arquivos) {
2820
+ const codigo = await readFile(arquivo, "utf8");
2821
+ const basesSimbolicas = caminhosSimbolicos(diretorio, arquivo);
2822
+ for (const recurso of extrairRecursosPersistenciaCodigoVivo(arquivo, codigo)) {
2823
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
2824
+ }
2825
+ for (const simbolo of extrairSimbolosRust(codigo)) {
2826
+ registrarSimboloGenerico(simbolos, "rust", basesSimbolicas, arquivo, simbolo.simbolo);
2827
+ }
2828
+ for (const rota of extrairRotasRust(codigo)) {
2829
+ rotas.push({
2830
+ origem: "rust",
2831
+ metodo: rota.metodo,
2832
+ caminho: rota.caminho,
2833
+ arquivo,
2834
+ simbolo: rota.simbolo,
2835
+ });
2836
+ }
2837
+ }
2838
+ }
2839
+
2840
+ return { simbolos: [...simbolos.values()], rotas, recursos: [...recursos.values()] };
2841
+ }
2842
+
2843
+ async function indexarCpp(diretorios: string[]): Promise<{ simbolos: SimboloResolvido[]; recursos: RecursoResolvido[] }> {
2844
+ const simbolos = new Map<string, SimboloResolvido>();
2845
+ const recursos = new Map<string, RecursoResolvido>();
2846
+
2847
+ for (const diretorio of diretorios) {
2848
+ const arquivos = (await listarArquivosRecursivos(diretorio, [".cpp", ".cc", ".cxx", ".hpp", ".h"]))
2849
+ .filter((arquivo) => !/(^|[\\/])(windows|linux|macos|runner|flutter|ephemeral|build|vendor)([\\/]|$)/i.test(arquivo));
2850
+
2851
+ for (const arquivo of arquivos) {
2852
+ const codigo = await readFile(arquivo, "utf8");
2853
+ const basesSimbolicas = caminhosSimbolicos(diretorio, arquivo);
2854
+ for (const recurso of extrairRecursosPersistenciaCodigoVivo(arquivo, codigo)) {
2855
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
2856
+ }
2857
+ for (const simbolo of extrairSimbolosCpp(codigo)) {
2858
+ registrarSimboloGenerico(simbolos, "cpp", basesSimbolicas, arquivo, simbolo.simbolo);
2859
+ }
2860
+ }
2861
+
2862
+ }
2863
+
2864
+ return {
2865
+ simbolos: [...simbolos.values()],
2866
+ recursos: [...recursos.values()],
2867
+ };
2868
+ }
2869
+
2870
+ async function indexarPersistenciaDeclarativa(diretorios: string[]): Promise<{ recursos: RecursoResolvido[] }> {
2871
+ const recursos = new Map<string, RecursoResolvido>();
2872
+
2873
+ for (const diretorio of diretorios) {
2874
+ const arquivos = await listarArquivosRecursivos(diretorio, [
2875
+ ".sql", ".psql", ".ddl", ".prisma",
2876
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
2877
+ ".py", ".dart", ".lua", ".php", ".cs", ".java", ".go", ".rs", ".cpp", ".cc", ".cxx", ".hpp", ".h",
2878
+ ]);
2879
+
2880
+ for (const arquivo of arquivos) {
2881
+ const codigo = await readFile(arquivo, "utf8");
2882
+ const extracoes = arquivo.endsWith(".prisma")
2883
+ ? extrairRecursosPrisma(arquivo, codigo)
2884
+ : extrairRecursosPersistenciaCodigoVivo(arquivo, codigo);
2885
+ for (const recurso of extracoes) {
2886
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
2887
+ }
2888
+ }
2889
+ }
2890
+
2891
+ return { recursos: [...recursos.values()] };
2892
+ }
2893
+
2894
+ function normalizarOrigemParaEngine(origem?: OrigemRecursoDrift): EngineBanco | undefined {
2895
+ return origem && origem !== "firebase" && origem !== "arquivo" ? origem : undefined;
2896
+ }
2897
+
2898
+ function registrarColunaPersistenciaDrift(
2899
+ colunas: Map<string, RegistroColunaPersistenciaDrift>,
2900
+ origem: OrigemRecursoDrift,
2901
+ recurso: string,
2902
+ coluna: string,
2903
+ arquivo: string,
2904
+ ): void {
2905
+ const recursoNormalizado = fecharPrefixoRecurso(limparLiteralRecurso(recurso));
2906
+ const colunaNormalizada = fecharPrefixoRecurso(limparLiteralRecurso(coluna));
2907
+ if (!recursoNormalizado || !colunaNormalizada || recursoEhIgnorado(colunaNormalizada)) {
2908
+ return;
2909
+ }
2910
+ const chave = `${origem}:${normalizarNomeRecursoDrift(recursoNormalizado)}:${normalizarNomeRecursoDrift(colunaNormalizada)}:${arquivo}`;
2911
+ if (!colunas.has(chave)) {
2912
+ colunas.set(chave, {
2913
+ origem,
2914
+ categoriaPersistencia: categorizarPersistenciaPorOrigem(origem),
2915
+ recurso: recursoNormalizado,
2916
+ coluna: colunaNormalizada,
2917
+ arquivo,
2918
+ });
2919
+ }
2920
+ }
2921
+
2922
+ function registrarRepositorioPersistenciaDrift(
2923
+ repositorios: Map<string, RegistroRepositorioPersistenciaDrift>,
2924
+ origem: OrigemRecursoDrift,
2925
+ recurso: string,
2926
+ arquivo: string,
2927
+ ): void {
2928
+ const recursoNormalizado = fecharPrefixoRecurso(limparLiteralRecurso(recurso));
2929
+ if (!recursoNormalizado) {
2930
+ return;
2931
+ }
2932
+ const chave = `${origem}:${normalizarNomeRecursoDrift(recursoNormalizado)}:${arquivo}`;
2933
+ if (!repositorios.has(chave)) {
2934
+ repositorios.set(chave, {
2935
+ origem,
2936
+ categoriaPersistencia: categorizarPersistenciaPorOrigem(origem),
2937
+ recurso: recursoNormalizado,
2938
+ arquivo,
2939
+ });
2940
+ }
2941
+ }
2942
+
2943
+ function extrairColunasSqlDetalhadas(arquivo: string, codigo: string): RegistroColunaPersistenciaDrift[] {
2944
+ const colunas = new Map<string, RegistroColunaPersistenciaDrift>();
2945
+ const motores = inferirMotoresRelacionais(codigo, arquivo);
2946
+ if (motores.length === 0) {
2947
+ return [];
2948
+ }
2949
+
2950
+ const registrarParaMotores = (recurso: string, coluna: string) => {
2951
+ for (const motor of motores) {
2952
+ registrarColunaPersistenciaDrift(colunas, motor, recurso, coluna, arquivo);
2953
+ }
2954
+ };
2955
+
2956
+ for (const match of codigo.matchAll(/\bcreate\s+table\s+(?:if\s+not\s+exists\s+)?["'`]?([A-Za-z_][\w$.-]*)["'`]?\s*\(([\s\S]*?)\)\s*;?/gi)) {
2957
+ const tabela = match[1]!;
2958
+ const corpo = match[2] ?? "";
2959
+ for (const linha of corpo.split(/\r?\n|,/)) {
2960
+ const trecho = linha.trim();
2961
+ if (!trecho || /^(?:constraint|primary|foreign|unique|check|key|index)\b/i.test(trecho)) {
2962
+ continue;
2963
+ }
2964
+ const coluna = trecho.match(/^["'`]?([A-Za-z_][\w$.-]*)["'`]?/i)?.[1];
2965
+ if (coluna) {
2966
+ registrarParaMotores(tabela, coluna);
2967
+ }
2968
+ }
2969
+ }
2970
+
2971
+ for (const match of codigo.matchAll(/\binsert\s+into\s+["'`]?([A-Za-z_][\w$.-]*)["'`]?\s*\(([^)]+)\)/gi)) {
2972
+ for (const coluna of (match[2] ?? "").split(",").map((item) => item.trim())) {
2973
+ registrarParaMotores(match[1]!, coluna);
2974
+ }
2975
+ }
2976
+
2977
+ for (const match of codigo.matchAll(/\bupdate\s+["'`]?([A-Za-z_][\w$.-]*)["'`]?\s+set\s+([\s\S]*?)(?:\bwhere\b|;|$)/gi)) {
2978
+ for (const coluna of (match[2] ?? "").split(",").map((item) => item.split("=")[0]?.trim() ?? "")) {
2979
+ registrarParaMotores(match[1]!, coluna);
2980
+ }
2981
+ }
2982
+
2983
+ for (const match of codigo.matchAll(/\bselect\s+([\s\S]*?)\s+from\s+["'`]?([A-Za-z_][\w$.-]*)["'`]?/gi)) {
2984
+ const tabela = match[2]!;
2985
+ const lista = (match[1] ?? "").trim();
2986
+ if (!lista || lista === "*") {
2987
+ continue;
2988
+ }
2989
+ for (const coluna of lista.split(",").map((item) => item.trim().split(/\s+as\s+/i)[0] ?? "")) {
2990
+ const nome = coluna.split(".").at(-1) ?? coluna;
2991
+ registrarParaMotores(tabela, nome);
2992
+ }
2993
+ }
2994
+
2995
+ return [...colunas.values()];
2996
+ }
2997
+
2998
+ function extrairColunasPrismaDetalhadas(arquivo: string, codigo: string): RegistroColunaPersistenciaDrift[] {
2999
+ const colunas = new Map<string, RegistroColunaPersistenciaDrift>();
3000
+ const provider = codigo.match(/\bprovider\s*=\s*["'`](postgresql|mysql|sqlite)["'`]/i)?.[1]?.toLowerCase();
3001
+ const engine = provider === "postgresql"
3002
+ ? "postgres"
3003
+ : provider === "mysql"
3004
+ ? "mysql"
3005
+ : provider === "sqlite"
3006
+ ? "sqlite"
3007
+ : undefined;
3008
+ if (!engine) {
3009
+ return [];
3010
+ }
3011
+
3012
+ for (const match of codigo.matchAll(/\bmodel\s+([A-Za-z_]\w*)\s*\{([\s\S]*?)\n\}/g)) {
3013
+ const nomeModelo = match[1]!;
3014
+ const corpo = match[2] ?? "";
3015
+ const tabela = corpo.match(/@@map\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/)?.[1] ?? nomeModelo;
3016
+ for (const linha of corpo.split(/\r?\n/)) {
3017
+ const limpa = linha.trim();
3018
+ if (!limpa || limpa.startsWith("@@") || limpa.startsWith("//")) {
3019
+ continue;
3020
+ }
3021
+ const coluna = limpa.match(/^([A-Za-z_]\w*)\s+/)?.[1];
3022
+ const colunaMapeada = limpa.match(/@map\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/)?.[1];
3023
+ if (coluna) {
3024
+ registrarColunaPersistenciaDrift(colunas, engine, tabela, colunaMapeada ?? coluna, arquivo);
3025
+ }
3026
+ }
3027
+ }
3028
+
3029
+ return [...colunas.values()];
3030
+ }
3031
+
3032
+ function extrairCamposMongoDetalhados(arquivo: string, codigo: string): RegistroColunaPersistenciaDrift[] {
3033
+ const colunas = new Map<string, RegistroColunaPersistenciaDrift>();
3034
+ const colecoes = extrairRecursosMongoDb(arquivo, codigo).filter((item) => item.tipo === "collection");
3035
+ if (colecoes.length === 0) {
3036
+ return [];
3037
+ }
3038
+
3039
+ const registrarCampoMongo = (colecao: string, trecho: string) => {
3040
+ for (const match of trecho.matchAll(/([A-Za-z_][\w$]*)\s*:/g)) {
3041
+ registrarColunaPersistenciaDrift(colunas, "mongodb", colecao, match[1]!, arquivo);
3042
+ }
3043
+ };
3044
+
3045
+ for (const schema of codigo.matchAll(/\bnew\s+Schema\s*\(\s*\{([\s\S]*?)\}\s*\)/g)) {
3046
+ for (const colecao of colecoes) {
3047
+ registrarCampoMongo(colecao.nome, schema[1] ?? "");
3048
+ }
3049
+ }
3050
+
3051
+ for (const trecho of codigo.matchAll(/\b(?:find(?:One)?|update(?:One|Many)?|insertOne|insertMany)\s*\(\s*\{([\s\S]*?)\}\s*(?:,|\))/g)) {
3052
+ for (const colecao of colecoes) {
3053
+ registrarCampoMongo(colecao.nome, trecho[1] ?? "");
3054
+ }
3055
+ }
3056
+
3057
+ return [...colunas.values()];
3058
+ }
3059
+
3060
+ function extrairCamposRedisDetalhados(arquivo: string, codigo: string): RegistroColunaPersistenciaDrift[] {
3061
+ const colunas = new Map<string, RegistroColunaPersistenciaDrift>();
3062
+ for (const match of codigo.matchAll(/\bh(?:set|get|del|exists)\s*\(\s*["'`]([^"'`]+)["'`]\s*,\s*["'`]([^"'`]+)["'`]/gi)) {
3063
+ registrarColunaPersistenciaDrift(colunas, "redis", match[1]!, match[2]!, arquivo);
3064
+ }
3065
+ return [...colunas.values()];
3066
+ }
3067
+
3068
+ function extrairCamposArquivoLocalDetalhados(arquivo: string, codigo: string): RegistroColunaPersistenciaDrift[] {
3069
+ const colunas = new Map<string, RegistroColunaPersistenciaDrift>();
3070
+ const recursos = extrairRecursosArquivoLocal(arquivo, codigo);
3071
+ if (recursos.length === 0) {
3072
+ return [];
3073
+ }
3074
+
3075
+ const registrarCampo = (recurso: string, trecho: string) => {
3076
+ for (const match of trecho.matchAll(/["'`]?([A-Za-z_][\w$-]*)["'`]?\s*:/g)) {
3077
+ registrarColunaPersistenciaDrift(colunas, "arquivo", recurso, match[1]!, arquivo);
3078
+ }
3079
+ };
3080
+
3081
+ const blocos = [
3082
+ ...codigo.matchAll(/\b(?:_empty_store|empty_store|default_store)\b[\s\S]{0,1000}?return\s*\{([\s\S]*?)\n\s*\}/g),
3083
+ ...codigo.matchAll(/\bstore\s*=\s*\{([\s\S]*?)\n\s*\}/g),
3084
+ ];
3085
+
3086
+ for (const recurso of recursos) {
3087
+ for (const bloco of blocos) {
3088
+ registrarCampo(recurso.nome, bloco[1] ?? "");
3089
+ }
3090
+ }
3091
+
3092
+ for (const match of codigo.matchAll(/Preferences\.(?:get|set|remove)\s*\(\s*\{[\s\S]{0,160}?key\s*:\s*["'`]([^"'`]+)["'`]/gi)) {
3093
+ registrarColunaPersistenciaDrift(colunas, "arquivo", match[1]!, match[1]!, arquivo);
3094
+ }
3095
+
3096
+ for (const match of codigo.matchAll(/\b(?:localStorage|sessionStorage)\.(?:getItem|setItem|removeItem)\s*\(\s*["'`]([^"'`]+)["'`]/gi)) {
3097
+ registrarColunaPersistenciaDrift(colunas, "arquivo", match[1]!, match[1]!, arquivo);
3098
+ }
3099
+
3100
+ return [...colunas.values()];
3101
+ }
3102
+
3103
+ function registrarRepositoriosPorRecursos(
3104
+ repositorios: Map<string, RegistroRepositorioPersistenciaDrift>,
3105
+ arquivo: string,
3106
+ codigo: string,
3107
+ recursos: RecursoResolvido[],
3108
+ ): void {
3109
+ const contextoRepositorio = /(?:repository|repositories|repositorio|repositorios|repo|dao|store|queries|persistence|persistencia)/i.test(arquivo)
3110
+ || /\b(?:Repository|Repositories|Dao|Store)\b/.test(codigo);
3111
+ const contextoAcesso = /\b(?:select|insert|update|delete|aggregate|findOne|findMany|findUnique|findFirst|prisma\.|db\.collection|createClient|hset|hget|xadd|xread|json\.(?:load|loads|dump|dumps)|JSON\.(?:parse|stringify)|read_text|write_text|readFile(?:Sync)?|writeFile(?:Sync)?|open|Preferences\.(?:get|set|remove)|localStorage\.(?:getItem|setItem|removeItem)|sessionStorage\.(?:getItem|setItem|removeItem))\b/i.test(codigo)
3112
+ || /\.(?:json|jsonl|ndjson|db|sqlite|sqlite3)\b/i.test(codigo);
3113
+ if (!contextoRepositorio && !contextoAcesso) {
3114
+ return;
3115
+ }
3116
+
3117
+ for (const recurso of recursos) {
3118
+ registrarRepositorioPersistenciaDrift(repositorios, recurso.origem, recurso.nome, arquivo);
3119
+ }
3120
+ }
3121
+
3122
+ async function indexarPersistenciaDetalhada(
3123
+ diretorios: string[],
3124
+ ): Promise<{
3125
+ colunas: RegistroColunaPersistenciaDrift[];
3126
+ repositorios: RegistroRepositorioPersistenciaDrift[];
3127
+ }> {
3128
+ const colunas = new Map<string, RegistroColunaPersistenciaDrift>();
3129
+ const repositorios = new Map<string, RegistroRepositorioPersistenciaDrift>();
3130
+
3131
+ for (const diretorio of diretorios) {
3132
+ const arquivos = await listarArquivosRecursivos(diretorio, [
3133
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
3134
+ ".py", ".dart", ".lua", ".cs", ".java", ".go", ".rs", ".cpp", ".cc", ".cxx", ".hpp", ".h",
3135
+ ".sql", ".psql", ".ddl", ".prisma",
3136
+ ]);
3137
+
3138
+ for (const arquivo of arquivos) {
3139
+ const codigo = await readFile(arquivo, "utf8");
3140
+ const recursos = arquivo.endsWith(".prisma")
3141
+ ? extrairRecursosPrisma(arquivo, codigo)
3142
+ : extrairRecursosPersistenciaCodigoVivo(arquivo, codigo);
3143
+
3144
+ for (const coluna of extrairColunasSqlDetalhadas(arquivo, codigo)) {
3145
+ registrarColunaPersistenciaDrift(colunas, coluna.origem, coluna.recurso, coluna.coluna, coluna.arquivo);
3146
+ }
3147
+ for (const coluna of extrairColunasPrismaDetalhadas(arquivo, codigo)) {
3148
+ registrarColunaPersistenciaDrift(colunas, coluna.origem, coluna.recurso, coluna.coluna, coluna.arquivo);
3149
+ }
3150
+ for (const coluna of extrairCamposMongoDetalhados(arquivo, codigo)) {
3151
+ registrarColunaPersistenciaDrift(colunas, coluna.origem, coluna.recurso, coluna.coluna, coluna.arquivo);
3152
+ }
3153
+ for (const coluna of extrairCamposRedisDetalhados(arquivo, codigo)) {
3154
+ registrarColunaPersistenciaDrift(colunas, coluna.origem, coluna.recurso, coluna.coluna, coluna.arquivo);
3155
+ }
3156
+ for (const coluna of extrairCamposArquivoLocalDetalhados(arquivo, codigo)) {
3157
+ registrarColunaPersistenciaDrift(colunas, coluna.origem, coluna.recurso, coluna.coluna, coluna.arquivo);
3158
+ }
3159
+
3160
+ registrarRepositoriosPorRecursos(repositorios, arquivo, codigo, recursos);
3161
+ }
3162
+ }
3163
+
3164
+ return {
3165
+ colunas: [...colunas.values()],
3166
+ repositorios: [...repositorios.values()],
3167
+ };
3168
+ }
3169
+
3170
+ function recursoDetalhadoCombina(
3171
+ recurso: string,
3172
+ esperado: RecursoEsperadoDrift,
3173
+ ): boolean {
3174
+ return variantesNomeRecursoDrift(recurso).some((variante) =>
3175
+ esperado.nomes.some((nome) => variantesNomeRecursoDrift(nome).includes(variante)));
3176
+ }
3177
+
3178
+ function deduplicarRecursosResolvidos(recursos: RecursoResolvido[]): RecursoResolvido[] {
3179
+ return [...new Map(recursos.map((recurso) =>
3180
+ [`${recurso.origem}:${recurso.tipo}:${recurso.nome}:${recurso.arquivo}:${recurso.simbolo ?? ""}`, recurso] as const)).values()];
3181
+ }
3182
+
3183
+ function normalizarArquivoDeclaradoDrift(valor: string): string {
3184
+ return normalizarFragmentoArquivo(valor);
3185
+ }
3186
+
3187
+ function arquivoCombinaDeclaradoDrift(arquivoReal: string, arquivoDeclarado: string): boolean {
3188
+ const real = normalizarArquivoDeclaradoDrift(arquivoReal);
3189
+ const declarado = normalizarArquivoDeclaradoDrift(arquivoDeclarado);
3190
+ return real === declarado || real.endsWith(declarado) || declarado.endsWith(real);
3191
+ }
3192
+
3193
+ function coletarArquivosPreferidosPersistenciaTask(
3194
+ task: IrTask,
3195
+ mapaImpl?: Map<string, SimboloResolvido>,
3196
+ ): Set<string> {
3197
+ const arquivos = new Set<string>();
3198
+ for (const vinculo of task.vinculos) {
3199
+ if (vinculo.arquivo) {
3200
+ arquivos.add(vinculo.arquivo);
3201
+ }
3202
+ if (vinculo.tipo === "arquivo" && vinculo.valor) {
3203
+ arquivos.add(vinculo.valor);
3204
+ }
3205
+ }
3206
+ if (mapaImpl) {
3207
+ for (const impl of task.implementacoesExternas) {
3208
+ const resolvido = mapaImpl.get(impl.caminho);
3209
+ if (resolvido?.arquivo) {
3210
+ arquivos.add(resolvido.arquivo);
3211
+ }
3212
+ }
3213
+ }
3214
+ return arquivos;
3215
+ }
3216
+
3217
+ function resolverPersistenciaLocalPorTask(
3218
+ mapaRecursos: Map<string, RecursoResolvido[]>,
3219
+ task: IrTask,
3220
+ ir: IrModulo,
3221
+ esperado: RecursoEsperadoDrift,
3222
+ mapaImpl?: Map<string, SimboloResolvido>,
3223
+ ): RecursoResolvido[] {
3224
+ const todosRecursos = deduplicarRecursosResolvidos([...mapaRecursos.values()].flat());
3225
+ const arquivosPreferidos = [...coletarArquivosPreferidosPersistenciaTask(task, mapaImpl)];
3226
+ const candidatosPorArquivo = arquivosPreferidos.length > 0
3227
+ ? todosRecursos.filter((recurso) =>
3228
+ recurso.origem === "arquivo"
3229
+ && arquivosPreferidos.some((arquivo) => arquivoCombinaDeclaradoDrift(recurso.arquivo, arquivo)))
3230
+ : [];
3231
+ const variantesAlvo = new Set(variantesNomeRecursoDrift(esperado.alvo));
3232
+ const normalizarBusca = (valor: string) => valor.toLowerCase().replace(/[^a-z0-9]+/g, "_");
3233
+ const pontuarCandidato = (recurso: RecursoResolvido) => {
3234
+ let score = 0;
3235
+ if (recursoResolvidoCombinaEsperado(recurso, esperado)) {
3236
+ score += 4;
3237
+ }
3238
+ if (variantesNomeRecursoDrift(recurso.nome).some((variacao) => variantesAlvo.has(variacao))) {
3239
+ score += 2;
3240
+ }
3241
+ const nomeNormalizado = normalizarBusca(recurso.nome);
3242
+ const alvoNormalizado = normalizarBusca(esperado.alvo);
3243
+ if (nomeNormalizado.includes(alvoNormalizado) || alvoNormalizado.includes(nomeNormalizado)) {
3244
+ score += 1;
3245
+ }
3246
+ return score;
3247
+ };
3248
+ const ordenarCandidatos = (candidatos: RecursoResolvido[]) =>
3249
+ [...candidatos].sort((a, b) =>
3250
+ pontuarCandidato(b) - pontuarCandidato(a)
3251
+ || a.nome.localeCompare(b.nome, "pt-BR")
3252
+ || a.arquivo.localeCompare(b.arquivo, "pt-BR"));
3253
+ if (candidatosPorArquivo.length > 0) {
3254
+ return ordenarCandidatos(candidatosPorArquivo);
3255
+ }
3256
+
3257
+ const termos = new Set([
3258
+ ...quebrarTermosEscopo(ir.nome),
3259
+ ...quebrarTermosEscopo(task.nome),
3260
+ ...quebrarTermosEscopo(esperado.alvo),
3261
+ ]);
3262
+ if (termos.size === 0) {
3263
+ return [];
3264
+ }
3265
+
3266
+ return ordenarCandidatos(todosRecursos.filter((recurso) =>
3267
+ recurso.origem === "arquivo"
3268
+ && [...termos].some((termo) =>
3269
+ variantesNomeRecursoDrift(recurso.nome).some((variacao) => variacao.includes(termo)))));
3270
+ }
3271
+
3272
+ function detalhePersistenciaCombinaOrigem(
3273
+ origemDetalhe: OrigemRecursoDrift,
3274
+ recursoReal?: RecursoResolvido,
3275
+ ): boolean {
3276
+ if (!recursoReal) {
3277
+ return true;
3278
+ }
3279
+ return origemDetalhe === recursoReal.origem;
3280
+ }
3281
+
3282
+ function localizarCompatibilidadePersistencia(
3283
+ bancos: IrBancoDados[],
3284
+ esperado: RecursoEsperadoDrift,
3285
+ recursoReal?: RecursoResolvido,
3286
+ ): {
3287
+ engine: OrigemRecursoDrift | "desconhecido";
3288
+ compatibilidade: RegistroPersistenciaRealDrift["compatibilidade"];
3289
+ motivoCompatibilidade?: string;
3290
+ tipo: TipoRecursoDrift;
3291
+ } {
3292
+ for (const banco of bancos) {
3293
+ for (const recurso of banco.resources) {
3294
+ if (!recursoPersistenciaCombinaAlvo(recurso, esperado.alvo)) {
3295
+ continue;
3296
+ }
3297
+ if (recursoReal?.origem === "arquivo") {
3298
+ return {
3299
+ engine: "arquivo",
3300
+ compatibilidade: "adaptado",
3301
+ motivoCompatibilidade: `Persistencia local/arquivo detectada no codigo vivo em vez do engine ${banco.engine}.`,
3302
+ tipo: recursoReal.tipo,
3303
+ };
3304
+ }
3305
+ const engine = banco.engine ?? normalizarOrigemParaEngine(recursoReal?.origem);
3306
+ const compatibilidade = engine
3307
+ ? recurso.compatibilidade.find((item) => item.engine === engine) ?? recurso.compatibilidade[0]
3308
+ : recurso.compatibilidade[0];
3309
+ return {
3310
+ engine: (engine ?? recursoReal?.origem ?? "desconhecido") as OrigemRecursoDrift | "desconhecido",
3311
+ compatibilidade: compatibilidade?.status ?? "desconhecida",
3312
+ motivoCompatibilidade: compatibilidade?.motivo,
3313
+ tipo: (recurso.resourceKind as TipoRecursoDrift) ?? recursoReal?.tipo ?? esperado.tiposAceitos[0] ?? "query",
3314
+ };
3315
+ }
3316
+ }
3317
+
3318
+ if (recursoReal?.origem === "arquivo") {
3319
+ return {
3320
+ engine: "arquivo",
3321
+ compatibilidade: "desconhecida",
3322
+ motivoCompatibilidade: "Persistencia local/arquivo detectada sem database vendor-first declarado.",
3323
+ tipo: recursoReal.tipo,
3324
+ };
3325
+ }
3326
+
3327
+ return {
3328
+ engine: recursoReal?.origem ?? esperado.origem ?? "desconhecido",
3329
+ compatibilidade: "desconhecida",
3330
+ tipo: recursoReal?.tipo ?? esperado.tiposAceitos[0] ?? "query",
3331
+ };
3332
+ }
3333
+
3334
+ export async function analisarPersistenciaReal(
3335
+ contexto: ContextoProjetoCarregado,
3336
+ mapaRecursos?: Map<string, RecursoResolvido[]>,
3337
+ detalhesPersistencia?: Awaited<ReturnType<typeof indexarPersistenciaDetalhada>>,
3338
+ opcoes?: OpcoesDriftLegado,
3339
+ mapaImpl?: Map<string, SimboloResolvido>,
3340
+ ): Promise<RegistroPersistenciaRealDrift[]> {
3341
+ const opcoesResolvidas = resolverOpcoesDrift(opcoes);
3342
+ const diretoriosCodigoAtivos = resolverDiretoriosCodigoEscopoReal(contexto, opcoesResolvidas);
3343
+ const mapa = mapaRecursos ?? construirMapaRecursos((await indexarPersistenciaDeclarativa(diretoriosCodigoAtivos)).recursos);
3344
+ const detalhes = detalhesPersistencia ?? await indexarPersistenciaDetalhada(diretoriosCodigoAtivos);
3345
+ const registros: RegistroPersistenciaRealDrift[] = [];
3346
+
3347
+ for (const item of contexto.modulosSelecionados) {
3348
+ const ir = item.resultado.ir;
3349
+ if (!ir) {
3350
+ continue;
3351
+ }
3352
+
3353
+ for (const task of ir.tasks) {
3354
+ for (const esperado of extrairRecursosEsperados(task, ir, mapa, mapaImpl)) {
3355
+ const correspondencias = esperado.nomes.flatMap((nome) =>
3356
+ variantesNomeRecursoDrift(nome).flatMap((variante) =>
3357
+ (mapa.get(variante) ?? []).filter((recurso) => recursoResolvidoCombinaEsperado(recurso, esperado))));
3358
+ let recursosReais = deduplicarRecursosResolvidos(correspondencias);
3359
+ const arquivosPreferidos = [...coletarArquivosPreferidosPersistenciaTask(task, mapaImpl)];
3360
+ if (recursosReais.length === 0) {
3361
+ recursosReais = resolverPersistenciaLocalPorTask(mapa, task, ir, esperado, mapaImpl);
3362
+ }
3363
+ const compatibilidade = localizarCompatibilidadePersistencia(ir.databases, esperado, recursosReais[0]);
3364
+ let colunas = [...new Set(detalhes.colunas
3365
+ .filter((coluna) =>
3366
+ detalhePersistenciaCombinaOrigem(coluna.origem, recursosReais[0])
3367
+ && recursoDetalhadoCombina(coluna.recurso, esperado))
3368
+ .map((coluna) => coluna.coluna))].sort((a, b) => a.localeCompare(b, "pt-BR"));
3369
+ let repositorios = [...new Set(detalhes.repositorios
3370
+ .filter((repositorio) =>
3371
+ detalhePersistenciaCombinaOrigem(repositorio.origem, recursosReais[0])
3372
+ && recursoDetalhadoCombina(repositorio.recurso, esperado))
3373
+ .map((repositorio) => repositorio.arquivo))].sort((a, b) => a.localeCompare(b, "pt-BR"));
3374
+ if (recursosReais.some((recurso) => recurso.origem === "arquivo") && arquivosPreferidos.length > 0) {
3375
+ if (colunas.length === 0) {
3376
+ colunas = [...new Set(detalhes.colunas
3377
+ .filter((coluna) =>
3378
+ coluna.origem === "arquivo"
3379
+ && arquivosPreferidos.some((arquivo) => arquivoCombinaDeclaradoDrift(coluna.arquivo, arquivo)))
3380
+ .map((coluna) => coluna.coluna))].sort((a, b) => a.localeCompare(b, "pt-BR"));
3381
+ }
3382
+ if (repositorios.length === 0) {
3383
+ repositorios = [...new Set(detalhes.repositorios
3384
+ .filter((repositorio) =>
3385
+ repositorio.origem === "arquivo"
3386
+ && arquivosPreferidos.some((arquivo) => arquivoCombinaDeclaradoDrift(repositorio.arquivo, arquivo)))
3387
+ .map((repositorio) => repositorio.arquivo))].sort((a, b) => a.localeCompare(b, "pt-BR"));
3388
+ }
3389
+ }
3390
+ const arquivos = [...new Set(recursosReais.map((recurso) => recurso.arquivo))].sort((a, b) => a.localeCompare(b, "pt-BR"));
3391
+
3392
+ registros.push({
3393
+ modulo: ir.nome,
3394
+ task: task.nome,
3395
+ alvo: esperado.alvo,
3396
+ engine: compatibilidade.engine,
3397
+ categoriaPersistencia: categorizarPersistenciaPorOrigem((compatibilidade.engine === "desconhecido" ? undefined : compatibilidade.engine) as OrigemRecursoDrift | undefined),
3398
+ tipo: compatibilidade.tipo,
3399
+ status: recursosReais.length === 0
3400
+ ? "divergente"
3401
+ : colunas.length > 0 || repositorios.length > 0
3402
+ ? "materializado"
3403
+ : "parcial",
3404
+ arquivos,
3405
+ colunas,
3406
+ repositorios,
3407
+ compatibilidade: compatibilidade.compatibilidade,
3408
+ motivoCompatibilidade: compatibilidade.motivoCompatibilidade,
3409
+ });
3410
+ }
3411
+ }
3412
+ }
3413
+
3414
+ return registros.sort((a, b) =>
3415
+ a.modulo.localeCompare(b.modulo, "pt-BR")
3416
+ || a.task.localeCompare(b.task, "pt-BR")
3417
+ || a.alvo.localeCompare(b.alvo, "pt-BR"));
3418
+ }
3419
+
3420
+ function normalizarCaminhoRota(caminho?: string): string {
3421
+ if (!caminho) {
3422
+ return "/";
3423
+ }
3424
+ const limpo = normalizarCaminhoFlask(caminho.trim().replace(/\s*\/\s*/g, "/"));
3425
+ const comBarra = limpo.startsWith("/") ? limpo : `/${limpo}`;
3426
+ const normalizado = comBarra.replace(/\/+/g, "/");
3427
+ return normalizado.endsWith("/") && normalizado !== "/" ? normalizado.slice(0, -1) : normalizado;
3428
+ }
3429
+
3430
+ function extrairFontesHttpTypeScript(fontesLegado: FonteLegado[]): Array<"nestjs" | "nextjs" | "firebase"> {
3431
+ return fontesLegado.filter((fonte): fonte is "nestjs" | "nextjs" | "firebase" =>
3432
+ fonte === "nestjs" || fonte === "nextjs" || fonte === "firebase");
3433
+ }
3434
+
3435
+ function extrairFontesHttpBackend(fontesLegado: FonteLegado[]): Array<"php" | "dotnet" | "java" | "go" | "rust"> {
3436
+ return fontesLegado.filter((fonte): fonte is "php" | "dotnet" | "java" | "go" | "rust" =>
3437
+ fonte === "php" || fonte === "dotnet" || fonte === "java" || fonte === "go" || fonte === "rust");
3438
+ }
3439
+
3440
+ function ultimoSegmentoSimbolico(caminho: string): string {
3441
+ const partes = caminho.split(".").filter(Boolean);
3442
+ return paraIdentificadorModulo(partes[partes.length - 1] ?? caminho);
3443
+ }
3444
+
3445
+ function pontuarCandidatoDeclarado(candidato: SimboloResolvido, origem: SimboloResolvido["origem"], caminhoDeclarado: string): SimboloCandidatoDrift | undefined {
3446
+ if (candidato.origem !== origem) {
3447
+ return undefined;
3448
+ }
3449
+
3450
+ const caminhoNormalizado = paraIdentificadorModulo(caminhoDeclarado.replace(/\./g, "_"));
3451
+ const candidatoNormalizado = paraIdentificadorModulo(candidato.caminho.replace(/\./g, "_"));
3452
+ const ultimoDeclarado = ultimoSegmentoSimbolico(caminhoDeclarado);
3453
+ const ultimoCandidato = ultimoSegmentoSimbolico(candidato.caminho);
3454
+ const prefixoDeclarado = caminhoDeclarado.split(".").slice(0, -1).join(".");
3455
+ const prefixoCandidato = candidato.caminho.split(".").slice(0, -1).join(".");
3456
+
3457
+ if (candidato.caminho === caminhoDeclarado) {
3458
+ return {
3459
+ origem: candidato.origem,
3460
+ caminho: candidato.caminho,
3461
+ arquivo: candidato.arquivo,
3462
+ simbolo: candidato.simbolo,
3463
+ confianca: "alta",
3464
+ motivo: "Caminho simbolico bate exatamente com o declarado.",
3465
+ };
3466
+ }
3467
+
3468
+ if (ultimoDeclarado && ultimoDeclarado === ultimoCandidato) {
3469
+ return {
3470
+ origem: candidato.origem,
3471
+ caminho: candidato.caminho,
3472
+ arquivo: candidato.arquivo,
3473
+ simbolo: candidato.simbolo,
3474
+ confianca: "alta",
3475
+ motivo: "Ultimo simbolo bate com a implementacao declarada.",
3476
+ };
3477
+ }
3478
+
3479
+ if (ultimoDeclarado && (candidatoNormalizado.includes(ultimoDeclarado) || caminhoNormalizado.includes(ultimoCandidato))) {
3480
+ return {
3481
+ origem: candidato.origem,
3482
+ caminho: candidato.caminho,
3483
+ arquivo: candidato.arquivo,
3484
+ simbolo: candidato.simbolo,
3485
+ confianca: "media",
3486
+ motivo: "Trecho relevante do caminho simbolico parece compativel com o declarado.",
3487
+ };
3488
+ }
3489
+
3490
+ if (prefixoDeclarado && prefixoDeclarado === prefixoCandidato) {
3491
+ return {
3492
+ origem: candidato.origem,
3493
+ caminho: candidato.caminho,
3494
+ arquivo: candidato.arquivo,
3495
+ simbolo: candidato.simbolo,
3496
+ confianca: "media",
3497
+ motivo: "Prefixo do caminho simbolico bate com a implementacao declarada; o simbolo final pode ter mudado.",
3498
+ };
3499
+ }
3500
+
3501
+ return undefined;
3502
+ }
3503
+
3504
+ function pontuarCandidatoPorTask(candidato: SimboloResolvido, task: string): SimboloCandidatoDrift | undefined {
3505
+ const taskNormalizada = paraIdentificadorModulo(task);
3506
+ const simboloNormalizado = paraIdentificadorModulo(candidato.simbolo.replace(/\./g, "_"));
3507
+ const caminhoNormalizado = paraIdentificadorModulo(candidato.caminho.replace(/\./g, "_"));
3508
+
3509
+ if (!taskNormalizada) {
3510
+ return undefined;
3511
+ }
3512
+
3513
+ if (simboloNormalizado === taskNormalizada || ultimoSegmentoSimbolico(candidato.caminho) === taskNormalizada) {
3514
+ return {
3515
+ origem: candidato.origem,
3516
+ caminho: candidato.caminho,
3517
+ arquivo: candidato.arquivo,
3518
+ simbolo: candidato.simbolo,
3519
+ confianca: "alta",
3520
+ motivo: "Nome da task bate com o simbolo encontrado no codigo vivo.",
3521
+ };
3522
+ }
3523
+
3524
+ if (simboloNormalizado.includes(taskNormalizada) || taskNormalizada.includes(simboloNormalizado) || caminhoNormalizado.includes(taskNormalizada)) {
3525
+ return {
3526
+ origem: candidato.origem,
3527
+ caminho: candidato.caminho,
3528
+ arquivo: candidato.arquivo,
3529
+ simbolo: candidato.simbolo,
3530
+ confianca: "media",
3531
+ motivo: "Nome da task parece compativel com o simbolo encontrado no codigo vivo.",
3532
+ };
3533
+ }
3534
+
3535
+ return undefined;
3536
+ }
3537
+
3538
+ function deduplicarCandidatos(candidatos: SimboloCandidatoDrift[]): SimboloCandidatoDrift[] {
3539
+ const mapa = new Map<string, SimboloCandidatoDrift>();
3540
+ for (const candidato of candidatos) {
3541
+ const chave = `${candidato.origem}:${candidato.caminho}:${candidato.arquivo}:${candidato.simbolo}`;
3542
+ const anterior = mapa.get(chave);
3543
+ if (!anterior || (anterior.confianca === "media" && candidato.confianca === "alta")) {
3544
+ mapa.set(chave, candidato);
3545
+ }
3546
+ }
3547
+ return [...mapa.values()];
3548
+ }
3549
+
3550
+ function ordenarCandidatos(candidatos: SimboloCandidatoDrift[]): SimboloCandidatoDrift[] {
3551
+ return [...candidatos].sort((a, b) => {
3552
+ if (a.confianca !== b.confianca) {
3553
+ return a.confianca === "alta" ? -1 : 1;
3554
+ }
3555
+ return a.caminho.localeCompare(b.caminho, "pt-BR");
3556
+ });
3557
+ }
3558
+
3559
+ function sugerirCandidatosParaImpl(
3560
+ simbolos: SimboloResolvido[],
3561
+ origem: SimboloResolvido["origem"],
3562
+ caminhoDeclarado: string,
3563
+ ): SimboloCandidatoDrift[] {
3564
+ return ordenarCandidatos(deduplicarCandidatos(
3565
+ simbolos
3566
+ .map((candidato) => pontuarCandidatoDeclarado(candidato, origem, caminhoDeclarado))
3567
+ .filter((item): item is SimboloCandidatoDrift => Boolean(item)),
3568
+ )).slice(0, 5);
3569
+ }
3570
+
3571
+ function sugerirCandidatosParaTaskSemImpl(simbolos: SimboloResolvido[], nomeTask: string): SimboloCandidatoDrift[] {
3572
+ return ordenarCandidatos(deduplicarCandidatos(
3573
+ simbolos
3574
+ .map((candidato) => pontuarCandidatoPorTask(candidato, nomeTask))
3575
+ .filter((item): item is SimboloCandidatoDrift => Boolean(item)),
3576
+ )).slice(0, 5);
3577
+ }
3578
+
3579
+ function escolherRotasEsperadas(task: IrTask, fontesLegado: FonteLegado[]): Array<"nestjs" | "fastapi" | "flask" | "nextjs" | "firebase" | "php" | "dotnet" | "java" | "go" | "rust"> {
3580
+ const fontesTs = extrairFontesHttpTypeScript(fontesLegado);
3581
+ const fontesBackend = extrairFontesHttpBackend(fontesLegado);
3582
+ const implTs = task.implementacoesExternas.find((impl) => impl.origem === "ts");
3583
+ if (implTs) {
3584
+ const esperadas = new Set<"nestjs" | "nextjs" | "firebase">();
3585
+ if (/\.route\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/i.test(implTs.caminho) || /\.route\./i.test(implTs.caminho)) {
3586
+ esperadas.add("nextjs");
3587
+ }
3588
+ if (/\bcontroller\b/i.test(implTs.caminho) && fontesTs.includes("nestjs")) {
3589
+ esperadas.add("nestjs");
3590
+ }
3591
+ if (fontesTs.includes("firebase") && /(apps\.worker|worker|sema_contract_bridge|health)/i.test(implTs.caminho)) {
3592
+ esperadas.add("firebase");
3593
+ }
3594
+ if (esperadas.size > 0) {
3595
+ return [...esperadas];
3596
+ }
3597
+ if (fontesTs.length > 0) {
3598
+ return fontesTs;
3599
+ }
3600
+ return ["nestjs", "nextjs", "firebase"];
3601
+ }
3602
+ if (task.implementacoesExternas.some((impl) => impl.origem === "py")) {
3603
+ const fontesPython = fontesLegado.filter((fonte): fonte is "fastapi" | "flask" => fonte === "fastapi" || fonte === "flask");
3604
+ if (fontesPython.length > 0) {
3605
+ return fontesPython;
3606
+ }
3607
+ return ["fastapi", "flask"];
3608
+ }
3609
+ const implPhp = task.implementacoesExternas.find((impl) => impl.origem === "php");
3610
+ if (implPhp) {
3611
+ return fontesBackend.includes("php") ? ["php"] : ["php"];
3612
+ }
3613
+ const implCs = task.implementacoesExternas.find((impl) => impl.origem === "cs");
3614
+ if (implCs) {
3615
+ return fontesBackend.includes("dotnet") ? ["dotnet"] : ["dotnet"];
3616
+ }
3617
+ const implJava = task.implementacoesExternas.find((impl) => impl.origem === "java");
3618
+ if (implJava) {
3619
+ return fontesBackend.includes("java") ? ["java"] : ["java"];
3620
+ }
3621
+ const implGo = task.implementacoesExternas.find((impl) => impl.origem === "go");
3622
+ if (implGo) {
3623
+ return fontesBackend.includes("go") ? ["go"] : ["go"];
3624
+ }
3625
+ const implRust = task.implementacoesExternas.find((impl) => impl.origem === "rust");
3626
+ if (implRust) {
3627
+ return fontesBackend.includes("rust") ? ["rust"] : ["rust"];
3628
+ }
3629
+ if (fontesTs.length > 0) {
3630
+ return fontesTs;
3631
+ }
3632
+ const fontesPython = fontesLegado.filter((fonte): fonte is "fastapi" | "flask" => fonte === "fastapi" || fonte === "flask");
3633
+ if (fontesPython.length > 0) {
3634
+ return fontesPython;
3635
+ }
3636
+ if (fontesBackend.length > 0) {
3637
+ return fontesBackend;
3638
+ }
3639
+ return [];
3640
+ }
3641
+
3642
+ function taskEhBridgeFirebase(task: IrTask): boolean {
3643
+ return task.implementacoesExternas.some((impl) =>
3644
+ impl.origem === "ts" && /sema_contract_bridge|collections?|apps\.worker/i.test(impl.caminho));
3645
+ }
3646
+
3647
+ function tiposAceitosParaRecursoPersistencia(recurso: IrRecursoPersistencia): TipoRecursoDrift[] {
3648
+ switch (recurso.resourceKind) {
3649
+ case "table":
3650
+ case "view":
3651
+ case "query":
3652
+ case "index":
3653
+ case "collection":
3654
+ case "document":
3655
+ case "keyspace":
3656
+ case "stream":
3657
+ return [recurso.resourceKind];
3658
+ default:
3659
+ return [];
3660
+ }
3661
+ }
3662
+
3663
+ function nomesRecursoPersistencia(recurso: IrRecursoPersistencia): string[] {
3664
+ return [...new Set([
3665
+ recurso.nome,
3666
+ recurso.table,
3667
+ recurso.collection,
3668
+ recurso.entity,
3669
+ recurso.path,
3670
+ recurso.surface,
3671
+ ].filter((item): item is string => Boolean(item)))];
3672
+ }
3673
+
3674
+ function recursoPersistenciaCombinaAlvo(recurso: IrRecursoPersistencia, alvo: string): boolean {
3675
+ const alvoVariantes = new Set(variantesNomeRecursoDrift(alvo));
3676
+ if (alvoVariantes.size === 0) {
3677
+ return false;
3678
+ }
3679
+
3680
+ return nomesRecursoPersistencia(recurso).some((nome) =>
3681
+ variantesNomeRecursoDrift(nome).some((variacao) => alvoVariantes.has(variacao)));
3682
+ }
3683
+
3684
+ function taskSugerePersistenciaSemBanco(
3685
+ task: IrTask,
3686
+ mapaRecursos?: Map<string, RecursoResolvido[]>,
3687
+ mapaImpl?: Map<string, SimboloResolvido>,
3688
+ ): boolean {
3689
+ if (task.vinculos.some((vinculo) =>
3690
+ /(?:repository|repositories|repositorio|repo|store|storage|persist|cache)/i.test(
3691
+ `${vinculo.valor} ${vinculo.arquivo ?? ""} ${vinculo.simbolo ?? ""}`,
3692
+ ))) {
3693
+ return true;
3694
+ }
3695
+ if (task.implementacoesExternas.some((impl) =>
3696
+ /(?:repository|repositories|repositorio|repo|store|storage|persist|cache)/i.test(impl.caminho))) {
3697
+ return true;
3698
+ }
3699
+ if (!mapaRecursos || !mapaImpl) {
3700
+ return false;
3701
+ }
3702
+ const arquivosPreferidos = [...coletarArquivosPreferidosPersistenciaTask(task, mapaImpl)];
3703
+ if (arquivosPreferidos.length === 0) {
3704
+ return false;
3705
+ }
3706
+ const recursos = deduplicarRecursosResolvidos([...mapaRecursos.values()].flat());
3707
+ return recursos.some((recurso) =>
3708
+ recurso.origem === "arquivo"
3709
+ && arquivosPreferidos.some((arquivo) => arquivoCombinaDeclaradoDrift(recurso.arquivo, arquivo)));
3710
+ }
3711
+
3712
+ function extrairRecursosEsperados(
3713
+ task: IrTask,
3714
+ ir: IrModulo,
3715
+ mapaRecursos?: Map<string, RecursoResolvido[]>,
3716
+ mapaImpl?: Map<string, SimboloResolvido>,
3717
+ ): RecursoEsperadoDrift[] {
3718
+ const esperados = new Map<string, RecursoEsperadoDrift>();
3719
+ const registrar = (esperado: RecursoEsperadoDrift) => {
3720
+ const chave = `${esperado.origem ?? "qualquer"}:${esperado.tiposAceitos.join(",")}:${esperado.nomes.join("|")}:${esperado.alvo}`;
3721
+ if (!esperados.has(chave)) {
3722
+ esperados.set(chave, esperado);
3723
+ }
3724
+ };
3725
+
3726
+ if (taskEhBridgeFirebase(task)) {
3727
+ for (const efeito of task.efeitosEstruturados.filter((item) => item.categoria === "persistencia" && Boolean(item.alvo))) {
3728
+ registrar({
3729
+ categoria: "persistencia",
3730
+ alvo: efeito.alvo,
3731
+ origem: "firebase",
3732
+ tiposAceitos: ["colecao"],
3733
+ nomes: [efeito.alvo],
3734
+ });
3735
+ }
3736
+ }
3737
+
3738
+ const efeitosPersistencia = task.efeitosEstruturados.filter((efeito) =>
3739
+ ["persistencia", "db.read", "db.write"].includes(efeito.categoria) && Boolean(efeito.alvo));
3740
+ if (efeitosPersistencia.length === 0) {
3741
+ return [...esperados.values()];
3742
+ }
3743
+
3744
+ if (ir.databases.length === 0) {
3745
+ const sugerePersistenciaLocal = taskSugerePersistenciaSemBanco(task, mapaRecursos, mapaImpl);
3746
+ for (const efeito of efeitosPersistencia) {
3747
+ if (!sugerePersistenciaLocal) {
3748
+ continue;
3749
+ }
3750
+ if ([...esperados.values()].some((item) => item.alvo === efeito.alvo)) {
3751
+ continue;
3752
+ }
3753
+ registrar({
3754
+ categoria: "persistencia",
3755
+ alvo: efeito.alvo,
3756
+ tiposAceitos: ["table", "collection", "document", "keyspace", "stream", "view", "query", "index", "arquivo_local"],
3757
+ nomes: [efeito.alvo],
3758
+ });
3759
+ }
3760
+ return [...esperados.values()];
3761
+ }
3762
+
3763
+ for (const efeito of efeitosPersistencia) {
3764
+ for (const database of ir.databases) {
3765
+ for (const recurso of database.resources) {
3766
+ const tiposAceitos = tiposAceitosParaRecursoPersistencia(recurso);
3767
+ if (tiposAceitos.length === 0 || !recursoPersistenciaCombinaAlvo(recurso, efeito.alvo)) {
3768
+ continue;
3769
+ }
3770
+ registrar({
3771
+ categoria: "persistencia",
3772
+ alvo: efeito.alvo,
3773
+ origem: database.engine,
3774
+ tiposAceitos,
3775
+ nomes: nomesRecursoPersistencia(recurso),
3776
+ });
3777
+ }
3778
+ }
3779
+ }
3780
+
3781
+ return [...esperados.values()];
3782
+ }
3783
+
3784
+ function construirMapaRecursos(recursos: RecursoResolvido[]): Map<string, RecursoResolvido[]> {
3785
+ const mapa = new Map<string, RecursoResolvido[]>();
3786
+ for (const recurso of recursos) {
3787
+ for (const variante of variantesNomeRecursoDrift(recurso.nome)) {
3788
+ const existentes = mapa.get(variante) ?? [];
3789
+ if (!existentes.some((item) =>
3790
+ item.origem === recurso.origem
3791
+ && item.tipo === recurso.tipo
3792
+ && item.arquivo === recurso.arquivo
3793
+ && item.nome === recurso.nome
3794
+ && item.simbolo === recurso.simbolo)) {
3795
+ existentes.push(recurso);
3796
+ mapa.set(variante, existentes);
3797
+ }
3798
+ }
3799
+ }
3800
+ return mapa;
3801
+ }
3802
+
3803
+ function recursoResolvidoCombinaEsperado(recurso: RecursoResolvido, esperado: RecursoEsperadoDrift): boolean {
3804
+ if (esperado.origem && recurso.origem !== esperado.origem) {
3805
+ return false;
3806
+ }
3807
+ if (esperado.tiposAceitos.length > 0 && !esperado.tiposAceitos.includes(recurso.tipo)) {
3808
+ return false;
3809
+ }
3810
+ const recursoVariantes = new Set(variantesNomeRecursoDrift(recurso.nome));
3811
+ return esperado.nomes.some((nome) =>
3812
+ variantesNomeRecursoDrift(nome).some((variante) => recursoVariantes.has(variante)));
3813
+ }
3814
+
3815
+ function resolverRecursoEsperado(
3816
+ mapaRecursos: Map<string, RecursoResolvido[]>,
3817
+ esperado: RecursoEsperadoDrift,
3818
+ arquivosPreferidos?: Set<string>,
3819
+ ): RecursoResolvido | undefined {
3820
+ const candidatos = new Map<string, RecursoResolvido>();
3821
+ for (const nome of esperado.nomes) {
3822
+ for (const variante of variantesNomeRecursoDrift(nome)) {
3823
+ for (const recurso of mapaRecursos.get(variante) ?? []) {
3824
+ if (recursoResolvidoCombinaEsperado(recurso, esperado)) {
3825
+ candidatos.set(`${recurso.origem}:${recurso.tipo}:${recurso.nome}:${recurso.arquivo}:${recurso.simbolo ?? ""}`, recurso);
3826
+ }
3827
+ }
3828
+ }
3829
+ }
3830
+
3831
+ return [...candidatos.values()].sort((a, b) =>
3832
+ Number(Boolean(arquivosPreferidos?.has(b.arquivo))) - Number(Boolean(arquivosPreferidos?.has(a.arquivo)))
3833
+ || a.arquivo.localeCompare(b.arquivo, "pt-BR")
3834
+ || a.nome.localeCompare(b.nome, "pt-BR"))[0];
3835
+ }
3836
+
3837
+ function coletarVinculosIr(ir: IrModulo): Array<{ donoTipo: RegistroVinculoDrift["donoTipo"]; dono: string; vinculo: IrVinculo }> {
3838
+ return [
3839
+ ...ir.vinculos.map((vinculo) => ({ donoTipo: "modulo" as const, dono: ir.nome, vinculo })),
3840
+ ...ir.tasks.flatMap((task) => task.vinculos.map((vinculo) => ({ donoTipo: "task" as const, dono: task.nome, vinculo }))),
3841
+ ...ir.flows.flatMap((flow: IrFlow) => flow.vinculos.map((vinculo) => ({ donoTipo: "flow" as const, dono: flow.nome, vinculo }))),
3842
+ ...ir.routes.flatMap((route: IrRoute) => route.vinculos.map((vinculo) => ({ donoTipo: "route" as const, dono: route.nome, vinculo }))),
3843
+ ...ir.superficies.flatMap((superficie: IrSuperficie) => superficie.vinculos.map((vinculo) => ({ donoTipo: "superficie" as const, dono: `${superficie.tipo}:${superficie.nome}`, vinculo }))),
3844
+ ];
3845
+ }
3846
+
3847
+ export async function analisarDriftLegado(
3848
+ contexto: ContextoProjetoCarregado,
3849
+ opcoes?: OpcoesDriftLegado,
3850
+ ): Promise<ResultadoDrift> {
3851
+ const opcoesResolvidas = resolverOpcoesDrift(opcoes);
3852
+ const configuracaoEscopo: ConfiguracaoEscopoDriftAplicada = {
3853
+ escopo: opcoesResolvidas.escopo,
3854
+ ignorarWorktrees: opcoesResolvidas.ignorarWorktrees,
3855
+ ignorarConsumidoresLaterais: opcoesResolvidas.ignorarConsumidoresLaterais,
3856
+ termosEscopo: extrairTermosEscopoDrift(contexto, opcoesResolvidas.escopo),
3857
+ };
3858
+ const diretoriosIgnoradosAnteriores = diretoriosIgnoradosAtivos;
3859
+ diretoriosIgnoradosAtivos = resolverDiretoriosIgnoradosAtivos(opcoesResolvidas);
3860
+
3861
+ try {
3862
+ const diretoriosCodigoAtivos = resolverDiretoriosCodigoEscopoReal(contexto, configuracaoEscopo);
3863
+ const arquivosCodigo = await indexarArquivosCodigo(diretoriosCodigoAtivos);
3864
+ const indexTs = await indexarTypeScript(diretoriosCodigoAtivos);
3865
+ const indexPy = await indexarPython(diretoriosCodigoAtivos);
3866
+ const indexDart = await indexarDart(diretoriosCodigoAtivos);
3867
+ const indexLua = await indexarLua(diretoriosCodigoAtivos);
3868
+ const indexPhp = await indexarPhp(diretoriosCodigoAtivos);
3869
+ const indexDotnet = await indexarDotnet(diretoriosCodigoAtivos);
3870
+ const indexJava = await indexarJava(diretoriosCodigoAtivos);
3871
+ const indexGo = await indexarGo(diretoriosCodigoAtivos);
3872
+ const indexRust = await indexarRust(diretoriosCodigoAtivos);
3873
+ const indexPersistencia = await indexarPersistenciaDeclarativa(diretoriosCodigoAtivos);
3874
+ const detalhesPersistencia = await indexarPersistenciaDetalhada(diretoriosCodigoAtivos);
3875
+ const indexCpp = await indexarCpp(diretoriosCodigoAtivos);
3876
+ const todosSimbolos = [
3877
+ ...indexTs.simbolos,
3878
+ ...indexPy.simbolos,
3879
+ ...indexDart.simbolos,
3880
+ ...indexLua.simbolos,
3881
+ ...indexPhp.simbolos,
3882
+ ...indexDotnet.simbolos,
3883
+ ...indexJava.simbolos,
3884
+ ...indexGo.simbolos,
3885
+ ...indexRust.simbolos,
3886
+ ...indexCpp.simbolos,
3887
+ ];
3888
+ const mapaImpl = new Map<string, SimboloResolvido>([
3889
+ ...indexTs.simbolos.map((item) => [item.caminho, item] as const),
3890
+ ...indexPy.simbolos.map((item) => [item.caminho, item] as const),
3891
+ ...indexDart.simbolos.map((item) => [item.caminho, item] as const),
3892
+ ...indexLua.simbolos.map((item) => [item.caminho, item] as const),
3893
+ ...indexPhp.simbolos.map((item) => [item.caminho, item] as const),
3894
+ ...indexDotnet.simbolos.map((item) => [item.caminho, item] as const),
3895
+ ...indexJava.simbolos.map((item) => [item.caminho, item] as const),
3896
+ ...indexGo.simbolos.map((item) => [item.caminho, item] as const),
3897
+ ...indexRust.simbolos.map((item) => [item.caminho, item] as const),
3898
+ ...indexCpp.simbolos.map((item) => [item.caminho, item] as const),
3899
+ ]);
3900
+ const todosRecursos = [
3901
+ ...indexTs.recursos,
3902
+ ...indexPy.recursos,
3903
+ ...indexDart.recursos,
3904
+ ...indexLua.recursos,
3905
+ ...indexPhp.recursos,
3906
+ ...indexDotnet.recursos,
3907
+ ...indexJava.recursos,
3908
+ ...indexGo.recursos,
3909
+ ...indexRust.recursos,
3910
+ ...indexCpp.recursos,
3911
+ ...indexPersistencia.recursos,
3912
+ ];
3913
+ const mapaRecursos = construirMapaRecursos(todosRecursos);
3914
+ const todasRotasIndexadas = [
3915
+ ...indexTs.rotas,
3916
+ ...indexPy.rotas,
3917
+ ...indexDart.rotas,
3918
+ ...indexPhp.rotas,
3919
+ ...indexDotnet.rotas,
3920
+ ...indexJava.rotas,
3921
+ ...indexGo.rotas,
3922
+ ...indexRust.rotas,
3923
+ ];
3924
+ const todosArquivosConhecidos = [...new Set([
3925
+ ...arquivosCodigo,
3926
+ ...todosSimbolos.map((item) => item.arquivo),
3927
+ ...todasRotasIndexadas.map((item) => item.arquivo),
3928
+ ...todosRecursos.map((item) => item.arquivo),
3929
+ ])].sort((a, b) => a.localeCompare(b, "pt-BR"));
3930
+
3931
+ const implsValidos: RegistroImplDrift[] = [];
3932
+ const implsQuebrados: RegistroImplDrift[] = [];
3933
+ const vinculosValidos: RegistroVinculoDrift[] = [];
3934
+ const vinculosQuebrados: RegistroVinculoDrift[] = [];
3935
+ const rotasDivergentes: RegistroRotaDivergente[] = [];
3936
+ const recursosValidos: RegistroRecursoDrift[] = [];
3937
+ const recursosDivergentes: RegistroRecursoDrift[] = [];
3938
+ const diagnosticos: DiagnosticoDrift[] = [];
3939
+ const tasksResumo: ResultadoDrift["tasks"] = [];
3940
+ const taskPorChave = new Map<string, IrTask>();
3941
+ const guardrailsPorTask = new Map<string, {
3942
+ publica: boolean;
3943
+ sensivel: boolean;
3944
+ auth: boolean;
3945
+ authz: boolean;
3946
+ dados: boolean;
3947
+ audit: boolean;
3948
+ segredos: boolean;
3949
+ forbidden: boolean;
3950
+ dadosSensiveis: boolean;
3951
+ efeitoPrivilegiado: boolean;
3952
+ exigeSegredos: boolean;
3953
+ }>();
3954
+ const resumoVinculosPorTask = new Map<string, { validos: number; quebrados: number; arquivos: Set<string> }>();
3955
+ const arquivosAncoraHerdadosPorTask = new Map<string, Set<string>>();
3956
+
3957
+ for (const item of contexto.modulosSelecionados) {
3958
+ const ir = item.resultado.ir;
3959
+ if (!ir) {
3960
+ continue;
3961
+ }
3962
+ const superficiesPorChave = new Map<string, IrSuperficie>(
3963
+ ir.superficies.map((superficie) => [`${superficie.tipo}:${superficie.nome}`, superficie]),
3964
+ );
3965
+ const routesPorNome = new Map<string, IrRoute>(ir.routes.map((route) => [route.nome, route]));
3966
+ const flowsPorNome = new Map<string, IrFlow>(ir.flows.map((flow) => [flow.nome, flow]));
3967
+ const registrarArquivoAncoraHerdado = (taskNome: string, arquivo?: string) => {
3968
+ if (!arquivo) {
3969
+ return;
3970
+ }
3971
+ const chaveTask = `${ir.nome}:${taskNome}`;
3972
+ const arquivos = arquivosAncoraHerdadosPorTask.get(chaveTask) ?? new Set<string>();
3973
+ arquivos.add(arquivo);
3974
+ arquivosAncoraHerdadosPorTask.set(chaveTask, arquivos);
3975
+ };
3976
+ for (const task of ir.tasks) {
3977
+ guardrailsPorTask.set(`${ir.nome}:${task.nome}`, {
3978
+ publica: false,
3979
+ sensivel: calcularRiscoOperacional(task) === "alto",
3980
+ auth: task.auth.explicita,
3981
+ authz: task.authz.explicita,
3982
+ dados: task.dados.explicita,
3983
+ audit: task.audit.explicita,
3984
+ segredos: task.segredos.explicita,
3985
+ forbidden: task.forbidden.explicita,
3986
+ dadosSensiveis: Boolean(
3987
+ task.dados.classificacaoPadrao && ["pii", "financeiro", "credencial", "segredo"].includes(task.dados.classificacaoPadrao)
3988
+ || task.dados.campos.some((campo) => ["pii", "financeiro", "credencial", "segredo"].includes(campo.classificacao))
3989
+ ),
3990
+ efeitoPrivilegiado: task.efeitosEstruturados.some((efeito) =>
3991
+ ["db.read", "db.write", "queue.publish", "queue.consume", "fs.read", "fs.write", "network.egress", "secret.read", "shell.exec"].includes(efeito.categoria)
3992
+ || ["alta", "critica"].includes(efeito.criticidade ?? ""),
3993
+ ),
3994
+ exigeSegredos: task.efeitosEstruturados.some((efeito) => efeito.categoria === "secret.read")
3995
+ || Boolean(
3996
+ task.dados.classificacaoPadrao && ["credencial", "segredo"].includes(task.dados.classificacaoPadrao)
3997
+ || task.dados.campos.some((campo) => ["credencial", "segredo"].includes(campo.classificacao))
3998
+ ),
3999
+ });
4000
+ }
4001
+ for (const route of ir.routes) {
4002
+ if (!route.task || route.perfilCompatibilidade !== "publico") {
4003
+ continue;
4004
+ }
4005
+ const guardrails = guardrailsPorTask.get(`${ir.nome}:${route.task}`);
4006
+ if (guardrails) {
4007
+ guardrails.publica = true;
4008
+ guardrails.auth = guardrails.auth || route.auth.explicita;
4009
+ guardrails.authz = guardrails.authz || route.authz.explicita;
4010
+ guardrails.dados = guardrails.dados || route.dados.explicita;
4011
+ guardrails.audit = guardrails.audit || route.audit.explicita;
4012
+ guardrails.segredos = guardrails.segredos || route.segredos.explicita;
4013
+ guardrails.forbidden = guardrails.forbidden || route.forbidden.explicita;
4014
+ guardrails.dadosSensiveis = guardrails.dadosSensiveis || Boolean(
4015
+ route.dados.classificacaoPadrao && ["pii", "financeiro", "credencial", "segredo"].includes(route.dados.classificacaoPadrao)
4016
+ || route.dados.campos.some((campo) => ["pii", "financeiro", "credencial", "segredo"].includes(campo.classificacao))
4017
+ );
4018
+ guardrails.efeitoPrivilegiado = guardrails.efeitoPrivilegiado || route.efeitosPublicos.some((efeito) =>
4019
+ ["db.read", "db.write", "queue.publish", "queue.consume", "fs.read", "fs.write", "network.egress", "secret.read", "shell.exec"].includes(efeito.categoria)
4020
+ || ["alta", "critica"].includes(efeito.criticidade ?? ""),
4021
+ );
4022
+ guardrails.exigeSegredos = guardrails.exigeSegredos || route.efeitosPublicos.some((efeito) => efeito.categoria === "secret.read")
4023
+ || Boolean(
4024
+ route.dados.classificacaoPadrao && ["credencial", "segredo"].includes(route.dados.classificacaoPadrao)
4025
+ || route.dados.campos.some((campo) => ["credencial", "segredo"].includes(campo.classificacao))
4026
+ );
4027
+ }
4028
+ }
4029
+ for (const superficie of ir.superficies) {
4030
+ if (!superficie.task || superficie.perfilCompatibilidade !== "publico") {
4031
+ continue;
4032
+ }
4033
+ const guardrails = guardrailsPorTask.get(`${ir.nome}:${superficie.task}`);
4034
+ if (guardrails) {
4035
+ guardrails.publica = true;
4036
+ guardrails.auth = guardrails.auth || superficie.auth.explicita;
4037
+ guardrails.authz = guardrails.authz || superficie.authz.explicita;
4038
+ guardrails.dados = guardrails.dados || superficie.dados.explicita;
4039
+ guardrails.audit = guardrails.audit || superficie.audit.explicita;
4040
+ guardrails.segredos = guardrails.segredos || superficie.segredos.explicita;
4041
+ guardrails.forbidden = guardrails.forbidden || superficie.forbidden.explicita;
4042
+ guardrails.dadosSensiveis = guardrails.dadosSensiveis || Boolean(
4043
+ superficie.dados.classificacaoPadrao && ["pii", "financeiro", "credencial", "segredo"].includes(superficie.dados.classificacaoPadrao)
4044
+ || superficie.dados.campos.some((campo) => ["pii", "financeiro", "credencial", "segredo"].includes(campo.classificacao))
4045
+ );
4046
+ guardrails.efeitoPrivilegiado = guardrails.efeitoPrivilegiado || superficie.effects.some((efeito) =>
4047
+ ["db.read", "db.write", "queue.publish", "queue.consume", "fs.read", "fs.write", "network.egress", "secret.read", "shell.exec"].includes(efeito.categoria)
4048
+ || ["alta", "critica"].includes(efeito.criticidade ?? ""),
4049
+ );
4050
+ guardrails.exigeSegredos = guardrails.exigeSegredos || superficie.effects.some((efeito) => efeito.categoria === "secret.read")
4051
+ || Boolean(
4052
+ superficie.dados.classificacaoPadrao && ["credencial", "segredo"].includes(superficie.dados.classificacaoPadrao)
4053
+ || superficie.dados.campos.some((campo) => ["credencial", "segredo"].includes(campo.classificacao))
4054
+ );
4055
+ }
4056
+ }
4057
+
4058
+ for (const task of ir.tasks) {
4059
+ taskPorChave.set(`${ir.nome}:${task.nome}`, task);
4060
+ let validos = 0;
4061
+ let quebrados = 0;
4062
+ const arquivosReferenciados = new Set<string>();
4063
+ const simbolosReferenciados = new Set<string>();
4064
+ const candidatosTask = new Map<string, SimboloCandidatoDrift>();
4065
+
4066
+ if (task.implementacoesExternas.length === 0) {
4067
+ for (const candidato of sugerirCandidatosParaTaskSemImpl(todosSimbolos, task.nome)) {
4068
+ candidatosTask.set(`${candidato.origem}:${candidato.caminho}:${candidato.arquivo}:${candidato.simbolo}`, candidato);
4069
+ }
4070
+ diagnosticos.push({
4071
+ tipo: "task_sem_impl",
4072
+ modulo: ir.nome,
4073
+ task: task.nome,
4074
+ mensagem: `Task "${task.nome}" ainda nao foi ligada a nenhuma implementacao externa.`,
4075
+ });
4076
+ }
4077
+
4078
+ for (const impl of task.implementacoesExternas) {
4079
+ const resolvido = mapaImpl.get(impl.caminho);
4080
+ const registro: RegistroImplDrift = {
4081
+ modulo: ir.nome,
4082
+ task: task.nome,
4083
+ origem: impl.origem,
4084
+ caminho: impl.caminho,
4085
+ arquivo: resolvido?.arquivo,
4086
+ simbolo: resolvido?.simbolo,
4087
+ caminhoResolvido: resolvido?.caminho,
4088
+ status: resolvido ? "resolvido" : "quebrado",
4089
+ };
4090
+
4091
+ if (resolvido) {
4092
+ arquivosReferenciados.add(resolvido.arquivo);
4093
+ simbolosReferenciados.add(resolvido.simbolo);
4094
+ implsValidos.push(registro);
4095
+ validos += 1;
4096
+ } else {
4097
+ registro.candidatos = sugerirCandidatosParaImpl(todosSimbolos, impl.origem, impl.caminho);
4098
+ for (const candidato of registro.candidatos) {
4099
+ candidatosTask.set(`${candidato.origem}:${candidato.caminho}:${candidato.arquivo}:${candidato.simbolo}`, candidato);
4100
+ }
4101
+ implsQuebrados.push(registro);
4102
+ quebrados += 1;
4103
+ diagnosticos.push({
4104
+ tipo: "impl_quebrado",
4105
+ modulo: ir.nome,
4106
+ task: task.nome,
4107
+ mensagem: `Implementacao externa "${impl.origem}:${impl.caminho}" nao foi encontrada nos diretorios de codigo vivos.`,
4108
+ });
4109
+ }
4110
+ }
4111
+
4112
+ tasksResumo.push({
4113
+ modulo: ir.nome,
4114
+ task: task.nome,
4115
+ impls: task.implementacoesExternas.length,
4116
+ implsValidos: validos,
4117
+ implsQuebrados: quebrados,
4118
+ semImplementacao: task.implementacoesExternas.length === 0,
4119
+ scoreSemantico: 0,
4120
+ confiancaVinculo: "baixa",
4121
+ riscoOperacional: "baixo",
4122
+ lacunas: [],
4123
+ ancoragemVinculo: "ausente",
4124
+ arquivosReferenciados: [...arquivosReferenciados].sort((a, b) => a.localeCompare(b, "pt-BR")),
4125
+ arquivosAncoraHerdados: [],
4126
+ arquivosProvaveisEditar: [],
4127
+ simbolosReferenciados: [...simbolosReferenciados].sort((a, b) => a.localeCompare(b, "pt-BR")),
4128
+ candidatosImpl: ordenarCandidatos([...candidatosTask.values()]).slice(0, 5),
4129
+ checksSugeridos: [],
4130
+ });
4131
+
4132
+ for (const recursoEsperado of extrairRecursosEsperados(task, ir, mapaRecursos, mapaImpl)) {
4133
+ let resolvido = resolverRecursoEsperado(mapaRecursos, recursoEsperado, arquivosReferenciados);
4134
+ if (!resolvido) {
4135
+ resolvido = resolverPersistenciaLocalPorTask(mapaRecursos, task, ir, recursoEsperado, mapaImpl)[0];
4136
+ }
4137
+ const registro: RegistroRecursoDrift = {
4138
+ modulo: ir.nome,
4139
+ task: task.nome,
4140
+ categoria: recursoEsperado.categoria,
4141
+ alvo: recursoEsperado.alvo,
4142
+ arquivo: resolvido?.arquivo ?? "",
4143
+ origem: resolvido?.origem ?? recursoEsperado.origem ?? "firebase",
4144
+ tipo: resolvido?.tipo ?? recursoEsperado.tiposAceitos[0] ?? "query",
4145
+ status: resolvido ? "resolvido" : "divergente",
4146
+ };
4147
+
4148
+ if (resolvido) {
4149
+ registro.arquivo = resolvido.arquivo;
4150
+ recursosValidos.push(registro);
4151
+ } else {
4152
+ recursosDivergentes.push(registro);
4153
+ const escopo = recursoEsperado.origem ? `${recursoEsperado.origem}` : "persistencia declarada";
4154
+ diagnosticos.push({
4155
+ tipo: "recurso_divergente",
4156
+ modulo: ir.nome,
4157
+ task: task.nome,
4158
+ mensagem: `Recurso vivo "${recursoEsperado.alvo}" nao foi encontrado no codigo legado para ${escopo}.`,
4159
+ });
4160
+ }
4161
+ }
4162
+ }
4163
+
4164
+ for (const route of ir.routes) {
4165
+ const taskAssociada = ir.tasks.find((task) => task.nome === route.task);
4166
+ const esperadas = escolherRotasEsperadas(taskAssociada ?? {
4167
+ nome: "",
4168
+ input: [],
4169
+ output: [],
4170
+ rules: [],
4171
+ regrasEstruturadas: [],
4172
+ effects: [],
4173
+ efeitosEstruturados: [],
4174
+ implementacoesExternas: [],
4175
+ vinculos: [],
4176
+ execucao: {
4177
+ idempotencia: false,
4178
+ timeout: "padrao",
4179
+ retry: "nenhum",
4180
+ compensacao: "nenhuma",
4181
+ criticidadeOperacional: "media",
4182
+ explicita: false,
4183
+ },
4184
+ auth: {
4185
+ explicita: false,
4186
+ },
4187
+ authz: {
4188
+ explicita: false,
4189
+ papeis: [],
4190
+ escopos: [],
4191
+ },
4192
+ dados: {
4193
+ explicita: false,
4194
+ campos: [],
4195
+ },
4196
+ audit: {
4197
+ explicita: false,
4198
+ },
4199
+ segredos: {
4200
+ explicita: false,
4201
+ itens: [],
4202
+ },
4203
+ forbidden: {
4204
+ explicita: false,
4205
+ regras: [],
4206
+ },
4207
+ guarantees: [],
4208
+ garantiasEstruturadas: [],
4209
+ errors: {},
4210
+ errosDetalhados: [],
4211
+ perfilCompatibilidade: "interno",
4212
+ resumoAgente: {
4213
+ riscos: [],
4214
+ checks: [],
4215
+ entidadesAfetadas: [],
4216
+ superficiesPublicas: [],
4217
+ mutacoesPrevistas: [],
4218
+ },
4219
+ tests: [],
4220
+ }, contexto.fontesLegado);
4221
+
4222
+ if (!esperadas.length || !route.metodo || !route.caminho) {
4223
+ continue;
4224
+ }
4225
+
4226
+ const encontradas = todasRotasIndexadas.filter((rotaResolvida) =>
4227
+ rotaResolvida.origem !== "nextjs-consumer"
4228
+ && rotaResolvida.origem !== "react-vite-consumer"
4229
+ && rotaResolvida.origem !== "angular-consumer"
4230
+ && rotaResolvida.origem !== "flutter-consumer"
4231
+ && esperadas.includes(rotaResolvida.origem));
4232
+ const combina = encontradas.some((rotaResolvida) =>
4233
+ rotaResolvida.metodo === route.metodo
4234
+ && normalizarCaminhoRota(rotaResolvida.caminho) === normalizarCaminhoRota(route.caminho));
4235
+
4236
+ if (!combina) {
4237
+ const registro = {
4238
+ modulo: ir.nome,
4239
+ route: route.nome,
4240
+ metodo: route.metodo,
4241
+ caminho: route.caminho,
4242
+ motivo: `Nenhuma rota publica ${route.metodo} ${route.caminho} foi encontrada no codigo legado para o framework esperado.`,
4243
+ };
4244
+ rotasDivergentes.push(registro);
4245
+ diagnosticos.push({
4246
+ tipo: "rota_divergente",
4247
+ modulo: ir.nome,
4248
+ route: route.nome,
4249
+ mensagem: registro.motivo,
4250
+ });
4251
+ }
4252
+ }
4253
+
4254
+ for (const itemVinculo of coletarVinculosIr(ir)) {
4255
+ const registro: RegistroVinculoDrift = {
4256
+ modulo: ir.nome,
4257
+ donoTipo: itemVinculo.donoTipo,
4258
+ dono: itemVinculo.dono,
4259
+ tipo: itemVinculo.vinculo.tipo,
4260
+ valor: itemVinculo.vinculo.valor,
4261
+ status: "nao_encontrado",
4262
+ confianca: "baixa",
4263
+ };
4264
+
4265
+ const arquivoDeclarado = itemVinculo.vinculo.arquivo ?? (itemVinculo.vinculo.tipo === "arquivo" ? itemVinculo.vinculo.valor : undefined);
4266
+ const simboloDeclarado = itemVinculo.vinculo.simbolo ?? (itemVinculo.vinculo.tipo === "simbolo" ? itemVinculo.vinculo.valor : undefined);
4267
+ const recursoDeclarado = itemVinculo.vinculo.recurso ?? (["recurso", "tabela", "fila", "cache", "storage"].includes(itemVinculo.vinculo.tipo) ? itemVinculo.vinculo.valor : undefined);
4268
+ const superficieDeclarada = itemVinculo.vinculo.superficie ?? (["superficie", "rota", "worker", "cron", "webhook", "evento", "policy", "fila", "cache", "storage"].includes(itemVinculo.vinculo.tipo) ? itemVinculo.vinculo.valor : undefined);
4269
+
4270
+ if (simboloDeclarado) {
4271
+ const resolucaoSimbolo = escolherSimboloPorVinculo(todosSimbolos, mapaImpl, simboloDeclarado);
4272
+ registro.status = resolucaoSimbolo.status;
4273
+ registro.confianca = resolucaoSimbolo.confianca;
4274
+ registro.arquivo = resolucaoSimbolo.simbolo?.arquivo;
4275
+ registro.simbolo = resolucaoSimbolo.simbolo?.simbolo;
4276
+ } else if (arquivoDeclarado) {
4277
+ const resolucaoArquivo = escolherArquivoPorVinculo(todosArquivosConhecidos, arquivoDeclarado);
4278
+ registro.status = resolucaoArquivo.status;
4279
+ registro.confianca = resolucaoArquivo.confianca;
4280
+ registro.arquivo = resolucaoArquivo.arquivo;
4281
+ } else if (recursoDeclarado) {
4282
+ const recurso = resolverRecursoEsperado(mapaRecursos, {
4283
+ categoria: "persistencia",
4284
+ alvo: recursoDeclarado,
4285
+ tiposAceitos: [],
4286
+ nomes: [recursoDeclarado],
4287
+ });
4288
+ if (recurso) {
4289
+ registro.status = "resolvido";
4290
+ registro.confianca = "alta";
4291
+ registro.arquivo = recurso.arquivo;
4292
+ }
4293
+ } else if (superficieDeclarada) {
4294
+ const rota = todasRotasIndexadas.find((rotaResolvida) =>
4295
+ normalizarCaminhoRota(rotaResolvida.caminho) === normalizarCaminhoRota(superficieDeclarada));
4296
+ if (rota) {
4297
+ registro.status = "resolvido";
4298
+ registro.confianca = "alta";
4299
+ registro.arquivo = rota.arquivo;
4300
+ registro.simbolo = rota.simbolo;
4301
+ } else {
4302
+ const resolucaoArquivo = escolherArquivoPorVinculo(todosArquivosConhecidos, superficieDeclarada);
4303
+ registro.status = resolucaoArquivo.status;
4304
+ registro.confianca = resolucaoArquivo.confianca;
4305
+ registro.arquivo = resolucaoArquivo.arquivo;
4306
+ }
4307
+ } else {
4308
+ const resolucaoArquivo = escolherArquivoPorVinculo(todosArquivosConhecidos, itemVinculo.vinculo.valor);
4309
+ registro.status = resolucaoArquivo.status;
4310
+ registro.confianca = resolucaoArquivo.confianca;
4311
+ registro.arquivo = resolucaoArquivo.arquivo;
4312
+ }
4313
+
4314
+ if (registro.status === "nao_encontrado" && itemVinculo.donoTipo === "superficie") {
4315
+ const superficie = superficiesPorChave.get(itemVinculo.dono);
4316
+ const ancora = superficie
4317
+ ? encontrarAncoraSuperficie(ir, superficie, todosSimbolos, mapaImpl, todosArquivosConhecidos)
4318
+ : undefined;
4319
+ if (ancora) {
4320
+ registro.status = "parcial";
4321
+ registro.confianca = ancora.confianca === "alta" ? "media" : ancora.confianca;
4322
+ registro.arquivo = registro.arquivo ?? ancora.arquivo;
4323
+ registro.simbolo = registro.simbolo ?? ancora.simbolo;
4324
+ }
4325
+ }
4326
+
4327
+ if (registro.status === "nao_encontrado") {
4328
+ vinculosQuebrados.push(registro);
4329
+ diagnosticos.push({
4330
+ tipo: "vinculo_quebrado",
4331
+ modulo: ir.nome,
4332
+ mensagem: `Vinculo ${registro.tipo}="${registro.valor}" de ${registro.donoTipo} "${registro.dono}" nao foi resolvido no codigo vivo.`,
4333
+ ...(itemVinculo.donoTipo === "task" ? { task: itemVinculo.dono } : itemVinculo.donoTipo === "route" ? { route: itemVinculo.dono } : {}),
4334
+ });
4335
+ } else {
4336
+ vinculosValidos.push(registro);
4337
+ if (itemVinculo.donoTipo === "modulo") {
4338
+ for (const task of ir.tasks) {
4339
+ registrarArquivoAncoraHerdado(task.nome, registro.arquivo);
4340
+ }
4341
+ } else if (itemVinculo.donoTipo === "flow") {
4342
+ const flow = flowsPorNome.get(itemVinculo.dono);
4343
+ for (const taskNome of flow?.tasksReferenciadas ?? []) {
4344
+ registrarArquivoAncoraHerdado(taskNome, registro.arquivo);
4345
+ }
4346
+ } else if (itemVinculo.donoTipo === "route") {
4347
+ const route = routesPorNome.get(itemVinculo.dono);
4348
+ if (route?.task) {
4349
+ registrarArquivoAncoraHerdado(route.task, registro.arquivo);
4350
+ }
4351
+ } else if (itemVinculo.donoTipo === "superficie") {
4352
+ const superficie = superficiesPorChave.get(itemVinculo.dono);
4353
+ if (superficie?.task) {
4354
+ registrarArquivoAncoraHerdado(superficie.task, registro.arquivo);
4355
+ }
4356
+ }
4357
+ }
4358
+
4359
+ if (itemVinculo.donoTipo === "task") {
4360
+ const chaveTask = `${ir.nome}:${itemVinculo.dono}`;
4361
+ const resumo = resumoVinculosPorTask.get(chaveTask) ?? {
4362
+ validos: 0,
4363
+ quebrados: 0,
4364
+ arquivos: new Set<string>(),
4365
+ };
4366
+ if (registro.status === "nao_encontrado") {
4367
+ resumo.quebrados += 1;
4368
+ } else {
4369
+ resumo.validos += 1;
4370
+ }
4371
+ if (registro.arquivo) {
4372
+ resumo.arquivos.add(registro.arquivo);
4373
+ }
4374
+ resumoVinculosPorTask.set(chaveTask, resumo);
4375
+ }
4376
+ }
4377
+ }
4378
+
4379
+ for (const resumo of tasksResumo) {
4380
+ const chaveTask = `${resumo.modulo}:${resumo.task}`;
4381
+ const task = taskPorChave.get(chaveTask);
4382
+ const guardrails = guardrailsPorTask.get(chaveTask) ?? {
4383
+ publica: false,
4384
+ sensivel: false,
4385
+ auth: false,
4386
+ authz: false,
4387
+ dados: false,
4388
+ audit: false,
4389
+ segredos: false,
4390
+ forbidden: false,
4391
+ dadosSensiveis: false,
4392
+ efeitoPrivilegiado: false,
4393
+ exigeSegredos: false,
4394
+ };
4395
+ const resumoVinculos = resumoVinculosPorTask.get(chaveTask) ?? {
4396
+ validos: 0,
4397
+ quebrados: 0,
4398
+ arquivos: new Set<string>(),
4399
+ };
4400
+ const arquivosAncoraHerdados = [...(arquivosAncoraHerdadosPorTask.get(chaveTask) ?? new Set<string>())]
4401
+ .filter((arquivo) => !resumoVinculos.arquivos.has(arquivo))
4402
+ .sort((a, b) => a.localeCompare(b, "pt-BR"));
4403
+ if (!task) {
4404
+ continue;
4405
+ }
4406
+
4407
+ resumo.confiancaVinculo = calcularConfiancaTask(task, resumo.implsValidos, resumo.implsQuebrados, resumoVinculos.validos, resumoVinculos.quebrados);
4408
+ resumo.riscoOperacional = calcularRiscoOperacional(task);
4409
+ resumo.lacunas = resumirLacunasTask(task, resumo.semImplementacao, resumo.implsQuebrados, resumoVinculos.quebrados, guardrails);
4410
+ resumo.scoreSemantico = calcularScoreTask(task, resumo.implsValidos, resumo.implsQuebrados, resumoVinculos.validos, resumoVinculos.quebrados, resumo.semImplementacao);
4411
+ resumo.ancoragemVinculo = task.vinculos.length > 0
4412
+ ? "propria"
4413
+ : arquivosAncoraHerdados.length > 0
4414
+ ? "herdada_modulo"
4415
+ : "ausente";
4416
+ resumo.arquivosAncoraHerdados = arquivosAncoraHerdados;
4417
+ resumo.arquivosProvaveisEditar = [...new Set([
4418
+ ...resumo.arquivosReferenciados,
4419
+ ...arquivosAncoraHerdados,
4420
+ ...resumo.candidatosImpl.map((candidato) => candidato.arquivo),
4421
+ ...resumoVinculos.arquivos,
4422
+ ])].sort((a, b) => a.localeCompare(b, "pt-BR"));
4423
+ resumo.checksSugeridos = [...new Set([
4424
+ ...task.resumoAgente.checks,
4425
+ resumo.riscoOperacional !== "baixo" ? "revisar efeitos operacionais" : "",
4426
+ resumo.lacunas.includes("vinculo_quebrado") ? "corrigir vinculos rastreaveis" : "",
4427
+ resumo.lacunas.some((lacuna) => ["superficie_publica_sem_execucao", "execucao_critica_sem_bloco", "rastreabilidade_fraca"].includes(lacuna))
4428
+ ? "endurecer execucao e rastreabilidade para producao"
4429
+ : "",
4430
+ resumo.lacunas.some((lacuna) => ["auth_ausente", "authz_frouxa", "dados_nao_classificados", "audit_ausente", "segredo_sem_governanca", "proibicoes_ausentes"].includes(lacuna))
4431
+ ? "explicitar contratos de seguranca semantica"
4432
+ : "",
4433
+ ].filter(Boolean))];
4434
+
4435
+ if (resumo.lacunas.includes("superficie_publica_sem_execucao")) {
4436
+ diagnosticos.push({
4437
+ tipo: "seguranca_frouxa",
4438
+ modulo: resumo.modulo,
4439
+ task: resumo.task,
4440
+ mensagem: `Task "${resumo.task}" alimenta superficie publica, mas ainda depende de execucao implicita.`,
4441
+ });
4442
+ }
4443
+ if (resumo.lacunas.includes("execucao_critica_sem_bloco")) {
4444
+ diagnosticos.push({
4445
+ tipo: "seguranca_frouxa",
4446
+ modulo: resumo.modulo,
4447
+ task: resumo.task,
4448
+ mensagem: `Task "${resumo.task}" opera com risco alto, mas ainda nao declarou execucao explicita.`,
4449
+ });
4450
+ }
4451
+ if (resumo.lacunas.includes("rastreabilidade_fraca")) {
4452
+ diagnosticos.push({
4453
+ tipo: "seguranca_frouxa",
4454
+ modulo: resumo.modulo,
4455
+ task: resumo.task,
4456
+ mensagem: `Task "${resumo.task}" exige producao mais rastreavel, mas ainda nao declara impl nem vinculos.`,
4457
+ });
4458
+ }
4459
+ if (resumo.lacunas.includes("auth_ausente")) {
4460
+ diagnosticos.push({
4461
+ tipo: "seguranca_frouxa",
4462
+ modulo: resumo.modulo,
4463
+ task: resumo.task,
4464
+ mensagem: `Task "${resumo.task}" chega em superficie publica sem auth explicita em task, route ou superficie associada.`,
4465
+ });
4466
+ }
4467
+ if (resumo.lacunas.includes("authz_frouxa")) {
4468
+ diagnosticos.push({
4469
+ tipo: "seguranca_frouxa",
4470
+ modulo: resumo.modulo,
4471
+ task: resumo.task,
4472
+ mensagem: `Task "${resumo.task}" opera com risco ou exposicao, mas ainda nao explicita authz suficiente.`,
4473
+ });
4474
+ }
4475
+ if (resumo.lacunas.includes("dados_nao_classificados")) {
4476
+ diagnosticos.push({
4477
+ tipo: "seguranca_frouxa",
4478
+ modulo: resumo.modulo,
4479
+ task: resumo.task,
4480
+ mensagem: `Task "${resumo.task}" ainda nao classifica dados de entrada/saida de forma semantica.`,
4481
+ });
4482
+ }
4483
+ if (resumo.lacunas.includes("audit_ausente")) {
4484
+ diagnosticos.push({
4485
+ tipo: "seguranca_frouxa",
4486
+ modulo: resumo.modulo,
4487
+ task: resumo.task,
4488
+ mensagem: `Task "${resumo.task}" ainda nao declara audit explicita para operacao sensivel ou publica.`,
4489
+ });
4490
+ }
4491
+ if (resumo.lacunas.includes("segredo_sem_governanca")) {
4492
+ diagnosticos.push({
4493
+ tipo: "seguranca_frouxa",
4494
+ modulo: resumo.modulo,
4495
+ task: resumo.task,
4496
+ mensagem: `Task "${resumo.task}" toca segredo ou credencial sem bloco segredos governando origem, escopo e rotacao.`,
4497
+ });
4498
+ }
4499
+ if (resumo.lacunas.includes("proibicoes_ausentes")) {
4500
+ diagnosticos.push({
4501
+ tipo: "seguranca_frouxa",
4502
+ modulo: resumo.modulo,
4503
+ task: resumo.task,
4504
+ mensagem: `Task "${resumo.task}" opera com efeito privilegiado ou dado sensivel sem forbidden explicito para conter abuso e vazamento.`,
4505
+ });
4506
+ }
4507
+ }
4508
+
4509
+ const relevanciaConsumer = construirContextoRelevanciaConsumer(contexto, tasksResumo, vinculosValidos);
4510
+ const consumersFiltrados = filtrarConsumerSurfacesPorEscopo(
4511
+ [...indexTs.consumerSurfaces, ...indexDart.consumerSurfaces].sort((a, b) =>
4512
+ a.rota.localeCompare(b.rota, "pt-BR")
4513
+ || a.tipoArquivo.localeCompare(b.tipoArquivo, "pt-BR")
4514
+ || a.arquivo.localeCompare(b.arquivo, "pt-BR")),
4515
+ [...new Map(
4516
+ [...indexTs.simbolos, ...indexDart.simbolos]
4517
+ .filter((simbolo) => simboloEhBridgeConsumer(simbolo.caminho, simbolo.arquivo))
4518
+ .map((simbolo) => [
4519
+ `${simbolo.caminho}:${simbolo.arquivo}:${simbolo.simbolo}`,
4520
+ {
4521
+ caminho: simbolo.caminho,
4522
+ arquivo: simbolo.arquivo,
4523
+ simbolo: simbolo.simbolo,
4524
+ },
4525
+ ] as const),
4526
+ ).values()].sort((a, b) =>
4527
+ a.caminho.localeCompare(b.caminho, "pt-BR")
4528
+ || a.arquivo.localeCompare(b.arquivo, "pt-BR")),
4529
+ contexto,
4530
+ configuracaoEscopo,
4531
+ relevanciaConsumer,
4532
+ );
4533
+ const consumerSurfaces = consumersFiltrados.consumerSurfaces;
4534
+ const consumerBridges = consumersFiltrados.consumerBridges;
4535
+ const appRoutes = [...new Set(consumerSurfaces.map((surface) => surface.rota))]
4536
+ .sort((a, b) => a.localeCompare(b, "pt-BR"));
4537
+ const consumerFramework = inferirConsumerFrameworkPrincipal(contexto.fontesLegado, consumerSurfaces, consumerBridges);
4538
+ const persistenciaReal = await analisarPersistenciaReal(contexto, mapaRecursos, detalhesPersistencia, opcoesResolvidas, mapaImpl);
4539
+ for (const item of persistenciaReal) {
4540
+ if (item.status === "divergente") {
4541
+ diagnosticos.push({
4542
+ tipo: "recurso_divergente",
4543
+ modulo: item.modulo,
4544
+ task: item.task,
4545
+ mensagem: `Persistencia real para "${item.alvo}" ainda nao foi materializada no codigo vivo.`,
4546
+ });
4547
+ } else if (item.compatibilidade === "invalido") {
4548
+ diagnosticos.push({
4549
+ tipo: "recurso_divergente",
4550
+ modulo: item.modulo,
4551
+ task: item.task,
4552
+ mensagem: `Persistencia real para "${item.alvo}" conflita com a compatibilidade declarada do engine ${item.engine}.`,
4553
+ });
4554
+ }
4555
+ }
4556
+
4557
+ const payloadBase: ResultadoDrift = {
4558
+ comando: "drift",
4559
+ sucesso: implsQuebrados.length === 0
4560
+ && rotasDivergentes.length === 0
4561
+ && recursosDivergentes.length === 0
4562
+ && vinculosQuebrados.length === 0
4563
+ && persistenciaReal.every((item) => item.status !== "divergente" && item.compatibilidade !== "invalido"),
4564
+ escopo_aplicado: configuracaoEscopo,
4565
+ consumerFramework,
4566
+ appRoutes,
4567
+ consumerSurfaces,
4568
+ consumerBridges,
4569
+ modulos: contexto.modulosSelecionados.map((item) => ({
4570
+ caminho: item.caminho,
4571
+ modulo: item.resultado.ir?.nome ?? item.resultado.modulo?.nome ?? null,
4572
+ tasks: item.resultado.ir?.tasks.length ?? 0,
4573
+ routes: item.resultado.ir?.routes.length ?? 0,
4574
+ })),
4575
+ tasks: tasksResumo,
4576
+ impls_validos: implsValidos,
4577
+ impls_quebrados: implsQuebrados,
4578
+ vinculos_validos: vinculosValidos,
4579
+ vinculos_quebrados: vinculosQuebrados,
4580
+ rotas_divergentes: rotasDivergentes,
4581
+ recursos_validos: recursosValidos,
4582
+ recursos_divergentes: recursosDivergentes,
4583
+ persistencia_real: persistenciaReal,
4584
+ diagnosticos,
4585
+ resumo_operacional: {
4586
+ scoreMedio: 0,
4587
+ confiancaGeral: "baixa" as const,
4588
+ riscosPrincipais: [],
4589
+ oQueTocar: [],
4590
+ oQueValidar: [],
4591
+ oQueEstaFrouxo: [],
4592
+ oQueFoiInferido: [],
4593
+ },
4594
+ };
4595
+ const resumoOperacional = resumirOperacional(payloadBase);
4596
+
4597
+ return {
4598
+ ...payloadBase,
4599
+ resumo_operacional: resumoOperacional,
4600
+ };
4601
+ } finally {
4602
+ diretoriosIgnoradosAtivos = diretoriosIgnoradosAnteriores;
4603
+ }
4604
+ }
4605
+
4606
+ const EXTENSOES_BUSCA_IMPACTO = [
4607
+ ".sema",
4608
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
4609
+ ".py", ".dart", ".lua", ".php", ".cs", ".java", ".go", ".rs", ".cpp", ".cc", ".cxx", ".hpp", ".h",
4610
+ ".sql", ".psql", ".ddl", ".prisma", ".json",
4611
+ ];
4612
+
4613
+ function construirVariantesSemanticas(valor: string): string[] {
4614
+ const bruto = valor.trim();
4615
+ const partes = paraIdentificadorModulo(valor).split("_").filter(Boolean);
4616
+ if (!bruto && partes.length === 0) {
4617
+ return [];
4618
+ }
4619
+ const camel = partes.length > 0
4620
+ ? `${partes[0]}${partes.slice(1).map((item) => item[0]?.toUpperCase() + item.slice(1)).join("")}`
4621
+ : bruto;
4622
+ const pascal = partes.length > 0
4623
+ ? partes.map((item) => item[0]?.toUpperCase() + item.slice(1)).join("")
4624
+ : bruto;
4625
+ return [...new Set([
4626
+ bruto,
4627
+ partes.join("_"),
4628
+ partes.join("-"),
4629
+ partes.join("."),
4630
+ camel,
4631
+ pascal,
4632
+ ].filter(Boolean))];
4633
+ }
4634
+
4635
+ function classificarArquivoImpacto(arquivo: string): RegistroImpactoSemanticoArquivo["tipo"] {
4636
+ const normalizado = normalizarFragmentoArquivo(arquivo);
4637
+ if (normalizado.endsWith(".sema")) {
4638
+ return "contrato";
4639
+ }
4640
+ if (/\.(sql|psql|ddl|prisma)$/i.test(normalizado) || /(?:^|\/)(?:db|database|migrations?|schemas?)\//i.test(normalizado)) {
4641
+ return "persistencia";
4642
+ }
4643
+ if (/(?:^|\/)(?:repositorio|repositorios|repository|repositories|repo|dao|store)\//i.test(normalizado) || /(repository|repositorio|dao|store)/i.test(path.basename(normalizado))) {
4644
+ return "repositorio";
4645
+ }
4646
+ if (/(?:^|\/)(?:routes?|controllers?|api)\//i.test(normalizado) || /(controller|route)/i.test(path.basename(normalizado))) {
4647
+ return "rota";
4648
+ }
4649
+ if (/(?:^|\/)(?:workers?|jobs?|queues?|cron)\//i.test(normalizado) || /(worker|job|queue|cron)/i.test(path.basename(normalizado))) {
4650
+ return "worker";
4651
+ }
4652
+ if (/(?:^|\/)(?:pages|screens|components|views|app)\//i.test(normalizado)) {
4653
+ return "ui";
4654
+ }
4655
+ if (/(?:^|\/)(?:tests?|specs?|__tests__)\//i.test(normalizado) || /\.(spec|test)\./i.test(normalizado)) {
4656
+ return "teste";
4657
+ }
4658
+ return "codigo";
4659
+ }
4660
+
4661
+ function prioridadeArquivoImpacto(tipo: RegistroImpactoSemanticoArquivo["tipo"]): RegistroImpactoSemanticoArquivo["prioridade"] {
4662
+ switch (tipo) {
4663
+ case "contrato":
4664
+ case "persistencia":
4665
+ case "repositorio":
4666
+ case "rota":
4667
+ return "alta";
4668
+ case "worker":
4669
+ case "codigo":
4670
+ return "media";
4671
+ default:
4672
+ return "baixa";
4673
+ }
4674
+ }
4675
+
4676
+ function textoIrCombinaTermos(texto: string, termos: string[]): boolean {
4677
+ return textoCombinaEscopo(texto, termos);
4678
+ }
4679
+
4680
+ function registrarArquivoImpactado(
4681
+ mapa: Map<string, RegistroImpactoSemanticoArquivo>,
4682
+ arquivo: string,
4683
+ linhas: number[],
4684
+ motivos: string[],
4685
+ ): void {
4686
+ const tipo = classificarArquivoImpacto(arquivo);
4687
+ const atual = mapa.get(arquivo);
4688
+ if (atual) {
4689
+ atual.linhas = [...new Set([...atual.linhas, ...linhas])].sort((a, b) => a - b);
4690
+ atual.motivos = [...new Set([...atual.motivos, ...motivos])];
4691
+ if (prioridadeArquivoImpacto(tipo) === "alta") {
4692
+ atual.prioridade = "alta";
4693
+ } else if (prioridadeArquivoImpacto(tipo) === "media" && atual.prioridade === "baixa") {
4694
+ atual.prioridade = "media";
4695
+ }
4696
+ return;
4697
+ }
4698
+
4699
+ mapa.set(arquivo, {
4700
+ arquivo,
4701
+ tipo,
4702
+ prioridade: prioridadeArquivoImpacto(tipo),
4703
+ linhas: [...new Set(linhas)].sort((a, b) => a - b),
4704
+ motivos: [...new Set(motivos)],
4705
+ });
4706
+ }
4707
+
4708
+ async function listarArquivosImpacto(
4709
+ contexto: ContextoProjetoCarregado,
4710
+ opcoes?: OpcoesDriftLegado,
4711
+ ): Promise<string[]> {
4712
+ const opcoesResolvidas = resolverOpcoesDrift(opcoes);
4713
+ const arquivos = new Set<string>(filtrarCaminhosEscopoReal(contexto.arquivosProjeto, contexto, opcoesResolvidas));
4714
+ for (const diretorio of resolverDiretoriosCodigoEscopoReal(contexto, opcoesResolvidas)) {
4715
+ for (const arquivo of await listarArquivosRecursivos(diretorio, EXTENSOES_BUSCA_IMPACTO)) {
4716
+ arquivos.add(arquivo);
4717
+ }
4718
+ }
4719
+ return [...arquivos].sort((a, b) => a.localeCompare(b, "pt-BR"));
4720
+ }
4721
+
4722
+ function extrairLinhasComVariantes(codigo: string, variantes: string[]): number[] {
4723
+ const linhas: number[] = [];
4724
+ const texto = codigo.split(/\r?\n/);
4725
+ for (let indice = 0; indice < texto.length; indice += 1) {
4726
+ if (variantes.some((variante) => variante && texto[indice]!.includes(variante))) {
4727
+ linhas.push(indice + 1);
4728
+ }
4729
+ }
4730
+ return linhas;
4731
+ }
4732
+
4733
+ function serializarTaskParaImpacto(task: IrTask): string {
4734
+ return JSON.stringify({
4735
+ nome: task.nome,
4736
+ input: task.input.map((campo) => campo.nome),
4737
+ output: task.output.map((campo) => campo.nome),
4738
+ effects: task.effects,
4739
+ guarantees: task.guarantees,
4740
+ errors: task.errors,
4741
+ resumo: task.resumoAgente,
4742
+ });
4743
+ }
4744
+
4745
+ function serializarRouteParaImpacto(route: IrRoute): string {
4746
+ return JSON.stringify({
4747
+ nome: route.nome,
4748
+ caminho: route.caminho,
4749
+ metodo: route.metodo,
4750
+ task: route.task,
4751
+ input: route.inputPublico.map((campo) => campo.nome),
4752
+ output: route.outputPublico.map((campo) => campo.nome),
4753
+ });
4754
+ }
4755
+
4756
+ function serializarSuperficieParaImpacto(superficie: IrSuperficie): string {
4757
+ return JSON.stringify({
4758
+ tipo: superficie.tipo,
4759
+ nome: superficie.nome,
4760
+ task: superficie.task,
4761
+ input: superficie.input.map((campo) => campo.nome),
4762
+ output: superficie.output.map((campo) => campo.nome),
4763
+ });
4764
+ }
4765
+
4766
+ function ordenarArquivosImpacto(arquivos: RegistroImpactoSemanticoArquivo[]): RegistroImpactoSemanticoArquivo[] {
4767
+ const ordemPrioridade = { alta: 0, media: 1, baixa: 2 } as const;
4768
+ return [...arquivos].sort((a, b) =>
4769
+ ordemPrioridade[a.prioridade] - ordemPrioridade[b.prioridade]
4770
+ || a.tipo.localeCompare(b.tipo, "pt-BR")
4771
+ || a.arquivo.localeCompare(b.arquivo, "pt-BR"));
4772
+ }
4773
+
4774
+ export async function gerarMapaImpactoSemantico(
4775
+ contexto: ContextoProjetoCarregado,
4776
+ alvoSemantico: string,
4777
+ mudancaProposta: string,
4778
+ opcoes?: OpcoesDriftLegado,
4779
+ ): Promise<ResultadoImpactoSemantico> {
4780
+ const opcoesResolvidas = resolverOpcoesDrift(opcoes);
4781
+ const diretoriosIgnoradosAnteriores = diretoriosIgnoradosAtivos;
4782
+ diretoriosIgnoradosAtivos = resolverDiretoriosIgnoradosAtivos(opcoesResolvidas);
4783
+
4784
+ try {
4785
+ const drift = await analisarDriftLegado(contexto, opcoesResolvidas);
4786
+ const variantes = construirVariantesSemanticas(alvoSemantico);
4787
+ const termos = [...new Set([...quebrarTermosEscopo(alvoSemantico), ...drift.escopo_aplicado.termosEscopo])];
4788
+ const arquivosImpactados = new Map<string, RegistroImpactoSemanticoArquivo>();
4789
+ const arquivosBusca = await listarArquivosImpacto(contexto, opcoesResolvidas);
4790
+
4791
+ for (const arquivo of arquivosBusca) {
4792
+ const codigo = await readFile(arquivo, "utf8");
4793
+ const linhas = extrairLinhasComVariantes(codigo, variantes);
4794
+ if (linhas.length > 0) {
4795
+ registrarArquivoImpactado(arquivosImpactados, arquivo, linhas, ["token_semantico_encontrado"]);
4796
+ }
4797
+ }
4798
+
4799
+ const tasksAfetadas = new Set<string>();
4800
+ const routesAfetadas = new Set<string>();
4801
+ const superficiesAfetadas = new Set<string>();
4802
+ const persistenciaAfetada = new Set<string>();
4803
+
4804
+ for (const item of contexto.modulosSelecionados) {
4805
+ const ir = item.resultado.ir;
4806
+ if (!ir) {
4807
+ continue;
4808
+ }
4809
+ for (const task of ir.tasks) {
4810
+ if (textoIrCombinaTermos(serializarTaskParaImpacto(task), termos)) {
4811
+ tasksAfetadas.add(`${ir.nome}.${task.nome}`);
4812
+ }
4813
+ }
4814
+ for (const route of ir.routes) {
4815
+ if (textoIrCombinaTermos(serializarRouteParaImpacto(route), termos)) {
4816
+ routesAfetadas.add(`${ir.nome}.${route.nome}`);
4817
+ }
4818
+ }
4819
+ for (const superficie of ir.superficies) {
4820
+ if (textoIrCombinaTermos(serializarSuperficieParaImpacto(superficie), termos)) {
4821
+ superficiesAfetadas.add(`${ir.nome}.${superficie.tipo}.${superficie.nome}`);
4822
+ }
4823
+ }
4824
+ }
4825
+
4826
+ for (const task of drift.tasks.filter((item) => tasksAfetadas.has(`${item.modulo}.${item.task}`))) {
4827
+ for (const arquivo of task.arquivosProvaveisEditar) {
4828
+ registrarArquivoImpactado(arquivosImpactados, arquivo, [], ["arquivo_relacionado_por_drift"]);
4829
+ }
4830
+ }
4831
+
4832
+ for (const item of drift.persistencia_real) {
4833
+ if (textoIrCombinaTermos(`${item.alvo} ${item.task} ${item.colunas.join(" ")}`, termos)) {
4834
+ persistenciaAfetada.add(`${item.task}:${item.alvo}`);
4835
+ for (const arquivo of [...item.arquivos, ...item.repositorios]) {
4836
+ registrarArquivoImpactado(arquivosImpactados, arquivo, [], ["persistencia_relacionada"]);
4837
+ }
4838
+ }
4839
+ }
4840
+
4841
+ const contratosAfetados = ordenarArquivosImpacto(
4842
+ [...arquivosImpactados.values()].filter((arquivo) => arquivo.tipo === "contrato"),
4843
+ ).map((arquivo) => arquivo.arquivo);
4844
+
4845
+ return {
4846
+ comando: "impacto",
4847
+ sucesso: arquivosImpactados.size > 0 || tasksAfetadas.size > 0 || persistenciaAfetada.size > 0,
4848
+ escopo: drift.escopo_aplicado.escopo,
4849
+ alvoSemantico,
4850
+ mudancaProposta,
4851
+ contratosAfetados,
4852
+ tasksAfetadas: [...tasksAfetadas].sort((a, b) => a.localeCompare(b, "pt-BR")),
4853
+ routesAfetadas: [...routesAfetadas].sort((a, b) => a.localeCompare(b, "pt-BR")),
4854
+ superficiesAfetadas: [...superficiesAfetadas].sort((a, b) => a.localeCompare(b, "pt-BR")),
4855
+ persistenciaAfetada: [...persistenciaAfetada].sort((a, b) => a.localeCompare(b, "pt-BR")),
4856
+ arquivos: ordenarArquivosImpacto([...arquivosImpactados.values()]),
4857
+ ordemOperacional: [
4858
+ "Atualizar contrato .sema e revisar garantias publicas primeiro.",
4859
+ "Ajustar persistencia e repositorios concretos antes de materializacao externa.",
4860
+ "Revisar rotas, workers e bridges depois que o contrato e o storage estiverem coerentes.",
4861
+ "Fechar com UI/consumidores e testes alinhados ao payload final.",
4862
+ ],
4863
+ validacoes: [
4864
+ "Rodar sema validar no contrato alterado.",
4865
+ "Rodar sema drift com o mesmo escopo apos a mudanca.",
4866
+ "Revalidar testes de payload, persistencia e superficies publicas.",
4867
+ ],
4868
+ };
4869
+ } finally {
4870
+ diretoriosIgnoradosAtivos = diretoriosIgnoradosAnteriores;
4871
+ }
4872
+ }
4873
+
4874
+ export async function assistirRenomeacaoSemantica(
4875
+ contexto: ContextoProjetoCarregado,
4876
+ nomeAtual: string,
4877
+ nomeNovo: string,
4878
+ opcoes?: OpcoesDriftLegado,
4879
+ ): Promise<ResultadoRenomeacaoSemantica> {
4880
+ const impacto = await gerarMapaImpactoSemantico(
4881
+ contexto,
4882
+ nomeAtual,
4883
+ `renomear ${nomeAtual} para ${nomeNovo}`,
4884
+ opcoes,
4885
+ );
4886
+ const variantesAntigas = construirVariantesSemanticas(nomeAtual);
4887
+ const variantesNovas = construirVariantesSemanticas(nomeNovo);
4888
+ const mapaSubstituicao = new Map<string, string>();
4889
+ variantesAntigas.forEach((antiga, indice) => {
4890
+ mapaSubstituicao.set(antiga, variantesNovas[indice] ?? nomeNovo);
4891
+ });
4892
+
4893
+ const sugestoes: SugestaoRenomeacaoSemantica[] = [];
4894
+ for (const arquivo of impacto.arquivos) {
4895
+ const codigo = await readFile(arquivo.arquivo, "utf8");
4896
+ const linhas = codigo.split(/\r?\n/);
4897
+ for (let indice = 0; indice < linhas.length; indice += 1) {
4898
+ const linha = linhas[indice]!;
4899
+ for (const antiga of variantesAntigas) {
4900
+ if (!antiga || !linha.includes(antiga)) {
4901
+ continue;
4902
+ }
4903
+ sugestoes.push({
4904
+ arquivo: arquivo.arquivo,
4905
+ linha: indice + 1,
4906
+ atual: antiga,
4907
+ sugerido: mapaSubstituicao.get(antiga) ?? nomeNovo,
4908
+ contexto: linha.trim().slice(0, 180),
4909
+ });
4910
+ }
4911
+ }
4912
+ }
4913
+
4914
+ return {
4915
+ comando: "renomear-semantico",
4916
+ sucesso: sugestoes.length > 0,
4917
+ escopo: impacto.escopo,
4918
+ de: nomeAtual,
4919
+ para: nomeNovo,
4920
+ arquivos: impacto.arquivos,
4921
+ sugestoes,
4922
+ ordemOperacional: [
4923
+ "Renomear primeiro no contrato .sema e nos campos publicos derivados.",
4924
+ "Ajustar repositorios, payloads e bridges que materializam o nome antigo.",
4925
+ "Rodar sema drift e revisar sugestoes restantes antes de fechar a troca.",
4926
+ ],
4927
+ validacoes: [
4928
+ "Rodar sema validar no contrato renomeado.",
4929
+ "Rodar sema drift para confirmar que payload e superficie nao ficaram misturados.",
4930
+ "Reexecutar testes e checar snapshots ou fixtures afetados.",
4931
+ ],
4932
+ };
4933
+ }