@semacode/cli 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,7 +58,7 @@ sema drift ./contratos-importados --json
58
58
 
59
59
  ## Persistencia vendor-first
60
60
 
61
- A CLI 1.5.0 entende blocos `database` e recursos de persistencia no IR, no formatador, no semantico, na importacao, no drift, no impact map e na renomeacao semantica assistida. O objetivo nao e esconder diferencas entre bancos, e sim capturar essas diferencas no contrato.
61
+ A CLI 1.5.1 entende blocos `database` e recursos de persistencia no IR, no formatador, no semantico, na importacao, no drift, no impact map e na renomeacao semantica assistida. O objetivo nao e esconder diferencas entre bancos, e sim capturar essas diferencas no contrato.
62
62
 
63
63
  Cobertura publica:
64
64
 
package/dist/drift.d.ts CHANGED
@@ -42,8 +42,9 @@ interface RegistroRotaDivergente {
42
42
  caminho?: string;
43
43
  motivo: string;
44
44
  }
45
- type OrigemRecursoDrift = "firebase" | EngineBanco;
46
- type TipoRecursoDrift = "colecao" | TipoRecursoPersistencia;
45
+ type OrigemRecursoDrift = "firebase" | EngineBanco | "arquivo";
46
+ type TipoRecursoDrift = "colecao" | TipoRecursoPersistencia | "arquivo_local";
47
+ type CategoriaPersistenciaDrift = "relacional" | "documental" | "chave_valor" | "local_arquivo" | "desconhecida";
47
48
  interface RecursoResolvido {
48
49
  origem: OrigemRecursoDrift;
49
50
  nome: string;
@@ -62,13 +63,15 @@ interface RegistroRecursoDrift {
62
63
  status: "resolvido" | "divergente";
63
64
  }
64
65
  interface RegistroColunaPersistenciaDrift {
65
- engine: EngineBanco;
66
+ origem: OrigemRecursoDrift;
67
+ categoriaPersistencia: CategoriaPersistenciaDrift;
66
68
  recurso: string;
67
69
  coluna: string;
68
70
  arquivo: string;
69
71
  }
70
72
  interface RegistroRepositorioPersistenciaDrift {
71
- engine: EngineBanco;
73
+ origem: OrigemRecursoDrift;
74
+ categoriaPersistencia: CategoriaPersistenciaDrift;
72
75
  recurso: string;
73
76
  arquivo: string;
74
77
  }
@@ -113,6 +116,7 @@ export interface RegistroPersistenciaRealDrift {
113
116
  task: string;
114
117
  alvo: string;
115
118
  engine: OrigemRecursoDrift | "desconhecido";
119
+ categoriaPersistencia: CategoriaPersistenciaDrift;
116
120
  tipo: TipoRecursoDrift;
117
121
  status: "materializado" | "parcial" | "divergente";
118
122
  arquivos: string[];
package/dist/drift.js CHANGED
@@ -56,6 +56,7 @@ const TERMOS_ESCopo_IGNORADOS = new Set([
56
56
  "data",
57
57
  "drift",
58
58
  "flow",
59
+ "int",
59
60
  "module",
60
61
  "modulo",
61
62
  "publico",
@@ -110,18 +111,29 @@ function quebrarTermosEscopo(valor) {
110
111
  .map((item) => item.trim())
111
112
  .filter((item) => item.length >= 3 && !TERMOS_ESCopo_IGNORADOS.has(item));
112
113
  }
114
+ function quebrarTermosModuloEscopo(nomeModulo) {
115
+ const segmentos = nomeModulo
116
+ .split(".")
117
+ .map((item) => item.trim())
118
+ .filter(Boolean);
119
+ const relevantes = segmentos.length > 1 ? segmentos.slice(1) : segmentos;
120
+ return relevantes.flatMap((segmento) => quebrarTermosEscopo(segmento));
121
+ }
113
122
  function extrairTermosEscopoDrift(contexto, escopo) {
114
123
  if (escopo === "projeto") {
115
124
  return [];
116
125
  }
117
126
  const termos = new Set();
118
- termos.add(paraIdentificadorModulo(path.basename(contexto.entradaResolvida, path.extname(contexto.entradaResolvida))));
127
+ const termosRaizProjeto = new Set(quebrarTermosEscopo(path.basename(contexto.baseProjeto)));
128
+ if (escopo === "arquivo" || path.extname(contexto.entradaResolvida)) {
129
+ termos.add(paraIdentificadorModulo(path.basename(contexto.entradaResolvida, path.extname(contexto.entradaResolvida))));
130
+ }
119
131
  for (const modulo of contexto.modulosSelecionados) {
120
132
  const ir = modulo.resultado.ir;
121
133
  if (!ir) {
122
134
  continue;
123
135
  }
124
- for (const termo of quebrarTermosEscopo(ir.nome)) {
136
+ for (const termo of quebrarTermosModuloEscopo(ir.nome)) {
125
137
  termos.add(termo);
126
138
  }
127
139
  for (const task of ir.tasks) {
@@ -140,7 +152,26 @@ function extrairTermosEscopoDrift(contexto, escopo) {
140
152
  }
141
153
  }
142
154
  }
143
- return [...termos].filter(Boolean).sort((a, b) => a.localeCompare(b, "pt-BR"));
155
+ return [...termos]
156
+ .filter((termo) => Boolean(termo) && !termosRaizProjeto.has(termo))
157
+ .sort((a, b) => a.localeCompare(b, "pt-BR"));
158
+ }
159
+ function categorizarPersistenciaPorOrigem(origem) {
160
+ switch (origem) {
161
+ case "postgres":
162
+ case "mysql":
163
+ case "sqlite":
164
+ return "relacional";
165
+ case "mongodb":
166
+ case "firebase":
167
+ return "documental";
168
+ case "redis":
169
+ return "chave_valor";
170
+ case "arquivo":
171
+ return "local_arquivo";
172
+ default:
173
+ return "desconhecida";
174
+ }
144
175
  }
145
176
  function caminhoTemSegmentoIgnorado(arquivo, segmentos) {
146
177
  const partes = normalizarFragmentoArquivo(arquivo).split("/").filter(Boolean);
@@ -210,9 +241,104 @@ function textoCombinaEscopo(texto, termos) {
210
241
  const normalizado = paraIdentificadorModulo(texto);
211
242
  return termos.some((termo) => normalizado.includes(termo));
212
243
  }
213
- function filtrarConsumerSurfacesPorEscopo(consumerSurfaces, consumerBridges, contexto, configuracao) {
244
+ function construirContextoRelevanciaConsumer(contexto, tasksResumo, vinculosValidos) {
245
+ const arquivosAncora = new Set();
246
+ const rotasAncora = new Set();
247
+ for (const modulo of contexto.modulosSelecionados) {
248
+ const ir = modulo.resultado.ir;
249
+ if (!ir) {
250
+ continue;
251
+ }
252
+ for (const route of ir.routes) {
253
+ if (route.caminho) {
254
+ rotasAncora.add(normalizarCaminhoRota(route.caminho));
255
+ }
256
+ }
257
+ }
258
+ for (const task of tasksResumo) {
259
+ for (const arquivo of [...task.arquivosReferenciados, ...task.arquivosProvaveisEditar]) {
260
+ arquivosAncora.add(arquivo);
261
+ }
262
+ }
263
+ for (const vinculo of vinculosValidos) {
264
+ if (vinculo.arquivo) {
265
+ arquivosAncora.add(vinculo.arquivo);
266
+ }
267
+ if (vinculo.tipo === "superficie" || vinculo.tipo === "rota") {
268
+ rotasAncora.add(normalizarCaminhoRota(vinculo.valor));
269
+ }
270
+ }
271
+ return {
272
+ arquivosAncora: [...arquivosAncora].sort((a, b) => a.localeCompare(b, "pt-BR")),
273
+ rotasAncora: [...rotasAncora].sort((a, b) => a.localeCompare(b, "pt-BR")),
274
+ };
275
+ }
276
+ function pontuarTextoEscopo(texto, termos) {
277
+ if (termos.length === 0) {
278
+ return 0;
279
+ }
280
+ const normalizado = paraIdentificadorModulo(texto);
281
+ const segmentos = new Set(normalizado.split("_").filter(Boolean));
282
+ let score = 0;
283
+ for (const termo of termos) {
284
+ if (segmentos.has(termo)) {
285
+ score += 4;
286
+ continue;
287
+ }
288
+ if (normalizado.includes(termo)) {
289
+ score += 2;
290
+ }
291
+ }
292
+ return Math.min(score, 8);
293
+ }
294
+ function pontuarProximidadeArquivoConsumer(arquivo, arquivosAncora) {
295
+ if (arquivosAncora.length === 0) {
296
+ return 0;
297
+ }
298
+ const alvo = normalizarFragmentoArquivo(arquivo);
299
+ const diretorioAlvo = path.posix.dirname(alvo);
300
+ let scoreMaximo = 0;
301
+ for (const ancora of arquivosAncora) {
302
+ const ancoraNormalizada = normalizarFragmentoArquivo(ancora);
303
+ const diretorioAncora = path.posix.dirname(ancoraNormalizada);
304
+ if (alvo === ancoraNormalizada) {
305
+ return 8;
306
+ }
307
+ if (diretorioAlvo === diretorioAncora) {
308
+ scoreMaximo = Math.max(scoreMaximo, 6);
309
+ continue;
310
+ }
311
+ const ultimoDiretorioAlvo = diretorioAlvo.split("/").filter(Boolean).at(-1);
312
+ const ultimoDiretorioAncora = diretorioAncora.split("/").filter(Boolean).at(-1);
313
+ if (ultimoDiretorioAlvo && ultimoDiretorioAlvo === ultimoDiretorioAncora) {
314
+ scoreMaximo = Math.max(scoreMaximo, 4);
315
+ }
316
+ }
317
+ return scoreMaximo;
318
+ }
319
+ function pontuarProximidadeRotaConsumer(rota, rotasAncora) {
320
+ if (rotasAncora.length === 0) {
321
+ return 0;
322
+ }
323
+ const rotaNormalizada = normalizarCaminhoRota(rota);
324
+ const segmentosRota = rotaNormalizada.split("/").filter(Boolean);
325
+ let scoreMaximo = 0;
326
+ for (const ancora of rotasAncora) {
327
+ const rotaAncora = normalizarCaminhoRota(ancora);
328
+ if (rotaNormalizada === rotaAncora) {
329
+ return 8;
330
+ }
331
+ const segmentosAncora = rotaAncora.split("/").filter(Boolean);
332
+ if (segmentosRota[0] && segmentosRota[0] === segmentosAncora[0]) {
333
+ scoreMaximo = Math.max(scoreMaximo, 4);
334
+ }
335
+ }
336
+ return scoreMaximo;
337
+ }
338
+ function filtrarConsumerSurfacesPorEscopo(consumerSurfaces, consumerBridges, contexto, configuracao, relevancia) {
214
339
  const raizesWorktreePermitidas = resolverRaizesIgnoradasPermitidas(contexto, DIRETORIOS_WORKTREE);
215
340
  const raizesConsumidorPermitidas = resolverRaizesIgnoradasPermitidas(contexto, DIRETORIOS_CONSUMIDOR_LATERAL);
341
+ const limiar = configuracao.escopo === "arquivo" ? 5 : 4;
216
342
  const manterSurface = (surface) => {
217
343
  if (configuracao.ignorarWorktrees
218
344
  && caminhoIgnoradoForaDoEscopoReal(surface.arquivo, DIRETORIOS_WORKTREE, raizesWorktreePermitidas)) {
@@ -222,11 +348,13 @@ function filtrarConsumerSurfacesPorEscopo(consumerSurfaces, consumerBridges, con
222
348
  && caminhoIgnoradoForaDoEscopoReal(surface.arquivo, DIRETORIOS_CONSUMIDOR_LATERAL, raizesConsumidorPermitidas)) {
223
349
  return false;
224
350
  }
225
- const combinaEscopo = textoCombinaEscopo(`${surface.rota} ${surface.arquivo} ${surface.tipoArquivo}`, configuracao.termosEscopo);
226
- if (!configuracao.ignorarConsumidoresLaterais) {
227
- return combinaEscopo || configuracao.escopo === "projeto";
351
+ if (configuracao.escopo === "projeto") {
352
+ return true;
228
353
  }
229
- return configuracao.escopo === "projeto" ? true : combinaEscopo;
354
+ const score = pontuarTextoEscopo(`${surface.rota} ${surface.arquivo} ${surface.tipoArquivo}`, configuracao.termosEscopo)
355
+ + pontuarProximidadeArquivoConsumer(surface.arquivo, relevancia?.arquivosAncora ?? [])
356
+ + pontuarProximidadeRotaConsumer(surface.rota, relevancia?.rotasAncora ?? []);
357
+ return score >= limiar;
230
358
  };
231
359
  const manterBridge = (bridge) => {
232
360
  if (configuracao.ignorarWorktrees
@@ -237,11 +365,12 @@ function filtrarConsumerSurfacesPorEscopo(consumerSurfaces, consumerBridges, con
237
365
  && caminhoIgnoradoForaDoEscopoReal(bridge.arquivo, DIRETORIOS_CONSUMIDOR_LATERAL, raizesConsumidorPermitidas)) {
238
366
  return false;
239
367
  }
240
- const combinaEscopo = textoCombinaEscopo(`${bridge.caminho} ${bridge.arquivo} ${bridge.simbolo}`, configuracao.termosEscopo);
241
- if (!configuracao.ignorarConsumidoresLaterais) {
242
- return combinaEscopo || configuracao.escopo === "projeto";
368
+ if (configuracao.escopo === "projeto") {
369
+ return true;
243
370
  }
244
- return configuracao.escopo === "projeto" ? true : combinaEscopo;
371
+ const score = pontuarTextoEscopo(`${bridge.caminho} ${bridge.arquivo} ${bridge.simbolo}`, configuracao.termosEscopo)
372
+ + pontuarProximidadeArquivoConsumer(bridge.arquivo, relevancia?.arquivosAncora ?? []);
373
+ return score >= limiar;
245
374
  };
246
375
  return {
247
376
  consumerSurfaces: consumerSurfaces.filter(manterSurface),
@@ -542,6 +671,29 @@ function extrairRecursosRedis(arquivo, codigo) {
542
671
  }
543
672
  return [...recursos.values()];
544
673
  }
674
+ function extrairRecursosArquivoLocal(arquivo, codigo) {
675
+ const recursos = new Map();
676
+ const contextoArquivoLocal = /\b(?:json|jsonl|ndjson)\b/i.test(codigo)
677
+ || /\b(?:read_text|write_text|readFile(?:Sync)?|writeFile(?:Sync)?|open)\b/i.test(codigo)
678
+ || /\.(?:json|jsonl|ndjson|db|sqlite|sqlite3)\b/i.test(codigo)
679
+ || /(?:repository|repositories|repositorio|repo|store|storage|persist|cache)/i.test(normalizarFragmentoArquivo(arquivo));
680
+ if (!contextoArquivoLocal) {
681
+ return [];
682
+ }
683
+ for (const match of codigo.matchAll(/["'`]([^"'`]+\.(?:json|jsonl|ndjson|db|sqlite|sqlite3))["'`]/gi)) {
684
+ const literal = match[1] ?? "";
685
+ const nomeBase = path.basename(literal, path.extname(literal));
686
+ registrarRecursoDrift(recursos, "arquivo", "arquivo_local", nomeBase, arquivo);
687
+ }
688
+ const nomeArquivo = path.basename(arquivo).replace(/\.(?:ts|tsx|js|jsx|mjs|cjs|py|dart|cs|java|go|rs|cpp|cc|cxx|hpp|h)$/i, "");
689
+ const nomeStore = nomeArquivo
690
+ .replace(/(?:[_.-]?(?:repository|repositories|repo|store|storage|persistencia|persistence))$/i, "")
691
+ .trim();
692
+ if (nomeStore && /(?:repository|repositories|repositorio|repo|store|storage|persist|cache)/i.test(nomeArquivo)) {
693
+ registrarRecursoDrift(recursos, "arquivo", "arquivo_local", nomeStore, arquivo);
694
+ }
695
+ return [...recursos.values()];
696
+ }
545
697
  function extrairRecursosPersistenciaCodigoVivo(arquivo, codigo) {
546
698
  const recursos = new Map();
547
699
  for (const recurso of extrairColecoesFirebase(arquivo, codigo)) {
@@ -556,6 +708,9 @@ function extrairRecursosPersistenciaCodigoVivo(arquivo, codigo) {
556
708
  for (const recurso of extrairRecursosRedis(arquivo, codigo)) {
557
709
  registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
558
710
  }
711
+ for (const recurso of extrairRecursosArquivoLocal(arquivo, codigo)) {
712
+ registrarRecursoDrift(recursos, recurso.origem, recurso.tipo, recurso.nome, recurso.arquivo, recurso.simbolo);
713
+ }
559
714
  return [...recursos.values()];
560
715
  }
561
716
  function extrairRecursosPrisma(arquivo, codigo) {
@@ -1757,7 +1912,11 @@ async function indexarCpp(diretorios) {
1757
1912
  async function indexarPersistenciaDeclarativa(diretorios) {
1758
1913
  const recursos = new Map();
1759
1914
  for (const diretorio of diretorios) {
1760
- const arquivos = await listarArquivosRecursivos(diretorio, [".sql", ".psql", ".ddl", ".prisma"]);
1915
+ const arquivos = await listarArquivosRecursivos(diretorio, [
1916
+ ".sql", ".psql", ".ddl", ".prisma",
1917
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
1918
+ ".py", ".dart", ".cs", ".java", ".go", ".rs", ".cpp", ".cc", ".cxx", ".hpp", ".h",
1919
+ ]);
1761
1920
  for (const arquivo of arquivos) {
1762
1921
  const codigo = await readFile(arquivo, "utf8");
1763
1922
  const extracoes = arquivo.endsWith(".prisma")
@@ -1771,33 +1930,35 @@ async function indexarPersistenciaDeclarativa(diretorios) {
1771
1930
  return { recursos: [...recursos.values()] };
1772
1931
  }
1773
1932
  function normalizarOrigemParaEngine(origem) {
1774
- return origem && origem !== "firebase" ? origem : undefined;
1933
+ return origem && origem !== "firebase" && origem !== "arquivo" ? origem : undefined;
1775
1934
  }
1776
- function registrarColunaPersistenciaDrift(colunas, engine, recurso, coluna, arquivo) {
1935
+ function registrarColunaPersistenciaDrift(colunas, origem, recurso, coluna, arquivo) {
1777
1936
  const recursoNormalizado = fecharPrefixoRecurso(limparLiteralRecurso(recurso));
1778
1937
  const colunaNormalizada = fecharPrefixoRecurso(limparLiteralRecurso(coluna));
1779
1938
  if (!recursoNormalizado || !colunaNormalizada || recursoEhIgnorado(colunaNormalizada)) {
1780
1939
  return;
1781
1940
  }
1782
- const chave = `${engine}:${normalizarNomeRecursoDrift(recursoNormalizado)}:${normalizarNomeRecursoDrift(colunaNormalizada)}:${arquivo}`;
1941
+ const chave = `${origem}:${normalizarNomeRecursoDrift(recursoNormalizado)}:${normalizarNomeRecursoDrift(colunaNormalizada)}:${arquivo}`;
1783
1942
  if (!colunas.has(chave)) {
1784
1943
  colunas.set(chave, {
1785
- engine,
1944
+ origem,
1945
+ categoriaPersistencia: categorizarPersistenciaPorOrigem(origem),
1786
1946
  recurso: recursoNormalizado,
1787
1947
  coluna: colunaNormalizada,
1788
1948
  arquivo,
1789
1949
  });
1790
1950
  }
1791
1951
  }
1792
- function registrarRepositorioPersistenciaDrift(repositorios, engine, recurso, arquivo) {
1952
+ function registrarRepositorioPersistenciaDrift(repositorios, origem, recurso, arquivo) {
1793
1953
  const recursoNormalizado = fecharPrefixoRecurso(limparLiteralRecurso(recurso));
1794
1954
  if (!recursoNormalizado) {
1795
1955
  return;
1796
1956
  }
1797
- const chave = `${engine}:${normalizarNomeRecursoDrift(recursoNormalizado)}:${arquivo}`;
1957
+ const chave = `${origem}:${normalizarNomeRecursoDrift(recursoNormalizado)}:${arquivo}`;
1798
1958
  if (!repositorios.has(chave)) {
1799
1959
  repositorios.set(chave, {
1800
- engine,
1960
+ origem,
1961
+ categoriaPersistencia: categorizarPersistenciaPorOrigem(origem),
1801
1962
  recurso: recursoNormalizado,
1802
1963
  arquivo,
1803
1964
  });
@@ -1912,19 +2073,38 @@ function extrairCamposRedisDetalhados(arquivo, codigo) {
1912
2073
  }
1913
2074
  return [...colunas.values()];
1914
2075
  }
2076
+ function extrairCamposArquivoLocalDetalhados(arquivo, codigo) {
2077
+ const colunas = new Map();
2078
+ const recursos = extrairRecursosArquivoLocal(arquivo, codigo);
2079
+ if (recursos.length === 0) {
2080
+ return [];
2081
+ }
2082
+ const registrarCampo = (recurso, trecho) => {
2083
+ for (const match of trecho.matchAll(/["'`]?([A-Za-z_][\w$-]*)["'`]?\s*:/g)) {
2084
+ registrarColunaPersistenciaDrift(colunas, "arquivo", recurso, match[1], arquivo);
2085
+ }
2086
+ };
2087
+ const blocos = [
2088
+ ...codigo.matchAll(/\b(?:_empty_store|empty_store|default_store)\b[\s\S]{0,1000}?return\s*\{([\s\S]*?)\n\s*\}/g),
2089
+ ...codigo.matchAll(/\bstore\s*=\s*\{([\s\S]*?)\n\s*\}/g),
2090
+ ];
2091
+ for (const recurso of recursos) {
2092
+ for (const bloco of blocos) {
2093
+ registrarCampo(recurso.nome, bloco[1] ?? "");
2094
+ }
2095
+ }
2096
+ return [...colunas.values()];
2097
+ }
1915
2098
  function registrarRepositoriosPorRecursos(repositorios, arquivo, codigo, recursos) {
1916
2099
  const contextoRepositorio = /(?:repository|repositories|repositorio|repositorios|repo|dao|store|queries|persistence|persistencia)/i.test(arquivo)
1917
2100
  || /\b(?:Repository|Repositories|Dao|Store)\b/.test(codigo);
1918
- const contextoAcesso = /\b(?:select|insert|update|delete|aggregate|findOne|findMany|findUnique|findFirst|prisma\.|db\.collection|createClient|hset|hget|xadd|xread)\b/i.test(codigo);
2101
+ 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)\b/i.test(codigo)
2102
+ || /\.(?:json|jsonl|ndjson|db|sqlite|sqlite3)\b/i.test(codigo);
1919
2103
  if (!contextoRepositorio && !contextoAcesso) {
1920
2104
  return;
1921
2105
  }
1922
2106
  for (const recurso of recursos) {
1923
- const engine = normalizarOrigemParaEngine(recurso.origem);
1924
- if (!engine) {
1925
- continue;
1926
- }
1927
- registrarRepositorioPersistenciaDrift(repositorios, engine, recurso.nome, arquivo);
2107
+ registrarRepositorioPersistenciaDrift(repositorios, recurso.origem, recurso.nome, arquivo);
1928
2108
  }
1929
2109
  }
1930
2110
  async function indexarPersistenciaDetalhada(diretorios) {
@@ -1942,16 +2122,19 @@ async function indexarPersistenciaDetalhada(diretorios) {
1942
2122
  ? extrairRecursosPrisma(arquivo, codigo)
1943
2123
  : extrairRecursosPersistenciaCodigoVivo(arquivo, codigo);
1944
2124
  for (const coluna of extrairColunasSqlDetalhadas(arquivo, codigo)) {
1945
- registrarColunaPersistenciaDrift(colunas, coluna.engine, coluna.recurso, coluna.coluna, coluna.arquivo);
2125
+ registrarColunaPersistenciaDrift(colunas, coluna.origem, coluna.recurso, coluna.coluna, coluna.arquivo);
1946
2126
  }
1947
2127
  for (const coluna of extrairColunasPrismaDetalhadas(arquivo, codigo)) {
1948
- registrarColunaPersistenciaDrift(colunas, coluna.engine, coluna.recurso, coluna.coluna, coluna.arquivo);
2128
+ registrarColunaPersistenciaDrift(colunas, coluna.origem, coluna.recurso, coluna.coluna, coluna.arquivo);
1949
2129
  }
1950
2130
  for (const coluna of extrairCamposMongoDetalhados(arquivo, codigo)) {
1951
- registrarColunaPersistenciaDrift(colunas, coluna.engine, coluna.recurso, coluna.coluna, coluna.arquivo);
2131
+ registrarColunaPersistenciaDrift(colunas, coluna.origem, coluna.recurso, coluna.coluna, coluna.arquivo);
1952
2132
  }
1953
2133
  for (const coluna of extrairCamposRedisDetalhados(arquivo, codigo)) {
1954
- registrarColunaPersistenciaDrift(colunas, coluna.engine, coluna.recurso, coluna.coluna, coluna.arquivo);
2134
+ registrarColunaPersistenciaDrift(colunas, coluna.origem, coluna.recurso, coluna.coluna, coluna.arquivo);
2135
+ }
2136
+ for (const coluna of extrairCamposArquivoLocalDetalhados(arquivo, codigo)) {
2137
+ registrarColunaPersistenciaDrift(colunas, coluna.origem, coluna.recurso, coluna.coluna, coluna.arquivo);
1955
2138
  }
1956
2139
  registrarRepositoriosPorRecursos(repositorios, arquivo, codigo, recursos);
1957
2140
  }
@@ -1964,12 +2147,70 @@ async function indexarPersistenciaDetalhada(diretorios) {
1964
2147
  function recursoDetalhadoCombina(recurso, esperado) {
1965
2148
  return variantesNomeRecursoDrift(recurso).some((variante) => esperado.nomes.some((nome) => variantesNomeRecursoDrift(nome).includes(variante)));
1966
2149
  }
2150
+ function deduplicarRecursosResolvidos(recursos) {
2151
+ return [...new Map(recursos.map((recurso) => [`${recurso.origem}:${recurso.tipo}:${recurso.nome}:${recurso.arquivo}:${recurso.simbolo ?? ""}`, recurso])).values()];
2152
+ }
2153
+ function normalizarArquivoDeclaradoDrift(valor) {
2154
+ return normalizarFragmentoArquivo(valor);
2155
+ }
2156
+ function arquivoCombinaDeclaradoDrift(arquivoReal, arquivoDeclarado) {
2157
+ const real = normalizarArquivoDeclaradoDrift(arquivoReal);
2158
+ const declarado = normalizarArquivoDeclaradoDrift(arquivoDeclarado);
2159
+ return real === declarado || real.endsWith(declarado) || declarado.endsWith(real);
2160
+ }
2161
+ function coletarArquivosPreferidosPersistenciaTask(task) {
2162
+ const arquivos = new Set();
2163
+ for (const vinculo of task.vinculos) {
2164
+ if (vinculo.arquivo) {
2165
+ arquivos.add(vinculo.arquivo);
2166
+ }
2167
+ if (vinculo.tipo === "arquivo" && vinculo.valor) {
2168
+ arquivos.add(vinculo.valor);
2169
+ }
2170
+ }
2171
+ return arquivos;
2172
+ }
2173
+ function resolverPersistenciaLocalPorTask(mapaRecursos, task, ir, esperado) {
2174
+ const todosRecursos = deduplicarRecursosResolvidos([...mapaRecursos.values()].flat());
2175
+ const arquivosPreferidos = [...coletarArquivosPreferidosPersistenciaTask(task)];
2176
+ const candidatosPorArquivo = arquivosPreferidos.length > 0
2177
+ ? todosRecursos.filter((recurso) => recurso.origem === "arquivo"
2178
+ && arquivosPreferidos.some((arquivo) => arquivoCombinaDeclaradoDrift(recurso.arquivo, arquivo)))
2179
+ : [];
2180
+ if (candidatosPorArquivo.length > 0) {
2181
+ return candidatosPorArquivo;
2182
+ }
2183
+ const termos = new Set([
2184
+ ...quebrarTermosEscopo(ir.nome),
2185
+ ...quebrarTermosEscopo(task.nome),
2186
+ ...quebrarTermosEscopo(esperado.alvo),
2187
+ ]);
2188
+ if (termos.size === 0) {
2189
+ return [];
2190
+ }
2191
+ return todosRecursos.filter((recurso) => recurso.origem === "arquivo"
2192
+ && [...termos].some((termo) => variantesNomeRecursoDrift(recurso.nome).some((variacao) => variacao.includes(termo))));
2193
+ }
2194
+ function detalhePersistenciaCombinaOrigem(origemDetalhe, recursoReal) {
2195
+ if (!recursoReal) {
2196
+ return true;
2197
+ }
2198
+ return origemDetalhe === recursoReal.origem;
2199
+ }
1967
2200
  function localizarCompatibilidadePersistencia(bancos, esperado, recursoReal) {
1968
2201
  for (const banco of bancos) {
1969
2202
  for (const recurso of banco.resources) {
1970
2203
  if (!recursoPersistenciaCombinaAlvo(recurso, esperado.alvo)) {
1971
2204
  continue;
1972
2205
  }
2206
+ if (recursoReal?.origem === "arquivo") {
2207
+ return {
2208
+ engine: "arquivo",
2209
+ compatibilidade: "adaptado",
2210
+ motivoCompatibilidade: `Persistencia local/arquivo detectada no codigo vivo em vez do engine ${banco.engine}.`,
2211
+ tipo: recursoReal.tipo,
2212
+ };
2213
+ }
1973
2214
  const engine = banco.engine ?? normalizarOrigemParaEngine(recursoReal?.origem);
1974
2215
  const compatibilidade = engine
1975
2216
  ? recurso.compatibilidade.find((item) => item.engine === engine) ?? recurso.compatibilidade[0]
@@ -1982,6 +2223,14 @@ function localizarCompatibilidadePersistencia(bancos, esperado, recursoReal) {
1982
2223
  };
1983
2224
  }
1984
2225
  }
2226
+ if (recursoReal?.origem === "arquivo") {
2227
+ return {
2228
+ engine: "arquivo",
2229
+ compatibilidade: "desconhecida",
2230
+ motivoCompatibilidade: "Persistencia local/arquivo detectada sem database vendor-first declarado.",
2231
+ tipo: recursoReal.tipo,
2232
+ };
2233
+ }
1985
2234
  return {
1986
2235
  engine: recursoReal?.origem ?? esperado.origem ?? "desconhecido",
1987
2236
  compatibilidade: "desconhecida",
@@ -2002,22 +2251,41 @@ export async function analisarPersistenciaReal(contexto, mapaRecursos, detalhesP
2002
2251
  for (const task of ir.tasks) {
2003
2252
  for (const esperado of extrairRecursosEsperados(task, ir)) {
2004
2253
  const correspondencias = esperado.nomes.flatMap((nome) => variantesNomeRecursoDrift(nome).flatMap((variante) => (mapa.get(variante) ?? []).filter((recurso) => recursoResolvidoCombinaEsperado(recurso, esperado))));
2005
- const recursosReais = [...new Map(correspondencias.map((recurso) => [`${recurso.origem}:${recurso.tipo}:${recurso.nome}:${recurso.arquivo}`, recurso])).values()];
2254
+ let recursosReais = deduplicarRecursosResolvidos(correspondencias);
2255
+ const arquivosPreferidos = [...coletarArquivosPreferidosPersistenciaTask(task)];
2256
+ if (recursosReais.length === 0) {
2257
+ recursosReais = resolverPersistenciaLocalPorTask(mapa, task, ir, esperado);
2258
+ }
2006
2259
  const compatibilidade = localizarCompatibilidadePersistencia(ir.databases, esperado, recursosReais[0]);
2007
- const colunas = [...new Set(detalhes.colunas
2008
- .filter((coluna) => (!normalizarOrigemParaEngine(recursosReais[0]?.origem) || coluna.engine === normalizarOrigemParaEngine(recursosReais[0]?.origem))
2260
+ let colunas = [...new Set(detalhes.colunas
2261
+ .filter((coluna) => detalhePersistenciaCombinaOrigem(coluna.origem, recursosReais[0])
2009
2262
  && recursoDetalhadoCombina(coluna.recurso, esperado))
2010
2263
  .map((coluna) => coluna.coluna))].sort((a, b) => a.localeCompare(b, "pt-BR"));
2011
- const repositorios = [...new Set(detalhes.repositorios
2012
- .filter((repositorio) => (!normalizarOrigemParaEngine(recursosReais[0]?.origem) || repositorio.engine === normalizarOrigemParaEngine(recursosReais[0]?.origem))
2264
+ let repositorios = [...new Set(detalhes.repositorios
2265
+ .filter((repositorio) => detalhePersistenciaCombinaOrigem(repositorio.origem, recursosReais[0])
2013
2266
  && recursoDetalhadoCombina(repositorio.recurso, esperado))
2014
2267
  .map((repositorio) => repositorio.arquivo))].sort((a, b) => a.localeCompare(b, "pt-BR"));
2268
+ if (recursosReais.some((recurso) => recurso.origem === "arquivo") && arquivosPreferidos.length > 0) {
2269
+ if (colunas.length === 0) {
2270
+ colunas = [...new Set(detalhes.colunas
2271
+ .filter((coluna) => coluna.origem === "arquivo"
2272
+ && arquivosPreferidos.some((arquivo) => arquivoCombinaDeclaradoDrift(coluna.arquivo, arquivo)))
2273
+ .map((coluna) => coluna.coluna))].sort((a, b) => a.localeCompare(b, "pt-BR"));
2274
+ }
2275
+ if (repositorios.length === 0) {
2276
+ repositorios = [...new Set(detalhes.repositorios
2277
+ .filter((repositorio) => repositorio.origem === "arquivo"
2278
+ && arquivosPreferidos.some((arquivo) => arquivoCombinaDeclaradoDrift(repositorio.arquivo, arquivo)))
2279
+ .map((repositorio) => repositorio.arquivo))].sort((a, b) => a.localeCompare(b, "pt-BR"));
2280
+ }
2281
+ }
2015
2282
  const arquivos = [...new Set(recursosReais.map((recurso) => recurso.arquivo))].sort((a, b) => a.localeCompare(b, "pt-BR"));
2016
2283
  registros.push({
2017
2284
  modulo: ir.nome,
2018
2285
  task: task.nome,
2019
2286
  alvo: esperado.alvo,
2020
2287
  engine: compatibilidade.engine,
2288
+ categoriaPersistencia: categorizarPersistenciaPorOrigem((compatibilidade.engine === "desconhecido" ? undefined : compatibilidade.engine)),
2021
2289
  tipo: compatibilidade.tipo,
2022
2290
  status: recursosReais.length === 0
2023
2291
  ? "divergente"
@@ -2259,6 +2527,10 @@ function recursoPersistenciaCombinaAlvo(recurso, alvo) {
2259
2527
  }
2260
2528
  return nomesRecursoPersistencia(recurso).some((nome) => variantesNomeRecursoDrift(nome).some((variacao) => alvoVariantes.has(variacao)));
2261
2529
  }
2530
+ function taskSugerePersistenciaSemBanco(task) {
2531
+ return task.vinculos.some((vinculo) => /(?:repository|repositories|repositorio|repo|store|storage|persist|cache)/i.test(`${vinculo.valor} ${vinculo.arquivo ?? ""} ${vinculo.simbolo ?? ""}`))
2532
+ || task.implementacoesExternas.some((impl) => /(?:repository|repositories|repositorio|repo|store|storage|persist|cache)/i.test(impl.caminho));
2533
+ }
2262
2534
  function extrairRecursosEsperados(task, ir) {
2263
2535
  const esperados = new Map();
2264
2536
  const registrar = (esperado) => {
@@ -2279,7 +2551,25 @@ function extrairRecursosEsperados(task, ir) {
2279
2551
  }
2280
2552
  }
2281
2553
  const efeitosPersistencia = task.efeitosEstruturados.filter((efeito) => ["persistencia", "db.read", "db.write"].includes(efeito.categoria) && Boolean(efeito.alvo));
2282
- if (efeitosPersistencia.length === 0 || ir.databases.length === 0) {
2554
+ if (efeitosPersistencia.length === 0) {
2555
+ return [...esperados.values()];
2556
+ }
2557
+ if (ir.databases.length === 0) {
2558
+ const sugerePersistenciaLocal = taskSugerePersistenciaSemBanco(task);
2559
+ for (const efeito of efeitosPersistencia) {
2560
+ if (efeito.categoria !== "persistencia" || !sugerePersistenciaLocal) {
2561
+ continue;
2562
+ }
2563
+ if ([...esperados.values()].some((item) => item.alvo === efeito.alvo)) {
2564
+ continue;
2565
+ }
2566
+ registrar({
2567
+ categoria: "persistencia",
2568
+ alvo: efeito.alvo,
2569
+ tiposAceitos: ["table", "collection", "document", "keyspace", "stream", "view", "query", "index", "arquivo_local"],
2570
+ nomes: [efeito.alvo],
2571
+ });
2572
+ }
2283
2573
  return [...esperados.values()];
2284
2574
  }
2285
2575
  for (const efeito of efeitosPersistencia) {
@@ -2570,7 +2860,10 @@ export async function analisarDriftLegado(contexto, opcoes) {
2570
2860
  checksSugeridos: [],
2571
2861
  });
2572
2862
  for (const recursoEsperado of extrairRecursosEsperados(task, ir)) {
2573
- const resolvido = resolverRecursoEsperado(mapaRecursos, recursoEsperado, arquivosReferenciados);
2863
+ let resolvido = resolverRecursoEsperado(mapaRecursos, recursoEsperado, arquivosReferenciados);
2864
+ if (!resolvido) {
2865
+ resolvido = resolverPersistenciaLocalPorTask(mapaRecursos, task, ir, recursoEsperado)[0];
2866
+ }
2574
2867
  const registro = {
2575
2868
  modulo: ir.nome,
2576
2869
  task: task.nome,
@@ -2903,6 +3196,7 @@ export async function analisarDriftLegado(contexto, opcoes) {
2903
3196
  });
2904
3197
  }
2905
3198
  }
3199
+ const relevanciaConsumer = construirContextoRelevanciaConsumer(contexto, tasksResumo, vinculosValidos);
2906
3200
  const consumersFiltrados = filtrarConsumerSurfacesPorEscopo([...indexTs.consumerSurfaces, ...indexDart.consumerSurfaces].sort((a, b) => a.rota.localeCompare(b.rota, "pt-BR")
2907
3201
  || a.tipoArquivo.localeCompare(b.tipoArquivo, "pt-BR")
2908
3202
  || a.arquivo.localeCompare(b.arquivo, "pt-BR")), [...new Map([...indexTs.simbolos, ...indexDart.simbolos]
@@ -2915,7 +3209,7 @@ export async function analisarDriftLegado(contexto, opcoes) {
2915
3209
  simbolo: simbolo.simbolo,
2916
3210
  },
2917
3211
  ])).values()].sort((a, b) => a.caminho.localeCompare(b.caminho, "pt-BR")
2918
- || a.arquivo.localeCompare(b.arquivo, "pt-BR")), contexto, configuracaoEscopo);
3212
+ || a.arquivo.localeCompare(b.arquivo, "pt-BR")), contexto, configuracaoEscopo, relevanciaConsumer);
2919
3213
  const consumerSurfaces = consumersFiltrados.consumerSurfaces;
2920
3214
  const consumerBridges = consumersFiltrados.consumerBridges;
2921
3215
  const appRoutes = [...new Set(consumerSurfaces.map((surface) => surface.rota))]