@danhachuel/pandanews 0.4.3 → 0.4.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  Todas as versões publicadas são registradas aqui antes do publish (regra em AGENTS.md). A etiqueta visual usa PATCH de dois dígitos (ex.: 0.1.00); o registry npm normaliza para a versão canônica (ex.: 0.1.0).
4
4
 
5
+ ## 0.4.04 (2026-11-08)
6
+
7
+ - **HOTFIX real — fim das janelas de CMD no instalador**: o 0.4.03 blindou apenas o launcher (`start.js`) e deixou o **caminho de instalação** descoberto. No Windows, `tar` (extração do Python portátil), `python -m venv` e `pip install -r requirements.txt` eram lançados **sem `windowsHide`** — era aí que as múltiplas janelas de terminal apareciam. Os três `spawnSync` de `scripts/install.js` agora nascem com `windowsHide: true` (`CREATE_NO_WINDOW`)
8
+ - `scripts/build.js` blindado também: `npm install` e `npm run build` do frontend são chamados com `shell: true` (o wrapper `cmd.exe`/`npm.cmd` entra na conta) — ambos agora com `windowsHide: true`, sem piscar tela preta
9
+ - Saída preservada: o instalador mantém `stdio: 'inherit'`, então o progresso continua visível **no console que o usuário já abriu** — só a janela extra deixa de existir (verificado empiricamente no Windows)
10
+ - **Helper Python único e obrigatório**: `backend/core/proc.py` expõe `run_silent_process` / `run_silent` / `spawn_silent` / `call_silent` / `check_output_silent` / `system_silent`, aplicando `CREATE_NO_WINDOW = 0x08000000` via `creationflags` no Windows. O pipeline e o APScheduler rodam em memória (threads do `BackgroundScheduler`), sem processos do SO; o helper é o ponto único para qualquer subprocesso futuro
11
+ - **Teste de regressão ampliado e validado**: `scripts/test-start-isolation.mjs` deixa de olhar só o `start.js` e passa a varrer **todos** os pontos de entrada — `start.js`, `install.js`, `build.js` e `bin/cli.js` — exigindo `windowsHide: true` em cada `spawn`/`spawnSync`/`exec`/`execSync` (inclusive quando as opções vêm de constante compartilhada, ex. `...SILENT_WINDOWS`)
12
+ - O teste também trava regressões do backend Python: qualquer `subprocess.run/Popen/call/check_output/check_call` ou `os.system`/`os.popen` direto em `backend/**/*.py` falha o build (precisa passar por `backend.core.proc`)
13
+ - E ainda barra `uvicorn` com `--reload`/`--workers` no launcher — cada worker é um processo filho extra que, no Windows, pode alocar console próprio e ignorar as flags do pai
14
+ - **Guard verificado por mutação**: com a flag removida do `pip`, com um `subprocess.run` direto no backend e com `--reload` no uvicorn, o teste falha em todos os casos (exit 1) — não é carimbo de borracha
15
+ - Nota de transparência: a colisão de versão com o 0.4.03 já publicado no registry foi resolvida por renumeração, conforme a regra de sessões simultâneas do AGENTS.md
16
+
5
17
  ## 0.4.03 (2026-11-08)
6
18
 
7
19
  - **Regra permanente no AGENTS.md**: é PROIBIDO criar/publicar versões que abram janelas de terminal (CMD/PowerShell/console Python) no computador do usuário — todo processo filho deve nascer com `windowsHide: true` + grupo isolado, rodar em background com saída apenas em log e ser encerrado por código; versão que exiba janela de console é inválida (hotfix + CHANGELOG)
@@ -31,7 +31,7 @@ async def lifespan(_: FastAPI):
31
31
  scheduler.parar()
32
32
 
33
33
 
34
- app = FastAPI(title="PandaNews API", version="0.4.3", lifespan=lifespan)
34
+ app = FastAPI(title="PandaNews API", version="0.4.4", lifespan=lifespan)
35
35
 
36
36
  app.add_middleware(
37
37
  CORSMiddleware,
@@ -0,0 +1,89 @@
1
+ """Processos filhos silenciosos (regra permanente do AGENTS.md).
2
+
3
+ O PandaNews NUNCA pode abrir janelas de terminal (CMD, PowerShell, console
4
+ Python) no computador do usuário. No Windows, todo processo filho precisa da
5
+ flag ``CREATE_NO_WINDOW`` (0x08000000); sem ela, qualquer ``subprocess`` ou
6
+ gerador de console aloca uma tela preta.
7
+
8
+ Este módulo é o ÚNICO ponto de entrada autorizado para subprocessos no
9
+ backend. Nunca chame ``subprocess.run``/``subprocess.Popen``/``subprocess.call``
10
+ nem ``os.system`` diretamente — use ``run_silent_process``/``run_silent``/
11
+ ``spawn_silent``. O teste ``scripts/test-start-isolation.mjs`` (rodado no CI)
12
+ falha o build se encontrar uma chamada direta em ``backend/**/*.py``.
13
+
14
+ Exemplos::
15
+
16
+ from backend.core.proc import run_silent_process, spawn_silent
17
+
18
+ # execução síncrona
19
+ run_silent_process(["ffmpeg", "-version"], check=True, capture_output=True)
20
+
21
+ # execução assíncrona (não bloqueia o event loop)
22
+ proc = spawn_silent(["pandoc", "--version"])
23
+ proc.wait()
24
+
25
+ O pipeline de notícias e o APScheduler rodam em memória (threads do
26
+ ``BackgroundScheduler``), sem processos do sistema operacional — este módulo
27
+ só é necessário quando uma ferramenta externa realmente precisa ser chamada.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import subprocess
32
+ import sys
33
+
34
+ # Flag do Windows: o processo nasce sem console, portanto sem janela visível.
35
+ # Ver https://learn.microsoft.com/windows/win32/procthread/process-creation-flags
36
+ CREATE_NO_WINDOW = 0x08000000
37
+
38
+
39
+ def silent_kwargs(kwargs: dict | None = None) -> dict:
40
+ """Devolve as opções de subprocesso com a flag de ocultação aplicada.
41
+
42
+ Em Windows adiciona ``creationflags=CREATE_NO_WINDOW``; nas demais
43
+ plataformas não há console novo a ocultar, então as opções passam
44
+ intactas. Nunca use ``shell=True`` junto a este helper.
45
+ """
46
+ opts = dict(kwargs or {})
47
+ if sys.platform == "win32":
48
+ opts["creationflags"] = CREATE_NO_WINDOW
49
+ return opts
50
+
51
+
52
+ def run_silent_process(cmd_args, **kwargs):
53
+ """Substitui ``subprocess.run`` — sem janela de console no Windows."""
54
+ return subprocess.run(cmd_args, **silent_kwargs(kwargs))
55
+
56
+
57
+ # Alias: mesma semântica, nome curto para uso interno.
58
+ run_silent = run_silent_process
59
+
60
+
61
+ def spawn_silent(cmd_args, **kwargs):
62
+ """Substitui ``subprocess.Popen`` — sem janela de console no Windows."""
63
+ return subprocess.Popen(cmd_args, **silent_kwargs(kwargs))
64
+
65
+
66
+ def call_silent(cmd_args, **kwargs):
67
+ """Substitui ``subprocess.call`` — sem janela de console no Windows."""
68
+ return subprocess.call(cmd_args, **silent_kwargs(kwargs))
69
+
70
+
71
+ def check_output_silent(cmd_args, **kwargs):
72
+ """Substitui ``subprocess.check_output`` — sem janela de console no Windows."""
73
+ return subprocess.check_output(cmd_args, **silent_kwargs(kwargs))
74
+
75
+
76
+ def system_silent(cmd: str) -> int:
77
+ """Substitui ``os.system`` sem abrir shell visível.
78
+
79
+ ``os.system`` sempre passa por ``cmd.exe``/``/bin/sh`` e herda o console;
80
+ quando não há alternativa, a chamada é feita pelo helper silencioso.
81
+ """
82
+ if sys.platform == "win32":
83
+ proc = subprocess.run(
84
+ cmd,
85
+ shell=True,
86
+ creationflags=CREATE_NO_WINDOW,
87
+ )
88
+ return proc.returncode
89
+ return subprocess.call(cmd, shell=True)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/pandanews",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "SaaS local de criação e automação de múltiplos blogs de notícias com IA (FastAPI + React + Tailwind).",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/scripts/build.js CHANGED
@@ -15,12 +15,18 @@ export async function runBuild() {
15
15
  return;
16
16
  }
17
17
  const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
18
+ // Regra permanente (AGENTS.md): NENHUM spawn pode abrir janela de console
19
+ // no computador do usuário. No Windows o filho nasce com CREATE_NO_WINDOW
20
+ // (windowsHide: true) — com `shell: true` o wrapper cmd.exe também nasce
21
+ // oculto, sem piscar tela preta. O stdio segue 'inherit': a saída fica no
22
+ // console que o usuário já abriu.
23
+ const SILENT_WINDOWS = { windowsHide: true };
18
24
  console.log('Instalando dependências do frontend...');
19
25
  // shell: true — obrigatório para .cmd no Windows (Node >= 18)
20
- const install = spawnSync(npmCmd, ['install'], { cwd: frontendDir, stdio: 'inherit', shell: true });
26
+ const install = spawnSync(npmCmd, ['install'], { cwd: frontendDir, stdio: 'inherit', shell: true, ...SILENT_WINDOWS });
21
27
  if (install.status !== 0) throw new Error('Falha ao instalar dependências do frontend.');
22
28
  console.log('Compilando o frontend...');
23
- const build = spawnSync(npmCmd, ['run', 'build'], { cwd: frontendDir, stdio: 'inherit', shell: true });
29
+ const build = spawnSync(npmCmd, ['run', 'build'], { cwd: frontendDir, stdio: 'inherit', shell: true, ...SILENT_WINDOWS });
24
30
  if (build.status !== 0) throw new Error('Falha ao compilar o frontend.');
25
31
  console.log('Frontend compilado em frontend/dist.');
26
32
  }
@@ -21,6 +21,20 @@ import fse from 'fs-extra';
21
21
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
22
22
  export const PKG_ROOT = join(__dirname, '..');
23
23
 
24
+ // ------------------------------------------------------------------------
25
+ // Regra permanente (AGENTS.md): é PROIBIDO abrir janelas de terminal (CMD,
26
+ // PowerShell, console Python) no computador do usuário. Todo processo filho
27
+ // lançado pelo instalador nasce no Windows com `windowsHide: true`, que aplica
28
+ // a flag CREATE_NO_WINDOW — nenhuma tela preta aparece.
29
+ //
30
+ // `detached: true` NÃO é usado aqui de propósito: essas chamadas são curtas
31
+ // (tar/venv/pip) e o `stdio: 'inherit'` mantém o progresso no console que o
32
+ // usuário JÁ abriu. Com CREATE_NO_WINDOW o filho escreve nos handles herdados
33
+ // sem alocar uma janela extra. O teste scripts/test-start-isolation.mjs varre
34
+ // este arquivo e falha o build se qualquer spawn perder o windowsHide.
35
+ // ------------------------------------------------------------------------
36
+ const SILENT_WINDOWS = { windowsHide: true };
37
+
24
38
  const PYTHON_VERSION = '3.12.7';
25
39
  const PYTHON_RELEASE = '20241016';
26
40
 
@@ -152,7 +166,7 @@ async function downloadPython(home, platform) {
152
166
  const buf = Buffer.from(await res.arrayBuffer());
153
167
  writeFileSync(archive, buf);
154
168
  console.log(`Download concluído (${(buf.length / 1024 / 1024).toFixed(1)} MB). Extraindo...`);
155
- const tar = spawnSync('tar', ['-xzf', archive, '-C', runtimeDir]);
169
+ const tar = spawnSync('tar', ['-xzf', archive, '-C', runtimeDir], SILENT_WINDOWS);
156
170
  if (tar.status !== 0) throw new Error('Falha ao extrair o Python portátil (tar).');
157
171
  if (!existsSync(pythonBin(home))) {
158
172
  throw new Error('Extração do Python concluída, mas o binário não foi encontrado.');
@@ -162,7 +176,10 @@ async function downloadPython(home, platform) {
162
176
 
163
177
  function createVenv(home) {
164
178
  console.log('Criando virtualenv...');
165
- const venv = spawnSync(pythonBin(home), ['-m', 'venv', join(home, '.runtime', 'venv')], { stdio: 'inherit' });
179
+ const venv = spawnSync(pythonBin(home), ['-m', 'venv', join(home, '.runtime', 'venv')], {
180
+ stdio: 'inherit',
181
+ ...SILENT_WINDOWS,
182
+ });
166
183
  if (venv.status !== 0) throw new Error('Falha ao criar o virtualenv.');
167
184
  if (!existsSync(venvBin(home))) throw new Error('Virtualenv criado, mas o binário não foi encontrado.');
168
185
  }
@@ -173,7 +190,7 @@ function installDeps(home) {
173
190
  const pip = spawnSync(
174
191
  venvBin(home),
175
192
  ['-m', 'pip', 'install', '--disable-pip-version-check', '-r', req],
176
- { stdio: 'inherit' },
193
+ { stdio: 'inherit', ...SILENT_WINDOWS },
177
194
  );
178
195
  if (pip.status !== 0) throw new Error('Falha ao instalar as dependências do backend.');
179
196
  }
@@ -1,20 +1,134 @@
1
- // Teste de regressão do isolamento de processos do launcher (scripts/start.js).
1
+ // Teste de regressão do isolamento de processos (regra permanente do AGENTS.md):
2
+ // NENHUM spawn/exec do launcher/instalador pode abrir janela de terminal (CMD,
3
+ // PowerShell, console Python) no computador do usuário.
2
4
  //
3
- // Falha se as flags de isolamento forem removidas: detached (novo grupo de
4
- // processos — imune a Ctrl+C do console no Windows), windowsHide (sem janela
5
- // fantasma), stdio no arquivo (backend sobrevive ao terminal), unref e o
6
- // arquivo backend.pid (encerramento gracioso por código).
5
+ // Cobre TODOS os pontos de entrada:
6
+ // scripts/start.js — backend uvicorn + taskkill
7
+ // scripts/install.js — tar / venv / pip
8
+ // scripts/build.js — npm.cmd install e run build (shell: true)
9
+ // bin/cli.js — despacho de comandos
10
+ // backend/**/*.py — nenhuma chamada direta a subprocess/os.system
7
11
  //
8
12
  // Roda em qualquer plataforma, inclusive no CI (Linux/ubuntu) — sem exigir o
9
13
  // runtime Python completo.
10
- import { readFileSync } from 'node:fs';
14
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
11
15
  import { join, dirname } from 'node:path';
12
16
  import { fileURLToPath } from 'node:url';
13
17
 
14
18
  const root = join(dirname(fileURLToPath(import.meta.url)), '..');
15
- const src = readFileSync(join(root, 'scripts', 'start.js'), 'utf8');
16
19
 
17
- const verificacoes = [
20
+ const falhas = [];
21
+
22
+ function assert(condicao, mensagem) {
23
+ console.log(`${condicao ? 'PASS' : 'FAIL'} — ${mensagem}`);
24
+ if (!condicao) falhas.push(mensagem);
25
+ }
26
+
27
+ // ---------------------------------------------------------------- utilidades
28
+
29
+ // Extrai o conteúdo entre delimitadores balanceados, começando em `inicio`
30
+ // (índice do delimitador de abertura). Ignora delimitadores dentro de strings.
31
+ function ateOFechamento(src, inicio, abre, fecha) {
32
+ let profundidade = 1;
33
+ let aspa = null;
34
+ let escapando = false;
35
+ for (let i = inicio + 1; i < src.length; i++) {
36
+ const c = src[i];
37
+ if (escapando) {
38
+ escapando = false;
39
+ continue;
40
+ }
41
+ if (c === '\\') {
42
+ escapando = true;
43
+ continue;
44
+ }
45
+ if (aspa) {
46
+ if (c === aspa) aspa = null;
47
+ continue;
48
+ }
49
+ if (c === "'" || c === '"' || c === '`') {
50
+ aspa = c;
51
+ continue;
52
+ }
53
+ if (c === abre) profundidade += 1;
54
+ else if (c === fecha) {
55
+ profundidade -= 1;
56
+ if (profundidade === 0) return src.slice(inicio + 1, i);
57
+ }
58
+ }
59
+ return src.slice(inicio + 1);
60
+ }
61
+
62
+ // Texto completo dos argumentos de uma chamada, a partir do '(' encontrado.
63
+ function argumentosDaChamada(src, indiceAbre) {
64
+ return ateOFechamento(src, indiceAbre, '(', ')');
65
+ }
66
+
67
+ // Objetos literais de nível de módulo (const NOME = { ... }) — servem para
68
+ // resolver spreads como `...SILENT_WINDOWS` dentro das opções de spawn.
69
+ function constantesDeModulo(src) {
70
+ const mapa = new Map();
71
+ const regex = /const\s+([A-Za-z_$][\w$]*)\s*=\s*\{/g;
72
+ let m;
73
+ while ((m = regex.exec(src)) !== null) {
74
+ const inicioChaves = m.index + m[0].length - 1;
75
+ mapa.set(m[1], ateOFechamento(src, inicioChaves, '{', '}'));
76
+ }
77
+ return mapa;
78
+ }
79
+
80
+ // Uma referência a constante (solta ou via spread `...NOME`) é silenciosa
81
+ // apenas se a constante NOME trouxer windowsHide.
82
+ function referenciaConstanteSilenciosa(argTexto, constantes) {
83
+ const regex = /([A-Za-z_$][\w$]*)/g;
84
+ let m;
85
+ while ((m = regex.exec(argTexto)) !== null) {
86
+ const definicao = constantes.get(m[1]);
87
+ if (definicao && /windowsHide:\s*true/.test(definicao)) return true;
88
+ }
89
+ return false;
90
+ }
91
+
92
+ // ------------------------------------------------------------ varredura JS
93
+
94
+ const PONTOS_DE_ENTRADA_JS = ['scripts/start.js', 'scripts/install.js', 'scripts/build.js', 'bin/cli.js'];
95
+
96
+ for (const relativo of PONTOS_DE_ENTRADA_JS) {
97
+ const caminho = join(root, relativo);
98
+ if (!existsSync(caminho)) {
99
+ assert(false, `${relativo} — arquivo ausente`);
100
+ continue;
101
+ }
102
+ const src = readFileSync(caminho, 'utf8');
103
+ const constantes = constantesDeModulo(src);
104
+
105
+ const chamadas = [...src.matchAll(/\b(spawn|spawnSync|exec|execSync|execFile|execFileSync)\s*\(/g)];
106
+ let silenciosas = 0;
107
+ for (const chamada of chamadas) {
108
+ const indiceAbre = chamada.index + chamada[0].length - 1;
109
+ const texto = argumentosDaChamada(src, indiceAbre);
110
+ if (/windowsHide:\s*true/.test(texto) || referenciaConstanteSilenciosa(texto, constantes)) {
111
+ silenciosas += 1;
112
+ } else {
113
+ const marca = `${chamada[1]}(${texto.slice(0, 60).replace(/\s+/g, ' ')}…)`;
114
+ assert(false, `${relativo} — ${marca} sem windowsHide: true (abre janela de console no usuário)`);
115
+ }
116
+ }
117
+ if (chamadas.length === 0) {
118
+ assert(true, `${relativo} — nenhuma chamada de processo direta (sem risco de janela de console)`);
119
+ } else {
120
+ assert(
121
+ silenciosas === chamadas.length,
122
+ `${relativo} — ${silenciosas}/${chamadas.length} chamadas de processo com windowsHide: true`,
123
+ );
124
+ }
125
+ }
126
+
127
+ // ------------------------------------------- isolamento do backend (start.js)
128
+
129
+ const startSrc = readFileSync(join(root, 'scripts', 'start.js'), 'utf8');
130
+
131
+ for (const [regex, descricao] of [
18
132
  [/detached:\s*true/, 'detached: true — grupo de processos próprio (CREATE_NEW_PROCESS_GROUP no Windows)'],
19
133
  [/windowsHide:\s*true/, 'windowsHide: true — sem janela de console fantasma'],
20
134
  [/stdio:\s*\['ignore',\s*logFd,\s*logFd\]/, "stdio: ['ignore', logFd, logFd] — saída do backend no arquivo (fd)"],
@@ -24,32 +138,81 @@ const verificacoes = [
24
138
  [/\.unref\(\)/, 'unref() — launcher não acopla o ciclo de vida ao backend'],
25
139
  [/taskkill/, 'encerramento no Windows via taskkill /PID /T (árvore de processos)'],
26
140
  [/process\.kill/, 'encerramento no Unix via signal no grupo de processos'],
141
+ ]) {
142
+ assert(regex.test(startSrc), descricao);
143
+ }
144
+
145
+ // Uvicorn em produção JAMAIS com --reload/--workers: cada worker é um processo
146
+ // filho extra, que no Windows pode alocar console próprio e ignorar as flags
147
+ // do processo pai.
148
+ const uvicornExtras = startSrc.match(/['"]--(?:reload|workers)['"]/g);
149
+ assert(
150
+ !uvicornExtras,
151
+ `uvicorn sem --reload/--workers (nenhum processo filho extra no Windows)${
152
+ uvicornExtras ? ` — encontrado: ${uvicornExtras.join(', ')}` : ''
153
+ }`,
154
+ );
155
+
156
+ // ------------------------------------------------ varredura Python (backend)
157
+
158
+ const PROC_HELPER = join(root, 'backend', 'core', 'proc.py');
159
+ assert(existsSync(PROC_HELPER), 'backend/core/proc.py — helper silencioso existe (ponto único de subprocesso)');
160
+ if (existsSync(PROC_HELPER)) {
161
+ const procSrc = readFileSync(PROC_HELPER, 'utf8');
162
+ assert(/CREATE_NO_WINDOW\s*=\s*0x08000000/.test(procSrc), 'CREATE_NO_WINDOW = 0x08000000 definido no helper');
163
+ assert(/def run_silent_process/.test(procSrc), 'run_silent_process disponível no helper');
164
+ assert(/creationflags/.test(procSrc), 'helper aplica creationflags no Windows');
165
+ }
166
+
167
+ // Lista recursiva dos .py do backend (sem __pycache__).
168
+ function listarPy(diretorio, acumulador = []) {
169
+ for (const entrada of readdirSync(diretorio, { withFileTypes: true })) {
170
+ if (entrada.name === '__pycache__') continue;
171
+ const caminho = join(diretorio, entrada.name);
172
+ if (entrada.isDirectory()) listarPy(caminho, acumulador);
173
+ else if (entrada.name.endsWith('.py')) acumulador.push(caminho);
174
+ }
175
+ return acumulador;
176
+ }
177
+
178
+ const PADROES_PROIBIDOS = [
179
+ [/subprocess\.run\(/, 'subprocess.run('],
180
+ [/subprocess\.Popen\(/, 'subprocess.Popen('],
181
+ [/subprocess\.call\(/, 'subprocess.call('],
182
+ [/subprocess\.check_output\(/, 'subprocess.check_output('],
183
+ [/subprocess\.check_call\(/, 'subprocess.check_call('],
184
+ [/os\.system\(/, 'os.system('],
185
+ [/os\.popen\(/, 'os.popen('],
27
186
  ];
28
187
 
29
- let falhas = 0;
30
- for (const [regex, descricao] of verificacoes) {
31
- const ok = regex.test(src);
32
- console.log(`${ok ? 'PASS' : 'FAIL'} — ${descricao}`);
33
- if (!ok) falhas += 1;
34
- }
35
-
36
- // Regra permanente (AGENTS.md): NENHUMA spawn do launcher pode abrir janela de
37
- // console — toda chamada spawn/spawnSync precisa de windowsHide E detached.
38
- const chamadasSpawn = [...src.matchAll(/\bspawn(?:Sync)?\(/g)];
39
- for (const chamada of chamadasSpawn) {
40
- const trecho = src.slice(chamada.index, chamada.index + 700);
41
- const temJanelaOculta = /windowsHide:\s*true/.test(trecho);
42
- const temGrupoIsolado = /detached:\s*true/.test(trecho);
43
- if (temJanelaOculta && temGrupoIsolado) {
44
- console.log('PASS — spawn com windowsHide + detached (sem janela de console no usuário)');
45
- } else {
46
- console.log(`FAIL — spawn sem windowsHide/detached: ${trecho.slice(0, 80).replace(/\n/g, ' ')}…`);
47
- falhas += 1;
188
+ const backendDir = join(root, 'backend');
189
+ let arquivosPy = 0;
190
+ let diretasProibidas = 0;
191
+ if (existsSync(backendDir)) {
192
+ for (const arquivo of listarPy(backendDir)) {
193
+ if (arquivo.endsWith(join('core', 'proc.py'))) continue; // helper autorizado
194
+ arquivosPy += 1;
195
+ const conteudo = readFileSync(arquivo, 'utf8');
196
+ const relativo = arquivo.slice(root.length + 1);
197
+ for (const [regex, rotulo] of PADROES_PROIBIDOS) {
198
+ if (regex.test(conteudo)) {
199
+ diretasProibidas += 1;
200
+ assert(false, `${relativo} — uso direto de ${rotulo} sem CREATE_NO_WINDOW (use backend.core.proc)`);
201
+ }
202
+ }
48
203
  }
49
204
  }
205
+ assert(arquivosPy > 0, `backend/ — ${arquivosPy} arquivos .py varridos`);
206
+ assert(
207
+ diretasProibidas === 0,
208
+ 'nenhuma chamada direta a subprocess/os.system no backend (todas via helper silencioso)',
209
+ );
210
+
211
+ // -------------------------------------------------------------- veredicto
50
212
 
51
- if (falhas > 0) {
52
- console.error(`Isolamento de processos: ${falhas} verificação(ões) falharam.`);
53
- process.exit(1);
213
+ if (falhas.length === 0) {
214
+ console.log('Isolamento de processos: todas as verificações passaram.');
215
+ process.exit(0);
54
216
  }
55
- console.log('Isolamento de processos: todas as verificações passaram.');
217
+ console.error(`Isolamento de processos: ${falhas.length} verificação(ões) falharam.`);
218
+ process.exit(1);