@fontdata/ai-harness 2.7.0 → 3.1.0

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.
@@ -16,13 +16,53 @@ export const TIPOS_CONEXAO_PROJETO = ['postgres', 'mariadb', 'sqlite'];
16
16
  function isObject(value) {
17
17
  return typeof value === 'object' && value !== null && !Array.isArray(value);
18
18
  }
19
+ /** Seção do banco no config; até a 2.7.0 ela se chamava `banco`. */
20
+ export const BANCO_CHAVE = 'banco_dados';
21
+ export const BANCO_CHAVE_LEGADA = 'banco';
22
+ /** Só a seção com o nome antigo existe: ela vale e o update a renomeia. */
23
+ function soSecaoLegada(raw) {
24
+ return !(BANCO_CHAVE in raw) && BANCO_CHAVE_LEGADA in raw;
25
+ }
26
+ /** A seção do banco no JSON bruto: `banco_dados`, ou a `banco` antiga quando só ela existe. */
27
+ export function secaoBanco(raw) {
28
+ const secao = soSecaoLegada(raw) ? raw[BANCO_CHAVE_LEGADA] : raw[BANCO_CHAVE];
29
+ return isObject(secao) ? secao : {};
30
+ }
31
+ /** O JSON com a seção do banco gravada como `banco_dados`; a `banco` antiga é trocada na mesma posição. */
32
+ export function comSecaoBanco(raw, secao) {
33
+ if (!soSecaoLegada(raw))
34
+ return { ...raw, [BANCO_CHAVE]: secao };
35
+ return Object.fromEntries(Object.entries(raw).map(([chave, valor]) => (chave === BANCO_CHAVE_LEGADA ? [BANCO_CHAVE, secao] : [chave, valor])));
36
+ }
37
+ function lerJsonBruto(root) {
38
+ try {
39
+ const parsed = JSON.parse(readFileSync(join(root, CONFIG_PATH), 'utf8'));
40
+ return isObject(parsed) ? parsed : undefined;
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ /** O config ainda tem a seção do banco com o nome antigo (`banco`), que o update renomeia. */
47
+ export function temSecaoBancoLegada(root) {
48
+ const raw = lerJsonBruto(root);
49
+ return raw !== undefined && soSecaoLegada(raw);
50
+ }
51
+ /** Renomeia a seção `banco` para `banco_dados`, preservando o resto do JSON; devolve se mudou. */
52
+ export function migrarSecaoBanco(root) {
53
+ const raw = lerJsonBruto(root);
54
+ if (!raw || !soSecaoLegada(raw))
55
+ return false;
56
+ writeFileSync(join(root, CONFIG_PATH), `${JSON.stringify(comSecaoBanco(raw, secaoBanco(raw)), null, 2)}\n`);
57
+ return true;
58
+ }
19
59
  export function defaultConfig() {
20
60
  return {
21
61
  config_versao: CONFIG_VERSION,
22
62
  hooks: { ...DEFAULT_HOOK_MODES },
23
63
  rastro: { versionar: false },
24
64
  bloqueios: { dias_para_aviso: 7 },
25
- banco: {
65
+ banco_dados: {
26
66
  conexoes: [],
27
67
  producao: 'bloqueio',
28
68
  limite_linhas: 200,
@@ -41,23 +81,23 @@ const SO_NO_CADASTRO = 'O mascaramento LGPD só pode ser desligado no cadastro d
41
81
  const REGRA_DE_COLUNA = /^[^.\s]+(\.[^.\s]+){0,2}$/;
42
82
  function lerLgpd(raw, lgpd, problems) {
43
83
  if (!isObject(raw)) {
44
- problems.push(`banco.lgpd deve ser um objeto com mascarar_colunas e liberar_colunas em ${CONFIG_PATH}.`);
84
+ problems.push(`banco_dados.lgpd deve ser um objeto com mascarar_colunas e liberar_colunas em ${CONFIG_PATH}.`);
45
85
  return;
46
86
  }
47
87
  if ('desligar' in raw)
48
- problems.push(`${SO_NO_CADASTRO}; banco.lgpd.desligar em ${CONFIG_PATH} foi ignorado.`);
88
+ problems.push(`${SO_NO_CADASTRO}; banco_dados.lgpd.desligar em ${CONFIG_PATH} foi ignorado.`);
49
89
  for (const chave of ['mascarar_colunas', 'liberar_colunas']) {
50
90
  if (!(chave in raw))
51
91
  continue;
52
92
  const lista = raw[chave];
53
93
  if (!Array.isArray(lista)) {
54
- problems.push(`banco.lgpd.${chave} deve ser uma lista de colunas em ${CONFIG_PATH}.`);
94
+ problems.push(`banco_dados.lgpd.${chave} deve ser uma lista de colunas em ${CONFIG_PATH}.`);
55
95
  continue;
56
96
  }
57
97
  lgpd[chave] = lista.filter((regra) => {
58
98
  if (typeof regra === 'string' && REGRA_DE_COLUNA.test(regra))
59
99
  return true;
60
- problems.push(`Entrada ${JSON.stringify(regra)} ignorada em banco.lgpd.${chave} de ${CONFIG_PATH}; use coluna, tabela.coluna ou schema.tabela.coluna.`);
100
+ problems.push(`Entrada ${JSON.stringify(regra)} ignorada em banco_dados.lgpd.${chave} de ${CONFIG_PATH}; use coluna, tabela.coluna ou schema.tabela.coluna.`);
61
101
  return false;
62
102
  });
63
103
  }
@@ -83,7 +123,7 @@ function motivoConexaoInvalida(entrada) {
83
123
  function lerConexoes(conexoes, banco, problems) {
84
124
  banco.declarado = !(Array.isArray(conexoes) && conexoes.length === 0);
85
125
  if (!Array.isArray(conexoes)) {
86
- problems.push(`banco.conexoes deve ser uma lista de nomes ou de objetos { nome, dsn_env } em ${CONFIG_PATH}.`);
126
+ problems.push(`banco_dados.conexoes deve ser uma lista de nomes ou de objetos { nome, dsn_env } em ${CONFIG_PATH}.`);
87
127
  return;
88
128
  }
89
129
  for (const entrada of conexoes) {
@@ -94,17 +134,17 @@ function lerConexoes(conexoes, banco, problems) {
94
134
  else if (isObject(entrada)) {
95
135
  const motivo = motivoConexaoInvalida(entrada);
96
136
  if (motivo) {
97
- problems.push(`Entrada ${citada} descartada em banco.conexoes de ${CONFIG_PATH}: ${motivo}.`);
137
+ problems.push(`Entrada ${citada} descartada em banco_dados.conexoes de ${CONFIG_PATH}: ${motivo}.`);
98
138
  continue;
99
139
  }
100
140
  nome = entrada.nome;
101
141
  }
102
142
  else {
103
- problems.push(`Entrada ${citada} descartada em banco.conexoes de ${CONFIG_PATH}; use um nome (texto não vazio) ou um objeto { nome, dsn_env }.`);
143
+ problems.push(`Entrada ${citada} descartada em banco_dados.conexoes de ${CONFIG_PATH}; use um nome (texto não vazio) ou um objeto { nome, dsn_env }.`);
104
144
  continue;
105
145
  }
106
146
  if (banco.conexoes.includes(nome)) {
107
- problems.push(`Entrada ${citada} descartada em banco.conexoes de ${CONFIG_PATH}: o nome "${nome}" já foi declarado antes.`);
147
+ problems.push(`Entrada ${citada} descartada em banco_dados.conexoes de ${CONFIG_PATH}: o nome "${nome}" já foi declarado antes.`);
108
148
  continue;
109
149
  }
110
150
  banco.conexoes.push(nome);
@@ -112,8 +152,8 @@ function lerConexoes(conexoes, banco, problems) {
112
152
  continue;
113
153
  for (const chave of Object.keys(entrada).filter((chave) => !CHAVES_CONEXAO_PROJETO.includes(chave))) {
114
154
  problems.push(chave === 'mascaramento'
115
- ? `${SO_NO_CADASTRO}; mascaramento da entrada ${citada} em banco.conexoes de ${CONFIG_PATH} foi ignorado.`
116
- : `Chave desconhecida ${chave} ignorada na entrada ${citada} de banco.conexoes em ${CONFIG_PATH}.`);
155
+ ? `${SO_NO_CADASTRO}; mascaramento da entrada ${citada} em banco_dados.conexoes de ${CONFIG_PATH} foi ignorado.`
156
+ : `Chave desconhecida ${chave} ignorada na entrada ${citada} de banco_dados.conexoes em ${CONFIG_PATH}.`);
117
157
  }
118
158
  banco.projeto[nome] = {
119
159
  nome,
@@ -130,28 +170,28 @@ function lerBanco(raw, banco, problems) {
130
170
  if (MODOS_PRODUCAO.includes(raw.producao))
131
171
  banco.producao = raw.producao;
132
172
  else
133
- problems.push(`Modo "${String(raw.producao)}" inválido para banco.producao em ${CONFIG_PATH}; use aviso ou bloqueio.`);
173
+ problems.push(`Modo "${String(raw.producao)}" inválido para banco_dados.producao em ${CONFIG_PATH}; use aviso ou bloqueio.`);
134
174
  }
135
175
  if ('limite_linhas' in raw) {
136
176
  if (inteiroEntre(raw.limite_linhas, 1, 5000))
137
177
  banco.limite_linhas = raw.limite_linhas;
138
178
  else
139
- problems.push(`banco.limite_linhas deve ser um inteiro de 1 a 5000 em ${CONFIG_PATH}.`);
179
+ problems.push(`banco_dados.limite_linhas deve ser um inteiro de 1 a 5000 em ${CONFIG_PATH}.`);
140
180
  }
141
181
  if ('timeout_segundos' in raw) {
142
182
  if (inteiroEntre(raw.timeout_segundos, 1, 600))
143
183
  banco.timeout_segundos = raw.timeout_segundos;
144
184
  else
145
- problems.push(`banco.timeout_segundos deve ser um inteiro de 1 a 600 em ${CONFIG_PATH}.`);
185
+ problems.push(`banco_dados.timeout_segundos deve ser um inteiro de 1 a 600 em ${CONFIG_PATH}.`);
146
186
  }
147
187
  if ('registrar_mcp' in raw) {
148
188
  if (typeof raw.registrar_mcp === 'boolean')
149
189
  banco.registrar_mcp = raw.registrar_mcp;
150
190
  else
151
- problems.push(`banco.registrar_mcp deve ser true ou false em ${CONFIG_PATH}.`);
191
+ problems.push(`banco_dados.registrar_mcp deve ser true ou false em ${CONFIG_PATH}.`);
152
192
  }
153
193
  if ('mascaramento' in raw)
154
- problems.push(`${SO_NO_CADASTRO}; banco.mascaramento em ${CONFIG_PATH} foi ignorado.`);
194
+ problems.push(`${SO_NO_CADASTRO}; banco_dados.mascaramento em ${CONFIG_PATH} foi ignorado.`);
155
195
  if ('lgpd' in raw)
156
196
  lerLgpd(raw.lgpd, banco.lgpd, problems);
157
197
  }
@@ -192,17 +232,24 @@ export function readConfig(root) {
192
232
  else
193
233
  problems.push(`bloqueios.dias_para_aviso deve ser um inteiro positivo em ${CONFIG_PATH}.`);
194
234
  }
195
- if (isObject(raw.banco))
196
- lerBanco(raw.banco, config.banco, problems);
235
+ const secao = soSecaoLegada(raw) ? raw[BANCO_CHAVE_LEGADA] : raw[BANCO_CHAVE];
236
+ if (isObject(secao))
237
+ lerBanco(secao, config.banco_dados, problems);
238
+ if (soSecaoLegada(raw)) {
239
+ problems.push(`A seção banco de ${CONFIG_PATH} agora se chama banco_dados; rode npx @fontdata/ai-harness update --yes para renomeá-la.`);
240
+ }
241
+ else if (BANCO_CHAVE_LEGADA in raw) {
242
+ problems.push(`A seção banco de ${CONFIG_PATH} foi ignorada: vale a banco_dados. Apague a banco.`);
243
+ }
197
244
  return { config, problems };
198
245
  }
199
- /** Cria a config com os padrões, sem a seção banco; retorna false se já existia (nunca sobrescreve). */
246
+ /** Cria a config com os padrões, sem a seção banco_dados; retorna false se já existia (nunca sobrescreve). */
200
247
  export function writeDefaultConfig(root) {
201
248
  const file = join(root, CONFIG_PATH);
202
249
  if (existsSync(file))
203
250
  return false;
204
251
  mkdirSync(dirname(file), { recursive: true });
205
- const { banco: _banco, ...gravada } = defaultConfig();
252
+ const { banco_dados: _banco, ...gravada } = defaultConfig();
206
253
  writeFileSync(file, `${JSON.stringify(gravada, null, 2)}\n`);
207
254
  return true;
208
255
  }
@@ -222,10 +269,10 @@ function lerJsonParaEscrita(file, chave) {
222
269
  export function validarConfigParaEscrita(root) {
223
270
  const file = join(root, CONFIG_PATH);
224
271
  if (existsSync(file))
225
- lerJsonParaEscrita(file, 'banco.conexoes');
272
+ lerJsonParaEscrita(file, 'banco_dados.conexoes');
226
273
  }
227
274
  /**
228
- * Grava a conexão em `banco.conexoes`, preservando o resto do JSON; sem config, cria uma com os padrões.
275
+ * Grava a conexão em `banco_dados.conexoes`, preservando o resto do JSON; sem config, cria uma com os padrões.
229
276
  * Se o nome já existir (objeto ou string), lança, ou troca a entrada na mesma posição com `substituir`.
230
277
  * JSON inválido é recusado sem tocar no arquivo.
231
278
  */
@@ -234,14 +281,14 @@ export function gravarConexaoProjeto(root, conexao, { substituir = false } = {})
234
281
  if (motivo)
235
282
  throw new Error(`Conexão ${JSON.stringify(conexao.nome)} inválida: ${motivo}.`);
236
283
  const file = join(root, CONFIG_PATH);
237
- const raw = existsSync(file) ? lerJsonParaEscrita(file, 'banco.conexoes') : undefined;
238
- const banco = raw && isObject(raw.banco) ? raw.banco : {};
284
+ const raw = existsSync(file) ? lerJsonParaEscrita(file, 'banco_dados.conexoes') : undefined;
285
+ const banco = raw ? secaoBanco(raw) : {};
239
286
  const conexoes = banco.conexoes === undefined ? [] : banco.conexoes;
240
287
  if (!Array.isArray(conexoes))
241
- throw new Error(`banco.conexoes em ${CONFIG_PATH} não é uma lista; corrija-o antes de gravar a conexão ${conexao.nome}.`);
288
+ throw new Error(`banco_dados.conexoes em ${CONFIG_PATH} não é uma lista; corrija-o antes de gravar a conexão ${conexao.nome}.`);
242
289
  const posicao = conexoes.findIndex((entrada) => (isObject(entrada) ? entrada.nome : entrada) === conexao.nome);
243
290
  if (posicao !== -1 && !substituir)
244
- throw new Error(`A conexão ${conexao.nome} já está em banco.conexoes de ${CONFIG_PATH}.`);
291
+ throw new Error(`A conexão ${conexao.nome} já está em banco_dados.conexoes de ${CONFIG_PATH}.`);
245
292
  const entrada = {
246
293
  nome: conexao.nome,
247
294
  dsn_env: conexao.dsn_env,
@@ -251,28 +298,52 @@ export function gravarConexaoProjeto(root, conexao, { substituir = false } = {})
251
298
  const proximas = posicao === -1 ? [...conexoes, entrada] : conexoes.map((atual, i) => (i === posicao ? entrada : atual));
252
299
  if (!raw)
253
300
  writeDefaultConfig(root);
254
- const base = raw ?? lerJsonParaEscrita(file, 'banco.conexoes');
255
- writeFileSync(file, `${JSON.stringify({ ...base, banco: { ...banco, conexoes: proximas } }, null, 2)}\n`);
301
+ const base = raw ?? lerJsonParaEscrita(file, 'banco_dados.conexoes');
302
+ writeFileSync(file, `${JSON.stringify(comSecaoBanco(base, { ...banco, conexoes: proximas }), null, 2)}\n`);
303
+ }
304
+ /**
305
+ * Tira de `banco_dados.conexoes` a primeira entrada com o nome (objeto, mesmo inválido, ou texto), preservando
306
+ * o resto do JSON, e devolve o que leu. Sem config ou sem o nome, devolve null e não escreve.
307
+ * JSON inválido ou `conexoes` que não é lista lança sem tocar no arquivo.
308
+ */
309
+ export function removerConexaoProjeto(root, nome) {
310
+ const file = join(root, CONFIG_PATH);
311
+ if (!existsSync(file))
312
+ return null;
313
+ const raw = lerJsonParaEscrita(file, 'banco_dados.conexoes');
314
+ const banco = secaoBanco(raw);
315
+ const conexoes = banco.conexoes === undefined ? [] : banco.conexoes;
316
+ if (!Array.isArray(conexoes))
317
+ throw new Error(`banco_dados.conexoes em ${CONFIG_PATH} não é uma lista; corrija-o antes de remover a conexão ${nome}.`);
318
+ const posicao = conexoes.findIndex((entrada) => (isObject(entrada) ? entrada.nome : entrada) === nome);
319
+ if (posicao === -1)
320
+ return null;
321
+ const entrada = conexoes[posicao];
322
+ const proximas = conexoes.filter((_atual, i) => i !== posicao);
323
+ writeFileSync(file, `${JSON.stringify(comSecaoBanco(raw, { ...banco, conexoes: proximas }), null, 2)}\n`);
324
+ if (!isObject(entrada))
325
+ return nome;
326
+ return typeof entrada.dsn_env === 'string' ? { nome, dsn_env: entrada.dsn_env } : { nome };
256
327
  }
257
328
  /**
258
- * Grava só `banco.registrar_mcp`, preservando o resto do JSON; sem config, cria uma com os padrões
329
+ * Grava só `banco_dados.registrar_mcp`, preservando o resto do JSON; sem config, cria uma com os padrões
259
330
  * e essa chave. JSON inválido é recusado sem tocar no arquivo.
260
331
  */
261
332
  export function gravarRegistrarMcp(root, valor) {
262
333
  const file = join(root, CONFIG_PATH);
263
334
  let raw;
264
335
  if (existsSync(file)) {
265
- raw = lerJsonParaEscrita(file, 'banco.registrar_mcp');
336
+ raw = lerJsonParaEscrita(file, 'banco_dados.registrar_mcp');
266
337
  }
267
338
  else {
268
- const { banco: _banco, ...padrao } = defaultConfig();
339
+ const { banco_dados: _banco, ...padrao } = defaultConfig();
269
340
  raw = padrao;
270
341
  }
271
- const banco = isObject(raw.banco) ? raw.banco : {};
342
+ const banco = secaoBanco(raw);
272
343
  if (banco.registrar_mcp === valor)
273
344
  return;
274
345
  mkdirSync(dirname(file), { recursive: true });
275
- writeFileSync(file, `${JSON.stringify({ ...raw, banco: { ...banco, registrar_mcp: valor } }, null, 2)}\n`);
346
+ writeFileSync(file, `${JSON.stringify(comSecaoBanco(raw, { ...banco, registrar_mcp: valor }), null, 2)}\n`);
276
347
  }
277
348
  const BLOCK_START = '# ai-harness (gerenciado pelo harness; não edite entre estas linhas)';
278
349
  const BLOCK_END = '# /ai-harness';
@@ -1,8 +1,8 @@
1
1
  import { execFileSync, spawnSync } from 'node:child_process';
2
2
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { lerEnvSettingsLocal, lerVariavel, SETTINGS_LOCAL_PATH } from '../banco/ambiente.js';
5
- import { caminhoCadastro, lerCadastro } from '../banco/cadastro.js';
4
+ import { lerEnvSettingsLocal, lerVariavel, SETTINGS_LOCAL_PATH } from '../banco-dados/ambiente.js';
5
+ import { caminhoCadastro, lerCadastro } from '../banco-dados/cadastro.js';
6
6
  import { allCommands, loadCatalog } from '../catalog.js';
7
7
  import { CONFIG_PATH, readConfig } from '../config/config.js';
8
8
  import { readArtifact, validateAllWorks, WORKS_DIR } from '../contratos/artefatos.js';
@@ -414,7 +414,7 @@ function settingsLocalIgnorado(root, env) {
414
414
  .some((linha) => LINHAS_IGNORE_SETTINGS_LOCAL.has(linha));
415
415
  }
416
416
  function checkBancoProjeto(root, config, env) {
417
- const conexoes = config.banco.conexoes.flatMap((nome) => config.banco.projeto[nome] ?? []);
417
+ const conexoes = config.banco_dados.conexoes.flatMap((nome) => config.banco_dados.projeto[nome] ?? []);
418
418
  if (!conexoes.length)
419
419
  return [];
420
420
  const findings = [];
@@ -424,8 +424,8 @@ function checkBancoProjeto(root, config, env) {
424
424
  findings.push({
425
425
  id: 'banco-variavel-ausente',
426
426
  severidade: 'aviso',
427
- mensagem: `A conexão ${nome}, declarada em banco.conexoes de ${join(root, CONFIG_PATH)}, lê o DSN da variável ${dsn_env}, que não está definida no ambiente, em ${SETTINGS_LOCAL_PATH} nem no .env.`,
428
- correcao: `Defina ${dsn_env} com \`npx @fontdata/ai-harness banco adicionar ${nome} --projeto\` ou no ambiente do terminal.`,
427
+ mensagem: `A conexão ${nome}, declarada em banco_dados.conexoes de ${join(root, CONFIG_PATH)}, lê o DSN da variável ${dsn_env}, que não está definida no ambiente, em ${SETTINGS_LOCAL_PATH} nem no .env.`,
428
+ correcao: `Defina ${dsn_env} com \`npx @fontdata/ai-harness banco-dados adicionar ${nome}\` ou no ambiente do terminal.`,
429
429
  });
430
430
  }
431
431
  const envLocal = lerEnvSettingsLocal(root);
@@ -453,12 +453,12 @@ function checkBancoProjeto(root, config, env) {
453
453
  }
454
454
  return findings;
455
455
  }
456
- /** Confere o cadastro desta máquina e as variáveis das conexões do projeto contra `banco.conexoes`, sem abrir conexão. */
456
+ /** Confere o cadastro desta máquina e as variáveis das conexões do projeto contra `banco_dados.conexoes`, sem abrir conexão. */
457
457
  export function checkBanco(root, config, env = process.env) {
458
- if (!config.banco.conexoes.length)
458
+ if (!config.banco_dados.conexoes.length)
459
459
  return [];
460
460
  const findings = checkBancoProjeto(root, config, env);
461
- const doCadastro = config.banco.conexoes.filter((nome) => !(nome in config.banco.projeto));
461
+ const doCadastro = config.banco_dados.conexoes.filter((nome) => !(nome in config.banco_dados.projeto));
462
462
  if (!doCadastro.length)
463
463
  return findings;
464
464
  const caminho = caminhoCadastro(env);
@@ -486,8 +486,8 @@ export function checkBanco(root, config, env = process.env) {
486
486
  findings.push({
487
487
  id: 'banco-conexao-ausente',
488
488
  severidade: 'aviso',
489
- mensagem: `A conexão ${nome}, citada em banco.conexoes de ${join(root, CONFIG_PATH)}, não está no cadastro desta máquina (${caminho}).`,
490
- correcao: `Cadastre-a com \`npx @fontdata/ai-harness banco adicionar ${nome}\`.`,
489
+ mensagem: `A conexão ${nome}, citada em banco_dados.conexoes de ${join(root, CONFIG_PATH)}, não está no cadastro desta máquina (${caminho}).`,
490
+ correcao: `Cadastre-a com \`npx @fontdata/ai-harness banco-dados adicionar ${nome} --global\`.`,
491
491
  });
492
492
  }
493
493
  for (const nome of doCadastro.filter((nome) => conexoes[nome]?.mascaramento === false)) {
package/dist/init.js CHANGED
@@ -14,7 +14,7 @@ const PLUGIN_HOOKS_NOTE = 'O plugin fontdata@ai-harness está habilitado: os hoo
14
14
  const PLUGIN_MCP_NOTE = 'O plugin fontdata@ai-harness está habilitado: o servidor MCP de banco já roda por ele e não foi registrado no .mcp.json, para não duplicar.';
15
15
  /** Registra o MCP de banco no .mcp.json quando não foi recusado (flag, wizard ou config) e o plugin não o fornece. */
16
16
  export function decidirRegistroMcp(options, root, home = homedir()) {
17
- return options.mcpBanco && readConfig(root).config.banco.registrar_mcp && !isHarnessPluginEnabled(root, home);
17
+ return options.mcpBanco && readConfig(root).config.banco_dados.registrar_mcp && !isHarnessPluginEnabled(root, home);
18
18
  }
19
19
  function applyMcpDecision(plan, options, root) {
20
20
  plan.registerMcp = decidirRegistroMcp(options, root);
@@ -7,7 +7,7 @@ const CLAUDE_MD = {
7
7
  };
8
8
  const CONFLICTS = { sobrescrever: 'overwrite', pular: 'skip' };
9
9
  const VALUE_FLAGS = ['--escopo', '--comandos', '--claude-md', '--conflito'];
10
- const BOOLEAN_FLAGS = ['--sem-harness', '--sem-hooks', '--sem-agents', '--sem-comentarios', '--sem-banco', '--yes', '-y', '--sim'];
10
+ const BOOLEAN_FLAGS = ['--sem-harness', '--sem-hooks', '--sem-agents', '--sem-comentarios', '--sem-banco-dados', '--yes', '-y', '--sim'];
11
11
  function pick(flag, value, options) {
12
12
  const chosen = options[value];
13
13
  if (chosen === undefined) {
@@ -28,8 +28,13 @@ function distance(a, b) {
28
28
  }
29
29
  return row[b.length] ?? 0;
30
30
  }
31
+ /** Flags que mudaram de nome: o erro aponta a nova, que a distância de edição não alcança. */
32
+ const RENAMED_FLAGS = { '--sem-banco': '--sem-banco-dados' };
31
33
  /** Flag conhecida mais parecida com a digitada, para sugerir em mensagens de erro. */
32
34
  export function suggestFlag(token, flags) {
35
+ const renamed = RENAMED_FLAGS[token];
36
+ if (renamed && flags.includes(renamed))
37
+ return renamed;
33
38
  const [best] = flags
34
39
  .map((flag) => ({ flag, d: distance(token, flag) }))
35
40
  .filter(({ d }) => d <= 2)
@@ -69,7 +74,7 @@ export function parseInitArgs(argv) {
69
74
  args.agents = false;
70
75
  else if (token === '--sem-comentarios')
71
76
  args.comments = false;
72
- else if (token === '--sem-banco')
77
+ else if (token === '--sem-banco-dados')
73
78
  args.mcpBanco = false;
74
79
  else
75
80
  args.yes = true;
@@ -2,13 +2,17 @@ import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { timestamp } from './settings.js';
4
4
  export const MCP_PATH = '.mcp.json';
5
+ /** Nome do servidor de banco no `.mcp.json`; as ferramentas ficam `mcp__banco_dados__*`. */
6
+ export const BANCO_MCP_NOME = 'banco_dados';
7
+ /** Nome do servidor até a 2.7.0; a entrada gerenciada com ele é trocada pela nova no update. */
8
+ export const BANCO_MCP_NOME_LEGADO = 'banco';
5
9
  const PIN_PREFIX = '@fontdata/ai-harness@';
6
10
  function isPlainObject(value) {
7
11
  return typeof value === 'object' && value !== null && !Array.isArray(value);
8
12
  }
9
13
  /** Servidor MCP de banco com a versão do pacote fixada no npx, no mesmo formato do plugin. */
10
14
  export function bancoMcpServer(versao) {
11
- return { command: 'npx', args: ['-y', `${PIN_PREFIX}${versao}`, 'banco-mcp'] };
15
+ return { command: 'npx', args: ['-y', `${PIN_PREFIX}${versao}`, 'banco-dados-mcp'] };
12
16
  }
13
17
  function pinOf(entrada) {
14
18
  if (!isPlainObject(entrada) || !Array.isArray(entrada.args))
@@ -22,9 +26,10 @@ export function ehEntradaGerenciada(entrada) {
22
26
  /**
23
27
  * Acrescenta os servidores ausentes em `mcpServers` do `.mcp.json`, preservando o resto. Com
24
28
  * `atualizarGerenciada`, regrava também a entrada gerenciada que fixa outra versão; entrada
25
- * personalizada nunca é tocada. Backup datado só quando muda; JSON inválido é recusado sem tocar.
29
+ * personalizada nunca é tocada. Os nomes de `removerGerenciadas` saem só quando a entrada é gerenciada. Backup
30
+ * datado só quando muda; JSON inválido é recusado sem tocar.
26
31
  */
27
- export function mergeMcpServers(root, servidores, now = new Date(), { atualizarGerenciada = false } = {}) {
32
+ export function mergeMcpServers(root, servidores, now = new Date(), { atualizarGerenciada = false, removerGerenciadas = [] } = {}) {
28
33
  const file = join(root, MCP_PATH);
29
34
  const original = existsSync(file) ? readFileSync(file, 'utf8') : null;
30
35
  let config = {};
@@ -43,6 +48,12 @@ export function mergeMcpServers(root, servidores, now = new Date(), { atualizarG
43
48
  }
44
49
  const mcpServers = { ...(config.mcpServers ?? {}) };
45
50
  let changed = false;
51
+ for (const nome of removerGerenciadas) {
52
+ if (!ehEntradaGerenciada(mcpServers[nome]))
53
+ continue;
54
+ delete mcpServers[nome];
55
+ changed = true;
56
+ }
46
57
  for (const [nome, servidor] of Object.entries(servidores)) {
47
58
  const atual = mcpServers[nome];
48
59
  if (atual === undefined) {
@@ -6,10 +6,10 @@ import { applyAtomically } from './atomic.js';
6
6
  import { buildLockItem, LOCK_VERSION, readLock, writeLock } from './lock.js';
7
7
  import { HOOKS_ITEM_ID, PROJECT_HOOKS_DIR, projectHooksConfig } from './hooks.js';
8
8
  import { neededSkillsFor, resolveSelection } from './selection.js';
9
- import { bancoMcpServer, MCP_PATH, mergeMcpServers } from './mcp.js';
9
+ import { BANCO_MCP_NOME, bancoMcpServer, MCP_PATH, mergeMcpServers } from './mcp.js';
10
10
  import { gravarRegistrarMcp } from '../config/config.js';
11
11
  import { COMMENTS_RULE_PATH, commentsHooksConfig, ensureCommentsBaseline } from '../comentarios/instalar.js';
12
- import { combineHooks, HARNESS_DENY, HARNESS_PERMISSIONS, mergeSettings } from './settings.js';
12
+ import { combineHooks, HARNESS_DENY, harnessPermissions, mergeSettings } from './settings.js';
13
13
  /** Rule de convenção com o que é específico do projeto (D-38): é do projeto, então fica fora do lock. */
14
14
  export const PROJECT_RULE_PATH = '.claude/rules/harness-projeto.md';
15
15
  /** Esqueleto da rule de projeto; os comandos `-legado` renomeados pelo update entram na última seção. */
@@ -165,12 +165,12 @@ export function executePlan(root, plan, pkg, now = new Date()) {
165
165
  const permission = needsEventPermission(plan);
166
166
  const hooks = combineHooks(plan.registerHooks ? projectHooksConfig() : {}, plan.registerComments ? commentsHooksConfig() : {});
167
167
  if (Object.keys(hooks).length || permission) {
168
- mergeSettings(root, { hooks, allow: permission ? HARNESS_PERMISSIONS : [], deny: permission ? HARNESS_DENY : [] }, now);
168
+ mergeSettings(root, { hooks, allow: permission ? harnessPermissions({ banco: bancoAtivo(plan) }) : [], deny: permission ? HARNESS_DENY : [] }, now);
169
169
  }
170
170
  if (plan.registerComments)
171
171
  ensureCommentsBaseline(root);
172
172
  if (plan.registerMcp)
173
- mergeMcpServers(root, { banco: bancoMcpServer(pkg.versao) }, now);
173
+ mergeMcpServers(root, { [BANCO_MCP_NOME]: bancoMcpServer(pkg.versao) }, now);
174
174
  if (plan.mcpRecusado)
175
175
  gravarRegistrarMcp(root, false);
176
176
  const items = new Map();
@@ -193,6 +193,10 @@ export function executePlan(root, plan, pkg, now = new Date()) {
193
193
  writeLock(root, lock);
194
194
  return lock;
195
195
  }
196
+ /** O banco não foi recusado (`--sem-banco-dados` ou `banco_dados.registrar_mcp: false`): as permissões do MCP entram. */
197
+ function bancoAtivo(plan) {
198
+ return !plan.mcpRecusado && plan.mcpSkipReason !== 'config';
199
+ }
196
200
  /** Skills e agents do harness registram o rastro com `npx @fontdata/ai-harness evento`. */
197
201
  function needsEventPermission(plan) {
198
202
  return plan.items.some((item) => item.tipo === 'skill' || item.tipo === 'agent');
@@ -209,7 +213,7 @@ export function describePlan(plan) {
209
213
  if (plan.registerHooks)
210
214
  lines.push('Hooks do harness registrados em .claude/settings.json (sem duplicar os existentes).');
211
215
  if (needsEventPermission(plan)) {
212
- lines.push(`Permissões ${HARNESS_PERMISSIONS.join(', ')} em .claude/settings.json, para o rastro, a checagem de conflitos, ` +
216
+ lines.push(`Permissões ${harnessPermissions({ banco: bancoAtivo(plan) }).join(', ')} em .claude/settings.json, para o rastro, a checagem de conflitos, ` +
213
217
  'o /git-commit-push, os sub-agents e as consultas de leitura não pedirem aprovação (inclusive no modo auto); ' +
214
218
  'o push forçado fica bloqueado em permissions.deny.');
215
219
  }
@@ -217,13 +221,13 @@ export function describePlan(plan) {
217
221
  lines.push(`Servidor MCP de banco (somente leitura) registrado em ${MCP_PATH}, sem duplicar os servidores existentes.`);
218
222
  }
219
223
  else if (plan.mcpRecusado) {
220
- lines.push(`Servidor MCP de banco não registrado (recusado); a escolha fica em banco.registrar_mcp: false no .ai-harness/config.json.`);
224
+ lines.push(`Servidor MCP de banco não registrado (recusado); a escolha fica em banco_dados.registrar_mcp: false no .ai-harness/config.json.`);
221
225
  }
222
226
  else if (plan.mcpSkipReason === 'plugin') {
223
227
  lines.push(`Servidor MCP de banco não registrado em ${MCP_PATH}: o plugin fontdata@ai-harness já o fornece.`);
224
228
  }
225
229
  else if (plan.mcpSkipReason === 'config') {
226
- lines.push(`Servidor MCP de banco não registrado em ${MCP_PATH}: banco.registrar_mcp está false no .ai-harness/config.json.`);
230
+ lines.push(`Servidor MCP de banco não registrado em ${MCP_PATH}: banco_dados.registrar_mcp está false no .ai-harness/config.json.`);
227
231
  }
228
232
  if (plan.registerComments) {
229
233
  lines.push('Padrão de comentários: rule carregada em toda sessão, hook que bloqueia a violação e catraca do legado (gerada só na primeira instalação).');
@@ -33,11 +33,14 @@ export const READ_PERMISSIONS = [
33
33
  'Bash(gh pr diff:*)',
34
34
  'Bash(npm outdated:*)',
35
35
  ];
36
+ const BANCO_TOOLS = ['listar_conexoes', 'listar_tabelas', 'descrever_tabela', 'consultar'];
36
37
  /**
37
- * Ferramentas de esquema do servidor MCP de banco, registrado pelo `.mcp.json` ou pelo plugin. A `consultar`
38
- * fica de fora: lê linhas das tabelas e continua pedindo aprovação.
38
+ * Ferramentas do servidor MCP de banco, registrado pelo `.mcp.json` (`banco_dados`) ou pelo plugin
39
+ * (`banco_dados`). A `consultar` também é liberada: os dados pessoais chegam mascarados.
39
40
  */
40
- export const BANCO_PERMISSIONS = ['mcp__banco__', 'mcp__plugin_fontdata_banco__'].flatMap((prefixo) => ['listar_conexoes', 'listar_tabelas', 'descrever_tabela', 'consultar'].map((ferramenta) => `${prefixo}${ferramenta}`));
41
+ export const BANCO_PERMISSIONS = ['mcp__banco_dados__', 'mcp__plugin_fontdata_banco_dados__'].flatMap((prefixo) => BANCO_TOOLS.map((ferramenta) => `${prefixo}${ferramenta}`));
42
+ /** As mesmas regras com o nome antigo do servidor (`banco`, até a 2.7.0); o update as remove. */
43
+ export const BANCO_PERMISSIONS_LEGADAS = ['mcp__banco__', 'mcp__plugin_fontdata_banco__'].flatMap((prefixo) => BANCO_TOOLS.map((ferramenta) => `${prefixo}${ferramenta}`));
41
44
  /** Regras de `permissions.allow` que o init e o update gravam para as skills e os agents do harness. */
42
45
  export const HARNESS_PERMISSIONS = [
43
46
  EVENT_PERMISSION,
@@ -48,6 +51,10 @@ export const HARNESS_PERMISSIONS = [
48
51
  ...READ_PERMISSIONS,
49
52
  ...BANCO_PERMISSIONS,
50
53
  ];
54
+ /** As regras do harness; sem o banco (recusado com `--sem-banco-dados` ou `banco_dados.registrar_mcp: false`), sem as do MCP. */
55
+ export function harnessPermissions({ banco }) {
56
+ return banco ? HARNESS_PERMISSIONS : HARNESS_PERMISSIONS.filter((rule) => !BANCO_PERMISSIONS.includes(rule));
57
+ }
51
58
  /** O push liberado não vale para reescrever o histórico remoto: `deny` vence o `allow`. */
52
59
  export const HARNESS_DENY = [
53
60
  'Bash(git push --force:*)',
@@ -92,7 +99,7 @@ export function mergeHooksIntoSettings(root, hooks, now = new Date()) {
92
99
  * Acrescenta hooks e regras de `permissions.allow` ao `.claude/settings.json` do projeto
93
100
  * sem perder o que já existe. Um hook é considerado presente quando algum grupo do mesmo
94
101
  * evento já tem o mesmo `command`, e uma regra quando já está na lista, o que torna a
95
- * operação idempotente. Tudo é gravado de uma vez, com um único backup datado do arquivo
102
+ * operação idempotente. As regras de `removeAllow` saem de `permissions.allow`. Tudo é gravado de uma vez, com um único backup datado do arquivo
96
103
  * original; JSON inválido é recusado sem tocar em nada.
97
104
  */
98
105
  export function mergeSettings(root, additions, now = new Date()) {
@@ -155,6 +162,12 @@ export function mergeSettings(root, additions, now = new Date()) {
155
162
  }
156
163
  next.permissions = { ...permissions, [key]: list };
157
164
  }
165
+ const remove = additions.removeAllow ?? [];
166
+ const currentAllow = next.permissions?.allow;
167
+ if (remove.length && currentAllow?.some((rule) => remove.includes(rule))) {
168
+ next.permissions = { ...next.permissions, allow: currentAllow.filter((rule) => !remove.includes(rule)) };
169
+ changed = true;
170
+ }
158
171
  if (!changed)
159
172
  return { changed: false, backup: null };
160
173
  let backup = null;