@fontdata/ai-harness 3.1.0 → 3.1.2
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/.claude-plugin/plugin.json +2 -2
- package/dist/banco-dados/ferramentas.js +12 -3
- package/dist/banco-dados/lgpd/aplicar.js +168 -30
- package/dist/banco-dados/lgpd/detectar.js +17 -4
- package/dist/banco-dados/lgpd/mascarar.js +4 -4
- package/dist/cli.js +2 -1
- package/dist/comentarios/command.js +2 -1
- package/dist/memoria/indice.js +2 -1
- package/dist/raiz.js +21 -0
- package/dist/rastro/command.js +2 -1
- package/dist/trabalhos/conflitos.js +2 -1
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fontdata",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"mcpServers": {
|
|
5
|
-
"banco_dados": { "command": "npx", "args": ["-y", "@fontdata/ai-harness@3.1.
|
|
5
|
+
"banco_dados": { "command": "npx", "args": ["-y", "@fontdata/ai-harness@3.1.2", "banco-dados-mcp"] }
|
|
6
6
|
},
|
|
7
7
|
"description": "Harness de engenharia com IA da Fontdata para Claude Code",
|
|
8
8
|
"author": { "name": "Fontdata" },
|
|
@@ -83,14 +83,23 @@ function paraJson(dados) {
|
|
|
83
83
|
function plural(n, singular, plural) {
|
|
84
84
|
return `${n} ${n === 1 ? singular : plural}`;
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
const MOTIVOS = {
|
|
87
|
+
nome: 'pelo nome',
|
|
88
|
+
projeto: 'pela regra do projeto',
|
|
89
|
+
sql: 'porque o SQL cita',
|
|
90
|
+
};
|
|
91
|
+
/** Diz, para cada coluna, a categoria e o motivo, para o usuário achar um falso positivo e liberá-lo no projeto. */
|
|
92
|
+
function rodapeLgpd({ colunasMascaradas, valoresEmTexto, detectados }) {
|
|
87
93
|
const partes = [];
|
|
88
94
|
if (colunasMascaradas.length > 0) {
|
|
89
|
-
const lista = colunasMascaradas
|
|
95
|
+
const lista = colunasMascaradas
|
|
96
|
+
.map((c) => `${c.coluna} → ${c.categoria} ${MOTIVOS[c.motivo]}${c.citada ? ` ${c.citada}` : ''}`)
|
|
97
|
+
.join(', ');
|
|
90
98
|
partes.push(`${plural(colunasMascaradas.length, 'coluna', 'colunas')} (${lista})`);
|
|
91
99
|
}
|
|
92
100
|
if (valoresEmTexto > 0) {
|
|
93
|
-
|
|
101
|
+
const lista = detectados.map((d) => `${d.coluna} → ${d.categorias.join('/')}`).join(', ');
|
|
102
|
+
partes.push(`${plural(valoresEmTexto, 'valor detectado pelo conteúdo', 'valores detectados pelo conteúdo')} (${lista})`);
|
|
94
103
|
}
|
|
95
104
|
return partes.length === 0 ? '' : `\n\nMascaramento LGPD: ${partes.join(' e ')}.`;
|
|
96
105
|
}
|
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
import { identificadoresDoSql } from '../somente-leitura.js';
|
|
1
|
+
import { identificadoresDoSql, tokenizar } from '../somente-leitura.js';
|
|
2
2
|
import { classificarColuna, classificarNumero, encontrarNoTexto } from './detectar.js';
|
|
3
3
|
import { mascararValor, OCULTO } from './mascarar.js';
|
|
4
|
+
/** Categorias com formato próprio: uma flag (`'S'`, `true`, `0`) numa coluna dessas não carrega o dado pessoal. */
|
|
5
|
+
const CATEGORIAS_DE_FORMATO = new Set(['cpf', 'cnpj', 'documento', 'email', 'telefone', 'cep', 'cartao', 'ip']);
|
|
6
|
+
/** Palavras que encerram a lista de itens do `SELECT` quando aparecem fora de parênteses. */
|
|
7
|
+
const FIM_DOS_ITENS = new Set(['from', 'where', 'group', 'having', 'order', 'limit', 'offset', 'window', 'fetch', 'for']);
|
|
8
|
+
const INTEIRO_EM_TEXTO = /^\d+$/;
|
|
9
|
+
const NUMERO_EM_TEXTO = /^-?\d+(\.\d+)?$/;
|
|
10
|
+
function novoContador() {
|
|
11
|
+
return { valores: 0, categorias: new Set() };
|
|
12
|
+
}
|
|
13
|
+
function contar(contador, categoria) {
|
|
14
|
+
contador.valores += 1;
|
|
15
|
+
contador.categorias.add(categoria);
|
|
16
|
+
}
|
|
4
17
|
function lerRegras(entradas) {
|
|
5
18
|
return entradas
|
|
6
19
|
.map((entrada) => entrada.trim().toLowerCase().split('.'))
|
|
@@ -21,20 +34,109 @@ function identificadores(sql, dialeto) {
|
|
|
21
34
|
return Array.from(sql.matchAll(/[\p{L}_][\p{L}\p{N}_$]*/gu), (m) => m[0].toLowerCase());
|
|
22
35
|
}
|
|
23
36
|
}
|
|
24
|
-
function
|
|
37
|
+
function ehContagem(tokens) {
|
|
38
|
+
if (tokens[0]?.tipo !== 'palavra' || tokens[0].valor !== 'count' || tokens[1]?.valor !== '(')
|
|
39
|
+
return false;
|
|
40
|
+
let profundidade = 0;
|
|
41
|
+
let fim = -1;
|
|
42
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
43
|
+
if (tokens[i].valor === '(')
|
|
44
|
+
profundidade += 1;
|
|
45
|
+
else if (tokens[i].valor === ')' && --profundidade === 0) {
|
|
46
|
+
fim = i;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (fim === -1)
|
|
51
|
+
return false;
|
|
52
|
+
const resto = tokens.slice(fim + 1);
|
|
53
|
+
const apelido = (t) => t?.tipo === 'palavra' || t?.tipo === 'identificador-citado';
|
|
54
|
+
if (resto.length === 0)
|
|
55
|
+
return true;
|
|
56
|
+
if (resto.length === 1)
|
|
57
|
+
return apelido(resto[0]);
|
|
58
|
+
return resto.length === 2 && resto[0].tipo === 'palavra' && resto[0].valor === 'as' && apelido(resto[1]);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Identificadores de cada item do `SELECT`, na ordem das colunas. Só quando o SQL tem um único `SELECT` (sem `WITH`,
|
|
62
|
+
* `UNION`, subconsulta nem `*`) e o número de itens bate com o de colunas; nos demais casos, `null`, e vale o SQL
|
|
63
|
+
* inteiro, porque um apelido de subconsulta ou CTE esconderia a coluna pessoal.
|
|
64
|
+
*/
|
|
65
|
+
function itensDoSelect(sql, dialeto, colunas) {
|
|
66
|
+
let tokens;
|
|
67
|
+
try {
|
|
68
|
+
tokens = tokenizar(sql, dialeto).filter((t) => t.tipo !== 'comentario');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const palavras = tokens.filter((t) => t.tipo === 'palavra').map((t) => t.valor);
|
|
74
|
+
if (tokens[0]?.tipo !== 'palavra' || tokens[0].valor !== 'select')
|
|
75
|
+
return null;
|
|
76
|
+
if (palavras.filter((p) => p === 'select').length !== 1)
|
|
77
|
+
return null;
|
|
78
|
+
if (palavras.some((p) => p === 'with' || p === 'union' || p === 'intersect' || p === 'except'))
|
|
79
|
+
return null;
|
|
80
|
+
const itens = [[]];
|
|
81
|
+
let profundidade = 0;
|
|
82
|
+
let anterior = 'select';
|
|
83
|
+
for (const t of tokens.slice(1)) {
|
|
84
|
+
if (t.tipo === 'palavra' && profundidade === 0 && FIM_DOS_ITENS.has(t.valor))
|
|
85
|
+
break;
|
|
86
|
+
if (t.tipo === 'simbolo') {
|
|
87
|
+
if (t.valor === '(')
|
|
88
|
+
profundidade += 1;
|
|
89
|
+
else if (t.valor === ')')
|
|
90
|
+
profundidade -= 1;
|
|
91
|
+
else if (t.valor === '*' && ['select', 'distinct', 'all', ',', '.'].includes(anterior))
|
|
92
|
+
return null;
|
|
93
|
+
if (t.valor === ',' && profundidade === 0) {
|
|
94
|
+
itens.push([]);
|
|
95
|
+
anterior = t.valor;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
itens.at(-1).push(t);
|
|
100
|
+
anterior = t.valor;
|
|
101
|
+
}
|
|
102
|
+
if (itens.length !== colunas)
|
|
103
|
+
return null;
|
|
104
|
+
return itens.map((item) => ({
|
|
105
|
+
ids: item.filter((t) => t.tipo === 'palavra' || t.tipo === 'identificador-citado').map((t) => t.valor),
|
|
106
|
+
contagem: ehContagem(item),
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
function colunaPessoalCitada(ids, mascarar) {
|
|
25
110
|
const extras = new Set(mascarar.map((r) => r.coluna));
|
|
26
|
-
for (const id of
|
|
111
|
+
for (const id of ids) {
|
|
27
112
|
const categoria = classificarColuna(id);
|
|
28
113
|
if (categoria)
|
|
29
|
-
return categoria;
|
|
114
|
+
return { categoria, identificador: id };
|
|
30
115
|
if (extras.has(id))
|
|
31
|
-
return 'mascarar_colunas';
|
|
116
|
+
return { categoria: 'mascarar_colunas', identificador: id };
|
|
32
117
|
}
|
|
33
118
|
return null;
|
|
34
119
|
}
|
|
35
120
|
function ehBinario(valor) {
|
|
36
121
|
return valor instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(valor));
|
|
37
122
|
}
|
|
123
|
+
function ehFlag(valor) {
|
|
124
|
+
if (typeof valor === 'boolean')
|
|
125
|
+
return true;
|
|
126
|
+
if (typeof valor === 'string')
|
|
127
|
+
return valor.trim().length <= 1;
|
|
128
|
+
if (typeof valor === 'number')
|
|
129
|
+
return Number.isInteger(valor) && valor >= 0 && valor <= 9;
|
|
130
|
+
if (typeof valor === 'bigint')
|
|
131
|
+
return valor >= 0n && valor <= 9n;
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
/** Valor que, numa coluna classificada pelo nome, segue só para a detecção pelo conteúdo. */
|
|
135
|
+
function dispensaMascaraDoNome(valor, categoria) {
|
|
136
|
+
if (!CATEGORIAS_DE_FORMATO.has(categoria))
|
|
137
|
+
return false;
|
|
138
|
+
return ehFlag(valor) || (categoria === 'email' && (typeof valor === 'number' || typeof valor === 'bigint'));
|
|
139
|
+
}
|
|
38
140
|
function mascararStringLivre(texto, contador) {
|
|
39
141
|
const inicio = texto.trimStart()[0];
|
|
40
142
|
if (inicio === '{' || inicio === '[') {
|
|
@@ -56,22 +158,23 @@ function mascararStringLivre(texto, contador) {
|
|
|
56
158
|
for (const t of trechos) {
|
|
57
159
|
saida += texto.slice(posicao, t.inicio) + String(mascararValor(texto.slice(t.inicio, t.fim), t.categoria));
|
|
58
160
|
posicao = t.fim;
|
|
161
|
+
contar(contador, t.categoria);
|
|
59
162
|
}
|
|
60
|
-
contador.valores += trechos.length;
|
|
61
163
|
return saida + texto.slice(posicao);
|
|
62
164
|
}
|
|
63
165
|
function mascararPorValor(valor, testarNumero, contador) {
|
|
64
166
|
if (typeof valor === 'string')
|
|
65
167
|
return mascararStringLivre(valor, contador);
|
|
66
168
|
if (typeof valor === 'number' || typeof valor === 'bigint') {
|
|
67
|
-
|
|
68
|
-
|
|
169
|
+
const categoria = testarNumero ? classificarNumero(valor) : null;
|
|
170
|
+
if (categoria !== null) {
|
|
171
|
+
contar(contador, categoria);
|
|
69
172
|
return OCULTO;
|
|
70
173
|
}
|
|
71
174
|
return valor;
|
|
72
175
|
}
|
|
73
176
|
if (ehBinario(valor)) {
|
|
74
|
-
contador
|
|
177
|
+
contar(contador, 'binario');
|
|
75
178
|
return OCULTO;
|
|
76
179
|
}
|
|
77
180
|
if (valor instanceof Date)
|
|
@@ -89,18 +192,32 @@ function mascararEstrutura(valor, contador) {
|
|
|
89
192
|
const saida = {};
|
|
90
193
|
for (const [chave, item] of Object.entries(valor)) {
|
|
91
194
|
const categoria = classificarColuna(chave);
|
|
92
|
-
if (categoria === null) {
|
|
195
|
+
if (categoria === null || dispensaMascaraDoNome(item, categoria)) {
|
|
93
196
|
saida[chave] = mascararPorValor(item, true, contador);
|
|
94
197
|
}
|
|
95
198
|
else {
|
|
96
199
|
const mascarado = mascararValor(item, categoria);
|
|
97
200
|
if (mascarado !== item)
|
|
98
|
-
contador
|
|
201
|
+
contar(contador, categoria);
|
|
99
202
|
saida[chave] = mascarado;
|
|
100
203
|
}
|
|
101
204
|
}
|
|
102
205
|
return saida;
|
|
103
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Texto calculado que é um número (o PostgreSQL devolve `count`, `sum` e `numeric` como texto) é testado como
|
|
209
|
+
* número, como na D-04: o inteiro também passa pelo CPF de 9 e 10 dígitos que o `CAST(cpf AS bigint)` produz.
|
|
210
|
+
*/
|
|
211
|
+
function mascararNumeroEmTexto(texto, contador) {
|
|
212
|
+
if (INTEIRO_EM_TEXTO.test(texto) && texto.length <= 19) {
|
|
213
|
+
const categoria = classificarNumero(BigInt(texto));
|
|
214
|
+
if (categoria !== null) {
|
|
215
|
+
contar(contador, categoria);
|
|
216
|
+
return OCULTO;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return mascararStringLivre(texto, contador);
|
|
220
|
+
}
|
|
104
221
|
function tratamentoDaColuna(nome, origem, mascarar, liberar) {
|
|
105
222
|
if (casa(mascarar, nome, origem))
|
|
106
223
|
return { tipo: 'ocultar', categoria: 'mascarar_colunas' };
|
|
@@ -114,36 +231,57 @@ function tratamentoDaColuna(nome, origem, mascarar, liberar) {
|
|
|
114
231
|
}
|
|
115
232
|
/**
|
|
116
233
|
* Mascara os dados pessoais de um resultado pela origem da coluna, pelo nome, pelas regras do projeto e pelo
|
|
117
|
-
* valor. Não altera o resultado recebido; com `ativo: false`, devolve-o sem mudança.
|
|
234
|
+
* valor, célula a célula. Não altera o resultado recebido; com `ativo: false`, devolve-o sem mudança.
|
|
118
235
|
*/
|
|
119
236
|
export function mascararResultado(resultado, opcoes) {
|
|
120
237
|
if (!opcoes.ativo)
|
|
121
|
-
return { resultado, colunasMascaradas: [], valoresEmTexto: 0 };
|
|
238
|
+
return { resultado, colunasMascaradas: [], valoresEmTexto: 0, detectados: [] };
|
|
122
239
|
const mascarar = lerRegras(opcoes.regras.mascarar_colunas);
|
|
123
240
|
const liberar = lerRegras(opcoes.regras.liberar_colunas);
|
|
124
241
|
const tratamentos = resultado.colunas.map((nome, i) => tratamentoDaColuna(nome, resultado.origens === undefined ? undefined : resultado.origens[i], mascarar, liberar));
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
242
|
+
const citacoes = new Map();
|
|
243
|
+
let itens;
|
|
244
|
+
const item = (i) => {
|
|
245
|
+
if (itens === undefined)
|
|
246
|
+
itens = itensDoSelect(opcoes.sql, opcoes.dialeto, resultado.colunas.length);
|
|
247
|
+
return itens ? itens[i] : null;
|
|
248
|
+
};
|
|
249
|
+
const citacao = (i) => {
|
|
250
|
+
if (!citacoes.has(i)) {
|
|
251
|
+
citacoes.set(i, colunaPessoalCitada(item(i)?.ids ?? identificadores(opcoes.sql, opcoes.dialeto), mascarar));
|
|
252
|
+
}
|
|
253
|
+
return citacoes.get(i);
|
|
130
254
|
};
|
|
131
255
|
const marcadas = new Map();
|
|
132
|
-
const
|
|
256
|
+
const contadores = resultado.colunas.map(() => novoContador());
|
|
133
257
|
const linhas = resultado.linhas.map((linha) => linha.map((valor, i) => {
|
|
134
258
|
const t = tratamentos[i] ?? { tipo: 'valor', calculada: false, numero: true };
|
|
259
|
+
const contador = contadores[i] ?? novoContador();
|
|
135
260
|
if (t.tipo === 'ocultar') {
|
|
136
|
-
marcadas.set(i, t.categoria);
|
|
261
|
+
marcadas.set(i, { coluna: resultado.colunas[i] ?? String(i), categoria: t.categoria, motivo: 'projeto' });
|
|
137
262
|
return valor === null || valor === undefined ? valor : OCULTO;
|
|
138
263
|
}
|
|
139
264
|
if (t.tipo === 'categoria') {
|
|
140
|
-
|
|
141
|
-
|
|
265
|
+
if (dispensaMascaraDoNome(valor, t.categoria))
|
|
266
|
+
return mascararPorValor(valor, true, contador);
|
|
267
|
+
const mascarado = mascararValor(valor, t.categoria);
|
|
268
|
+
if (mascarado !== valor) {
|
|
269
|
+
marcadas.set(i, { coluna: resultado.colunas[i] ?? String(i), categoria: t.categoria, motivo: 'nome' });
|
|
270
|
+
}
|
|
271
|
+
return mascarado;
|
|
142
272
|
}
|
|
143
273
|
if (t.calculada && typeof valor === 'string') {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
274
|
+
const citada = citacao(i);
|
|
275
|
+
// Um número só escapa da citação quando é contagem: `sum(salario)` por pessoa ou `substr(cpf, 1, 9)` não.
|
|
276
|
+
if (NUMERO_EM_TEXTO.test(valor) && (citada === null || item(i)?.contagem))
|
|
277
|
+
return mascararNumeroEmTexto(valor, contador);
|
|
278
|
+
if (citada !== null) {
|
|
279
|
+
marcadas.set(i, {
|
|
280
|
+
coluna: resultado.colunas[i] ?? String(i),
|
|
281
|
+
categoria: citada.categoria,
|
|
282
|
+
motivo: 'sql',
|
|
283
|
+
citada: citada.identificador,
|
|
284
|
+
});
|
|
147
285
|
return OCULTO;
|
|
148
286
|
}
|
|
149
287
|
}
|
|
@@ -152,10 +290,10 @@ export function mascararResultado(resultado, opcoes) {
|
|
|
152
290
|
const copia = { ...resultado, colunas: [...resultado.colunas], linhas };
|
|
153
291
|
if (resultado.origens !== undefined)
|
|
154
292
|
copia.origens = resultado.origens.map((o) => (o ? { ...o } : o));
|
|
155
|
-
const colunasMascaradas = [...marcadas.keys()]
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return { resultado: copia, colunasMascaradas, valoresEmTexto
|
|
293
|
+
const colunasMascaradas = [...marcadas.keys()].sort((a, b) => a - b).map((i) => marcadas.get(i));
|
|
294
|
+
const detectados = contadores.flatMap((c, i) => c.valores === 0 ? [] : [{ coluna: resultado.colunas[i] ?? String(i), categorias: [...c.categorias] }]);
|
|
295
|
+
const valoresEmTexto = contadores.reduce((total, c) => total + c.valores, 0);
|
|
296
|
+
return { resultado: copia, colunasMascaradas, valoresEmTexto, detectados };
|
|
159
297
|
}
|
|
160
298
|
/** Categoria com que a coluna `tabela.coluna` sai mascarada pelo nome ou pelas regras do projeto, ou `null`. */
|
|
161
299
|
export function categoriaDaColuna(tabela, coluna, regras) {
|
|
@@ -164,5 +302,5 @@ export function categoriaDaColuna(tabela, coluna, regras) {
|
|
|
164
302
|
}
|
|
165
303
|
/** Aplica a detecção por valor a um texto solto, como a mensagem de erro do banco. */
|
|
166
304
|
export function mascararTexto(texto) {
|
|
167
|
-
return mascararStringLivre(texto,
|
|
305
|
+
return mascararStringLivre(texto, novoContador());
|
|
168
306
|
}
|
|
@@ -8,7 +8,7 @@ const PARTES_ISOLADAS = [
|
|
|
8
8
|
['cep', ['cep', 'zip', 'postal']],
|
|
9
9
|
['nascimento', ['nascimento', 'nasc', 'birth', 'birthday']],
|
|
10
10
|
['nome', ['sobrenome']],
|
|
11
|
-
['financeiro', ['salario', 'renda', 'salary', 'income', '
|
|
11
|
+
['financeiro', ['salario', 'renda', 'salary', 'income', 'iban', 'pix']],
|
|
12
12
|
['cartao', ['cartao', 'card', 'cvv', 'cvc']],
|
|
13
13
|
['ip', ['ip']],
|
|
14
14
|
['segredo', ['senha', 'password', 'passwd', 'pwd', 'hash', 'token', 'secret', 'apikey']],
|
|
@@ -35,6 +35,10 @@ const COMBINACOES = [
|
|
|
35
35
|
['nome', [['first', 'last', 'full'], ['name']]],
|
|
36
36
|
['nome', [['nome', 'name'], PARTES_DE_PESSOA]],
|
|
37
37
|
];
|
|
38
|
+
/** Com uma destas partes, `pis` é o tributo (`valor_pis`, `retencao_pis_cofins`), não o número PIS do trabalhador. */
|
|
39
|
+
const PARTES_DE_TRIBUTO = new Set([
|
|
40
|
+
'valor', 'vl', 'vlr', 'percentual', 'perc', 'aliquota', 'aliq', 'bc', 'base', 'retencao', 'ret', 'cst', 'cofins',
|
|
41
|
+
]);
|
|
38
42
|
const TERMOS_FORTES = [
|
|
39
43
|
['cpf', 'cpf'],
|
|
40
44
|
['cnpj', 'cnpj'],
|
|
@@ -67,6 +71,8 @@ export function partesDoNome(nome) {
|
|
|
67
71
|
export function classificarColuna(nome) {
|
|
68
72
|
const partes = partesDoNome(nome);
|
|
69
73
|
const tem = new Set(partes);
|
|
74
|
+
if (partes.some((p) => PARTES_DE_TRIBUTO.has(p)))
|
|
75
|
+
tem.delete('pis');
|
|
70
76
|
for (const [categoria, listas] of COMBINACOES) {
|
|
71
77
|
if (listas.every((lista) => lista.some((p) => tem.has(p))))
|
|
72
78
|
return categoria;
|
|
@@ -90,8 +96,9 @@ function soDigitos(texto) {
|
|
|
90
96
|
}
|
|
91
97
|
function digitoVerificador(digitos, pesos) {
|
|
92
98
|
let soma = 0;
|
|
99
|
+
// No CNPJ alfanumérico, cada caractere vale o código ASCII menos 48; nos dígitos, isso é o próprio dígito.
|
|
93
100
|
for (let i = 0; i < pesos.length; i++)
|
|
94
|
-
soma +=
|
|
101
|
+
soma += ((digitos.charCodeAt(i) || 48) - 48) * (pesos[i] ?? 0);
|
|
95
102
|
const resto = soma % 11;
|
|
96
103
|
return resto < 2 ? 0 : 11 - resto;
|
|
97
104
|
}
|
|
@@ -102,9 +109,10 @@ export function cpfValido(cpf) {
|
|
|
102
109
|
return (digitoVerificador(d, [10, 9, 8, 7, 6, 5, 4, 3, 2]) === Number(d[9]) &&
|
|
103
110
|
digitoVerificador(d, [11, 10, 9, 8, 7, 6, 5, 4, 3, 2]) === Number(d[10]));
|
|
104
111
|
}
|
|
112
|
+
/** Aceita também o CNPJ alfanumérico: 12 caracteres `[0-9A-Z]` e 2 dígitos verificadores. */
|
|
105
113
|
export function cnpjValido(cnpj) {
|
|
106
|
-
const d =
|
|
107
|
-
if (d
|
|
114
|
+
const d = cnpj.toUpperCase().replace(/[^0-9A-Z]/g, '');
|
|
115
|
+
if (!/^[0-9A-Z]{12}\d{2}$/.test(d) || DIGITOS_REPETIDOS.test(d))
|
|
108
116
|
return false;
|
|
109
117
|
return (digitoVerificador(d, [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]) === Number(d[12]) &&
|
|
110
118
|
digitoVerificador(d, [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]) === Number(d[13]));
|
|
@@ -131,6 +139,7 @@ const MAX_DIGITOS = 19;
|
|
|
131
139
|
const CPF_FORMATADO = /^\d{3}\.\d{3}\.\d{3}-\d{2}$/;
|
|
132
140
|
const CNPJ_FORMATADO = /^\d{2}\.\d{3}\.\d{3}\/\d{4}-\d{2}$/;
|
|
133
141
|
const EMAIL = /[\p{L}\p{M}\p{N}._%+-]+@[\p{L}\p{M}\p{N}-]+(?:\.[\p{L}\p{M}\p{N}-]+)*\.[\p{L}\p{M}]{2,}/gu;
|
|
142
|
+
const CNPJ_ALFANUMERICO = /(?<![\p{L}\p{N}])[0-9A-Z]{2}\.?[0-9A-Z]{3}\.?[0-9A-Z]{3}\/?[0-9A-Z]{4}-?\d{2}(?![\p{L}\p{N}])/gu;
|
|
134
143
|
const TELEFONE = /(?<![\d+])(?:\+55\s?)?(?:\(\d{2}\)\s?|\d{2}[\s-])9?\d{4}[\s-]?\d{4}(?!\d)/g;
|
|
135
144
|
/** `seq` é um trecho de grupos inteiros com no máximo `MAX_DIGITOS` dígitos. */
|
|
136
145
|
function classificarSequencia(seq) {
|
|
@@ -185,6 +194,10 @@ export function encontrarNoTexto(texto) {
|
|
|
185
194
|
...trechosDe(texto, EMAIL, 'email'),
|
|
186
195
|
...trechosDe(texto, TELEFONE, 'telefone'),
|
|
187
196
|
...trechosDeDigitos(texto),
|
|
197
|
+
...trechosDe(texto, CNPJ_ALFANUMERICO, 'cnpj').filter((t) => {
|
|
198
|
+
const trecho = texto.slice(t.inicio, t.fim);
|
|
199
|
+
return /[A-Z]/.test(trecho) && cnpjValido(trecho);
|
|
200
|
+
}),
|
|
188
201
|
].sort((a, b) => prioridade(a.categoria) - prioridade(b.categoria) || b.fim - b.inicio - (a.fim - a.inicio) || a.inicio - b.inicio);
|
|
189
202
|
const escolhidos = [];
|
|
190
203
|
for (const t of candidatos) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const OCULTO = '[OCULTO]';
|
|
2
2
|
const CONECTIVOS = new Set(['da', 'de', 'do', 'das', 'dos', 'e']);
|
|
3
|
-
function manterIntervalo(texto, inicio, fim) {
|
|
3
|
+
function manterIntervalo(texto, inicio, fim, caractere = /\d/g) {
|
|
4
4
|
let posicao = 0;
|
|
5
|
-
return texto.replace(
|
|
5
|
+
return texto.replace(caractere, (digito) => {
|
|
6
6
|
posicao += 1;
|
|
7
7
|
return posicao >= inicio && posicao <= fim ? digito : '*';
|
|
8
8
|
});
|
|
@@ -13,9 +13,9 @@ function mascararCpf(texto) {
|
|
|
13
13
|
return manterIntervalo(texto, 4, 9);
|
|
14
14
|
}
|
|
15
15
|
function mascararCnpj(texto) {
|
|
16
|
-
if (!/^(
|
|
16
|
+
if (!/^([0-9A-Z]{2}\.[0-9A-Z]{3}\.[0-9A-Z]{3}\/[0-9A-Z]{4}-\d{2}|[0-9A-Z]{12}\d{2})$/.test(texto))
|
|
17
17
|
return OCULTO;
|
|
18
|
-
return manterIntervalo(texto, 3, 12);
|
|
18
|
+
return manterIntervalo(texto, 3, 12, /[0-9A-Z]/g);
|
|
19
19
|
}
|
|
20
20
|
function mascararDocumento(texto) {
|
|
21
21
|
if (!/^[A-Za-z0-9./\s-]+$/.test(texto))
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { allCommands, loadCatalog } from './catalog.js';
|
|
6
|
+
import { acharRaiz } from './raiz.js';
|
|
6
7
|
import { runCommand, runnableIds, suggestIds } from './run.js';
|
|
7
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
/** Comandos renomeados depois da 2.7.0: o nome antigo aponta o novo em vez de cair em "Comando desconhecido". */
|
|
@@ -127,7 +128,7 @@ async function main() {
|
|
|
127
128
|
}
|
|
128
129
|
if (cmd === 'doctor') {
|
|
129
130
|
const { formatFindings, runDoctor } = await import('./doctor/checks.js');
|
|
130
|
-
const root = resolve(process.cwd(), argv[1]
|
|
131
|
+
const root = argv[1] === undefined ? acharRaiz() : resolve(process.cwd(), argv[1]);
|
|
131
132
|
if (!existsSync(root))
|
|
132
133
|
throw new Error(`Pasta não encontrada: ${root}`);
|
|
133
134
|
const findings = runDoctor(root);
|
|
@@ -6,6 +6,7 @@ import { suggestFlag } from '../install/flags.js';
|
|
|
6
6
|
import { hashContent } from '../install/lock.js';
|
|
7
7
|
import { executePlan, packageInfo } from '../install/plan.js';
|
|
8
8
|
import { mergeSettings } from '../install/settings.js';
|
|
9
|
+
import { acharRaiz } from '../raiz.js';
|
|
9
10
|
import { COMMENTS_BASELINE_PATH, COMMENTS_HOOK_PATH, COMMENTS_RULE_PATH, commentsHooksConfig, ensureCommentsBaseline } from './instalar.js';
|
|
10
11
|
export { COMMENTS_HOOK_PATH } from './instalar.js';
|
|
11
12
|
const FLAGS = ['--yes', '-y', '--sim'];
|
|
@@ -27,7 +28,7 @@ export function runCommentsCommand(argv) {
|
|
|
27
28
|
throw new Error(`Flag desconhecida: ${token}.${suggestion ? ` Você quis dizer ${suggestion}?` : ''}`);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
|
-
const root = resolve(process.cwd(), path
|
|
31
|
+
const root = path === undefined ? acharRaiz() : resolve(process.cwd(), path);
|
|
31
32
|
if (!existsSync(root))
|
|
32
33
|
throw new Error(`Pasta não encontrada: ${root}`);
|
|
33
34
|
const rule = readFileSync(join(templatesDir, 'rules', 'comentarios.md'));
|
package/dist/memoria/indice.js
CHANGED
|
@@ -3,6 +3,7 @@ import { EOL } from 'node:os';
|
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { ARTIFACT_TYPES, readArtifact, WORKS_DIR } from '../contratos/artefatos.js';
|
|
5
5
|
import { suggestFlag } from '../install/flags.js';
|
|
6
|
+
import { acharRaiz } from '../raiz.js';
|
|
6
7
|
export const INDEX_PATH = join('.ai-harness', 'memoria', 'indice.json');
|
|
7
8
|
export const INDEX_VERSION = 1;
|
|
8
9
|
const BACKSLASH = String.fromCharCode(92);
|
|
@@ -159,7 +160,7 @@ export function runMemoryCommand(argv) {
|
|
|
159
160
|
else
|
|
160
161
|
bugId = value;
|
|
161
162
|
}
|
|
162
|
-
const root = resolve(process.cwd(), path
|
|
163
|
+
const root = path === undefined ? acharRaiz() : resolve(process.cwd(), path);
|
|
163
164
|
if (!existsSync(root))
|
|
164
165
|
throw new Error(`Pasta não encontrada: ${root}`);
|
|
165
166
|
const index = buildIndex(root);
|
package/dist/raiz.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
/** Sobe até o diretório mais próximo que tenha o marcador, ou devolve undefined. */
|
|
4
|
+
function subirAte(inicio, marcador) {
|
|
5
|
+
for (let pasta = inicio;; pasta = dirname(pasta)) {
|
|
6
|
+
if (existsSync(join(pasta, marcador)))
|
|
7
|
+
return pasta;
|
|
8
|
+
if (dirname(pasta) === pasta)
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Raiz do projeto para os comandos rodados sem caminho: o Bash dos agentes herda o `cd` de comandos anteriores, então
|
|
14
|
+
* o diretório atual pode ser uma subpasta. Vale o `.ai-harness/` mais próximo, depois o `.git` (arquivo em submódulo e
|
|
15
|
+
* worktree); sem nenhum, o próprio diretório. `CLAUDE_PROJECT_DIR` fica de fora: não é garantida no Bash, e a suíte
|
|
16
|
+
* roda dentro do Claude Code.
|
|
17
|
+
*/
|
|
18
|
+
export function acharRaiz(inicio = process.cwd()) {
|
|
19
|
+
const pasta = resolve(inicio);
|
|
20
|
+
return subirAte(pasta, '.ai-harness') ?? subirAte(pasta, '.git') ?? pasta;
|
|
21
|
+
}
|
package/dist/rastro/command.js
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
3
|
import { templatesDir } from '../catalog.js';
|
|
4
4
|
import { suggestFlag } from '../install/flags.js';
|
|
5
|
+
import { acharRaiz } from '../raiz.js';
|
|
5
6
|
/** O CLI grava pela mesma biblioteca dos hooks, para as duas formas produzirem o mesmo formato. */
|
|
6
7
|
export async function loadRastroLib() {
|
|
7
8
|
return (await import(pathToFileURL(join(templatesDir, 'hooks', 'lib', 'rastro.mjs')).href));
|
|
@@ -41,6 +42,6 @@ export async function runEventCommand(argv) {
|
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
|
-
(await loadRastroLib()).recordEvent(
|
|
45
|
+
(await loadRastroLib()).recordEvent(acharRaiz(), fields);
|
|
45
46
|
return 0;
|
|
46
47
|
}
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
4
|
import { readArtifact } from '../contratos/artefatos.js';
|
|
5
5
|
import { suggestFlag } from '../install/flags.js';
|
|
6
|
+
import { acharRaiz } from '../raiz.js';
|
|
6
7
|
import { loadRastroLib } from '../rastro/command.js';
|
|
7
8
|
const FINISHED = new Set(['concluido', 'cancelado']);
|
|
8
9
|
const normalize = (path) => path.trim().replace(/^\.\//, '').replace(/\/+$/, '');
|
|
@@ -84,7 +85,7 @@ export async function waitForFreeFiles(root, workId, options) {
|
|
|
84
85
|
}
|
|
85
86
|
const FLAGS = ['--aguardar', '--intervalo'];
|
|
86
87
|
/** `ai-harness conflitos <id> [--aguardar] [--intervalo <segundos>]` — sai com 1 quando há conflito sem --aguardar. */
|
|
87
|
-
export async function runConflictsCommand(argv, root =
|
|
88
|
+
export async function runConflictsCommand(argv, root = acharRaiz()) {
|
|
88
89
|
const [workId, ...rest] = argv;
|
|
89
90
|
if (!workId || workId.startsWith('-')) {
|
|
90
91
|
throw new Error('Informe o id do trabalho. Ex.: npx @fontdata/ai-harness conflitos 2026-09-18-login --aguardar');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fontdata/ai-harness",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "Harness de engenharia com IA da Fontdata para Claude Code: roda os slash commands direto na pasta do projeto via npx, sem instalar nada — ou instala-os em .claude/commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|