File size: 4,530 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#!/usr/bin/env node
// scripts/check/check-circular-deps.mjs
// Gate: dpdm circular-deps cross-check (segunda opinião complementar ao check-cycles.mjs).
//
// check-cycles.mjs usa AST-TS próprio mas cobre apenas 5 sub-árvores + somente
// imports relativos. Este script usa dpdm (v4) que rastreia path-aliases via
// tsconfig.json e cobre entrypoints de alto risco.
//
// Advisory nesta versão: exit 0 sempre; imprime `circularDeps=N` para baseline.
// Direção da catraca: down (não pode subir). Adicionar ao quality-baseline.json
// como `{ value: N, direction: "down" }` após a primeira run verde no CI.
//
// Escopo limitado a 4 entrypoints principais para manter o tempo de análise
// abaixo de 60s. dpdm rastreia transitivamente todas as deps de cada entry.
// Cobrir mais entries aumenta o tempo sem proporcional ganho (as deps core se
// repetem via transitividade).
//
// Nota: dpdm pode reportar mais ciclos que check-cycles.mjs porque conta
// permutações de paths que passam pelo mesmo SCC, não apenas SCCs únicos.
// Isso é esperado — ferramentas diferentes, métricas complementares.

import { execFileSync } from "node:child_process";
import { existsSync } from "node:fs";
import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import os from "node:os";
import path from "node:path";
import fs from "node:fs";

const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = resolve(__dirname, "../..");

// Entrypoints: cobrem o pipeline principal (chat, combo, MCP, DB).
// Mantido enxuto para que `dpdm -T` (transform) termine em < 60s.
const ENTRYPOINTS = [
  "open-sse/handlers/chatCore.ts",
  "open-sse/services/combo.ts",
  "open-sse/mcp-server/index.ts",
  "src/lib/db/core.ts",
];

const DPDM_BIN = resolve(projectRoot, "node_modules/.bin/dpdm");
const TSCONFIG = resolve(projectRoot, "tsconfig.json");

/**
 * Parseia a saída JSON do dpdm e retorna a contagem de ciclos.
 * Função exportada para ser testada isoladamente sem executar o dpdm.
 *
 * @param {string} jsonStr - string com o JSON de saída do dpdm (campo "circulars").
 * @returns {{ count: number, circulars: string[][] }}
 */
export function parseDpdmOutput(jsonStr) {
  let parsed;
  try {
    parsed = JSON.parse(jsonStr);
  } catch {
    throw new Error(`dpdm JSON parse failed: ${jsonStr.slice(0, 200)}`);
  }
  const circulars = Array.isArray(parsed.circulars) ? parsed.circulars : [];
  return { count: circulars.length, circulars };
}

/**
 * Executa o dpdm e retorna a string JSON bruta do arquivo de saída.
 *
 * @returns {string} conteúdo JSON do arquivo temporário.
 */
function runDpdm() {
  if (!existsSync(DPDM_BIN)) {
    throw new Error(`dpdm binary not found at ${DPDM_BIN}. Run: npm install`);
  }

  const tmpFile = path.join(os.tmpdir(), `dpdm-output-${process.pid}.json`);

  try {
    execFileSync(
      "node",
      [
        DPDM_BIN,
        "--circular",
        "--no-warning",
        "--no-tree",
        "-T",
        "--tsconfig",
        TSCONFIG,
        "-o",
        tmpFile,
        ...ENTRYPOINTS,
      ],
      {
        cwd: projectRoot,
        stdio: "inherit",
        timeout: 120_000,
      }
    );

    if (!existsSync(tmpFile)) {
      throw new Error(`dpdm did not produce output file at ${tmpFile}`);
    }

    const raw = readFileSync(tmpFile, "utf8");
    return raw;
  } finally {
    try {
      fs.unlinkSync(tmpFile);
    } catch {
      // best-effort cleanup
    }
  }
}

function main() {
  console.log("[circular-deps] Running dpdm cross-check...");
  console.log(`[circular-deps] Entrypoints: ${ENTRYPOINTS.join(", ")}`);

  let raw;
  try {
    raw = runDpdm();
  } catch (err) {
    console.error(`[circular-deps] ERROR running dpdm: ${err.message}`);
    process.exit(1);
  }

  let result;
  try {
    result = parseDpdmOutput(raw);
  } catch (err) {
    console.error(`[circular-deps] ERROR parsing dpdm output: ${err.message}`);
    process.exit(1);
  }

  // Advisory mode: always exit 0. Catraca pode ser adicionada no quality-baseline.json
  // após baseline ser estabelecida.
  console.log(`[circular-deps] circularDeps=${result.count}`);
  console.log(
    `[circular-deps] Advisory — add to quality-baseline.json: { "value": ${result.count}, "direction": "down" }`
  );
  process.exit(0);
}

// Permite que o módulo seja importado em testes sem executar main().
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main();