@fontdata/ai-harness 2.3.0 → 2.3.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/README.md
CHANGED
|
@@ -141,7 +141,7 @@ Os formatos estão em [`docs/contratos/ARTEFATOS.md`](docs/contratos/ARTEFATOS.m
|
|
|
141
141
|
| Hook | Quando | Modo inicial |
|
|
142
142
|
|---|---|---|
|
|
143
143
|
| `lembrete` | O pedido parece bug, funcionalidade nova, risco de segurança ou problema de performance: sugere o fluxo certo | aviso |
|
|
144
|
-
| `segredo-no-commit` | `git commit` com chave de acesso, token, chave privada ou senha no diff | **bloqueio** |
|
|
144
|
+
| `segredo-no-commit` | `git commit` com chave de acesso, token, chave privada ou senha no diff (em arquivo de teste, só token e chave de verdade) | **bloqueio** |
|
|
145
145
|
| `proteger-contratos` | SPEC, PLAN ou CLAUDE.md reescritos durante a execução de um trabalho | aviso |
|
|
146
146
|
| `task-so-fecha-verde` | Task marcada como concluída sem suíte verde no rastro | aviso |
|
|
147
147
|
| `sem-jargao` | Termo técnico no relatório para o cliente | aviso |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fontdata/ai-harness",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.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": {
|
|
@@ -9,7 +9,8 @@ const DENSIDADE_ALVO = 0.1;
|
|
|
9
9
|
const EXTENSOES = new Set([
|
|
10
10
|
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
11
11
|
'.py', '.go', '.rs', '.java', '.cs', '.php',
|
|
12
|
-
'.pas', '.dpr', '.sql', '.sh',
|
|
12
|
+
'.pas', '.dpr', '.dpk', '.lpr', '.sql', '.sh', '.bash', '.zsh',
|
|
13
|
+
'.ps1', '.psm1', '.bat', '.cmd',
|
|
13
14
|
]);
|
|
14
15
|
|
|
15
16
|
const IGNORAR = [
|
|
@@ -21,7 +22,7 @@ const IGNORAR = [
|
|
|
21
22
|
];
|
|
22
23
|
|
|
23
24
|
const CABECALHO_PERMITIDO =
|
|
24
|
-
/eslint|ts-nocheck|ts-check|ts-ignore|prettier|@license|copyright|spdx|jshint|globals
|
|
25
|
+
/eslint|ts-nocheck|ts-check|ts-ignore|prettier|@license|copyright|spdx|jshint|globals|use (strict|client|server)|code generated|do not edit|auto-generated/i;
|
|
25
26
|
|
|
26
27
|
const BANNER = /^[-=─-╿_*#~+]{3,}|[-=─-╿_*#~+]{3,}$/;
|
|
27
28
|
|
|
@@ -42,33 +43,136 @@ const DATA = /(^|[^"'\w])20\d{2}-\d{2}-\d{2}([^"'\w]|$)/;
|
|
|
42
43
|
const CODIGO_TOKEN = /(^|\s)(const|let|var|function|return|if|for|while|await|import|export|def|class)\b/;
|
|
43
44
|
const CODIGO_FIM = /[;{}]\s*$/;
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
const so = (...exts) => new Set(exts);
|
|
47
|
+
const JS = so('.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs');
|
|
48
|
+
const C_LIKE = so(...JS, '.go', '.rs', '.java', '.cs', '.php');
|
|
49
|
+
const PASCAL = so('.pas', '.dpr', '.dpk', '.lpr');
|
|
50
|
+
const SHELL = so('.sh', '.bash', '.zsh');
|
|
51
|
+
const POWERSHELL = so('.ps1', '.psm1');
|
|
52
|
+
const BATCH = so('.bat', '.cmd');
|
|
53
|
+
|
|
54
|
+
const SINTAXE = [
|
|
55
|
+
[so('.php'), { linha: ['//', '#'], blocos: [['/*', '*/']], naoComentario: /^#\[/ }],
|
|
56
|
+
[C_LIKE, { linha: ['//'], blocos: [['/*', '*/']] }],
|
|
57
|
+
[PASCAL, { linha: ['//'], blocos: [['{', '}'], ['(*', '*)']] }],
|
|
58
|
+
[so('.sql'), { linha: ['--'], blocos: [['/*', '*/']] }],
|
|
59
|
+
[POWERSHELL, { linha: ['#'], blocos: [['<#', '#>']] }],
|
|
60
|
+
[so('.py', ...SHELL), { linha: ['#'], blocos: [] }],
|
|
61
|
+
[BATCH, { linha: ['::'], blocos: [] }],
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Têm forma de comentário, mas são lidas pelo compilador, pelo interpretador ou por uma ferramenta.
|
|
65
|
+
const DIRETIVA = Symbol('diretiva');
|
|
66
|
+
const DIRETIVAS = [
|
|
67
|
+
[PASCAL, /^(\{|\(\*)[$%]/],
|
|
68
|
+
[C_LIKE, /^\/[/*]\s*(#?region\b|#?endregion\b|<\/?editor-fold|@formatter:)/i],
|
|
69
|
+
[JS, /^\/\/\/\s*<(reference|amd-module|amd-dependency)\b/],
|
|
70
|
+
[JS, /^\/[/*]\*?\s*(@ts-(check|nocheck|ignore|expect-error)\b|eslint|jshint|jscs:|tslint:|prettier-ignore|(istanbul|c8|v8) ignore|biome-ignore|deno-|oxlint-|@flow\b|@jsx|@vite-ignore|@vitest-environment|@jest-environment|webpack[A-Z]\w*:|@refresh\b)/i],
|
|
71
|
+
[JS, /^\/\*+\s*globals?\s/],
|
|
72
|
+
[JS, /^\/\/[#@]\s*source(Mapping)?URL=/],
|
|
73
|
+
[so('.go'), /^\/\/(go:|line |export |extern )|^\/\/\s*(\+build\b|nolint\b|lint:ignore\b)/],
|
|
74
|
+
[so('.rs'), /^#!\[/],
|
|
75
|
+
[so('.java'), /^\/[/*]\s*(noinspection\b|NOSONAR\b|CHECKSTYLE[:.]|\$NON-NLS)/],
|
|
76
|
+
[so('.cs'), /^#\s*(region|endregion|if|elif|else|endif|define|undef|pragma|nullable|warning|error|line)\b/],
|
|
77
|
+
[so('.cs'), /^\/\/\s*(ReSharper\s|<\/?auto-generated)/i],
|
|
78
|
+
[so('.php'), /^\/[/*]\*?\s*(phpcs:|@phpstan-|@psalm-|@codeCoverageIgnore|@noinspection\b|@var\s)/],
|
|
79
|
+
[so('.sql'), /^\/\*[!+]/],
|
|
80
|
+
[so('.sql'), /^--\s*(liquibase\s|changeset\s|rollback\b|precondition|comment:|validCheckSum\b|ignoreLines:|\+(goose|migrate)\b|name:\s*\S+\s*:|noqa\b|sqlfluff:|#?(region|endregion)\b)/i],
|
|
81
|
+
[so('.py'), /^#\s*(-\*-.*-\*-|(en)?coding[:=]|vim?:|noqa\b|nosec\b|type:|pylint:|mypy:|pyright:|pyre-|pytype:|ruff:|isort:|fmt:|pragma\b)/i],
|
|
82
|
+
[SHELL, /^#\s*(shellcheck\s|-\*-.*-\*-|vim?:|ex:)|^#(compdef|autoload)\b/i],
|
|
83
|
+
[POWERSHELL, /^#(requires|region|endregion)\b/i],
|
|
84
|
+
];
|
|
85
|
+
const BLOCOS_DIRETIVA = [
|
|
86
|
+
[SHELL, /^###\s*BEGIN INIT INFO/, /^###\s*END INIT INFO/],
|
|
87
|
+
[POWERSHELL, /^#\s*SIG\s*#\s*Begin signature block/i, /^#\s*SIG\s*#\s*End signature block/i],
|
|
88
|
+
];
|
|
89
|
+
const HEREDOC = /(?<!<)<<(?!<)-?\s*(['"]?)([A-Za-z_]\w*)\1/g;
|
|
90
|
+
|
|
91
|
+
const limpar = (texto) => texto.replace(/^\*+/, '').trim();
|
|
92
|
+
|
|
93
|
+
function comentarioDeLinha(t, sintaxe, ext) {
|
|
94
|
+
if (BATCH.has(ext)) {
|
|
95
|
+
if (t.startsWith('::')) return t.replace(/^:+/, '').trim();
|
|
96
|
+
const rem = t.match(/^@?rem(?:[\s.:/\\](.*))?$/i);
|
|
97
|
+
return rem ? (rem[1] ?? '').trim() : null;
|
|
53
98
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
99
|
+
const marcador = sintaxe.linha.find((m) => t.startsWith(m));
|
|
100
|
+
if (!marcador || sintaxe.naoComentario?.test(t)) return null;
|
|
101
|
+
let k = 0;
|
|
102
|
+
while (t[k] === marcador[0]) k++;
|
|
103
|
+
return t.slice(k).trim();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Devolve, por linha: o texto do comentário, DIRETIVA, ou null para código e linha em branco.
|
|
107
|
+
function classificar(linhas, ext) {
|
|
108
|
+
const sintaxe = SINTAXE.find(([exts]) => exts.has(ext))?.[1] ?? { linha: ['//'], blocos: [['/*', '*/']] };
|
|
109
|
+
const saida = [];
|
|
110
|
+
const heredocs = [];
|
|
111
|
+
let fimTexto = null;
|
|
112
|
+
let fimDiretiva = null;
|
|
113
|
+
let bloco = null;
|
|
114
|
+
for (let i = 0; i < linhas.length; i++) {
|
|
115
|
+
const t = linhas[i].trim();
|
|
116
|
+
if (heredocs.length) {
|
|
117
|
+
if (t === heredocs[0]) heredocs.shift();
|
|
118
|
+
saida.push(null);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (fimTexto) {
|
|
122
|
+
if (fimTexto.test(t)) fimTexto = null;
|
|
123
|
+
saida.push(null);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (bloco) {
|
|
127
|
+
const pos = t.indexOf(bloco.fim);
|
|
128
|
+
saida.push(bloco.diretiva ? DIRETIVA : limpar(pos === -1 ? t : t.slice(0, pos)));
|
|
129
|
+
if (pos !== -1) bloco = null;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!t) { saida.push(null); continue; }
|
|
133
|
+
if (fimDiretiva) {
|
|
134
|
+
if (fimDiretiva.test(t)) fimDiretiva = null;
|
|
135
|
+
saida.push(DIRETIVA);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const linhaDiretiva = BLOCOS_DIRETIVA.find(([exts, abre]) => exts.has(ext) && abre.test(t));
|
|
139
|
+
if (linhaDiretiva) { fimDiretiva = linhaDiretiva[2]; saida.push(DIRETIVA); continue; }
|
|
140
|
+
|
|
141
|
+
const diretiva = (i === 0 && t.startsWith('#!')) || DIRETIVAS.some(([exts, re]) => exts.has(ext) && re.test(t));
|
|
142
|
+
const abre = sintaxe.blocos.find(([inicio]) => t.startsWith(inicio));
|
|
143
|
+
if (abre) {
|
|
144
|
+
const resto = t.slice(abre[0].length);
|
|
145
|
+
const pos = resto.indexOf(abre[1]);
|
|
146
|
+
if (pos === -1) bloco = { fim: abre[1], diretiva };
|
|
147
|
+
saida.push(diretiva ? DIRETIVA : limpar(pos === -1 ? resto : resto.slice(0, pos)));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (diretiva) { saida.push(DIRETIVA); continue; }
|
|
151
|
+
|
|
152
|
+
const c = comentarioDeLinha(t, sintaxe, ext);
|
|
153
|
+
saida.push(c);
|
|
154
|
+
if (c !== null) continue;
|
|
155
|
+
if (SHELL.has(ext)) for (const m of t.matchAll(HEREDOC)) heredocs.push(m[2]);
|
|
156
|
+
if (POWERSHELL.has(ext) && /@["']$/.test(t)) fimTexto = t.endsWith('"') ? /^"@/ : /^'@/;
|
|
59
157
|
}
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
158
|
+
// O comentário colado em `import "C"` é o preâmbulo do cgo: código C, não comentário.
|
|
159
|
+
if (ext === '.go') {
|
|
160
|
+
for (let i = 0; i < linhas.length; i++) {
|
|
161
|
+
if (linhas[i].trim() !== 'import "C"') continue;
|
|
162
|
+
for (let j = i - 1; j >= 0 && typeof saida[j] === 'string'; j--) saida[j] = DIRETIVA;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return saida;
|
|
64
166
|
}
|
|
65
167
|
|
|
168
|
+
const ehTexto = (c) => typeof c === 'string';
|
|
169
|
+
|
|
66
170
|
function analisar(caminho, fonte) {
|
|
67
171
|
const ext = extname(caminho);
|
|
68
172
|
const linhas = fonte.split('\n');
|
|
69
173
|
const achados = [];
|
|
70
174
|
const marca = (linha, msg) => achados.push({ linha, msg });
|
|
71
|
-
const ehComentario = linhas
|
|
175
|
+
const ehComentario = classificar(linhas, ext);
|
|
72
176
|
|
|
73
177
|
let primeiroCodigo = -1;
|
|
74
178
|
for (let i = 0; i < linhas.length; i++) {
|
|
@@ -77,10 +181,10 @@ function analisar(caminho, fonte) {
|
|
|
77
181
|
|
|
78
182
|
let i = 0;
|
|
79
183
|
while (i < linhas.length) {
|
|
80
|
-
if (ehComentario[i]
|
|
184
|
+
if (!ehTexto(ehComentario[i])) { i++; continue; }
|
|
81
185
|
const inicio = i;
|
|
82
186
|
const conteudo = [];
|
|
83
|
-
while (i < linhas.length && ehComentario[i]
|
|
187
|
+
while (i < linhas.length && ehTexto(ehComentario[i])) { conteudo.push(ehComentario[i]); i++; }
|
|
84
188
|
const fim = i - 1;
|
|
85
189
|
const corpo = conteudo.filter(Boolean);
|
|
86
190
|
const texto = corpo.join(' ');
|
|
@@ -103,9 +207,9 @@ function analisar(caminho, fonte) {
|
|
|
103
207
|
}
|
|
104
208
|
|
|
105
209
|
let proxima = fim + 1;
|
|
106
|
-
while (proxima < linhas.length && !linhas[proxima].trim()) proxima++;
|
|
210
|
+
while (proxima < linhas.length && (!linhas[proxima].trim() || ehComentario[proxima] === DIRETIVA)) proxima++;
|
|
107
211
|
const ehDocstring =
|
|
108
|
-
/^(\/\*\*|"""|'''
|
|
212
|
+
/^(\/\*\*|"""|'''|\/\/\/|<#)/.test(linhas[inicio].trim()) &&
|
|
109
213
|
proxima < linhas.length &&
|
|
110
214
|
ehComentario[proxima] === null;
|
|
111
215
|
const teto = ehDocstring ? MAX_DOCSTRING : MAX_BLOCO;
|
|
@@ -120,7 +224,7 @@ function analisar(caminho, fonte) {
|
|
|
120
224
|
}
|
|
121
225
|
}
|
|
122
226
|
|
|
123
|
-
const comentadas = ehComentario.filter(
|
|
227
|
+
const comentadas = ehComentario.filter(ehTexto).length;
|
|
124
228
|
const uteis = linhas.filter((l) => l.trim()).length;
|
|
125
229
|
return { achados, comentadas, uteis, densidade: uteis ? comentadas / uteis : 0 };
|
|
126
230
|
}
|
|
@@ -33,6 +33,15 @@ function isEnvFile(path) {
|
|
|
33
33
|
return /^\.env(\..+)?$/.test(name) && !/\.(example|sample|template|dist)$/.test(name);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Arquivo de teste: `tests/`, `test/`, `__tests__/`, `spec/` ou `*.test.ts` / `*.spec.js`...
|
|
38
|
+
* Fixture de senha é o caso normal em teste, então a heurística de atribuição não vale ali.
|
|
39
|
+
* @param {string} path
|
|
40
|
+
*/
|
|
41
|
+
function isTestFile(path) {
|
|
42
|
+
return /(^|\/)(tests?|__tests__|spec)\//.test(path) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(basename(path));
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
/**
|
|
37
46
|
* @param {string} path
|
|
38
47
|
* @param {string} content Linha adicionada, sem o `+`.
|
|
@@ -47,6 +56,8 @@ function secretIn(path, content) {
|
|
|
47
56
|
return value && !isPlaceholder(value) ? 'valor em arquivo .env' : null;
|
|
48
57
|
}
|
|
49
58
|
|
|
59
|
+
if (isTestFile(path)) return null;
|
|
60
|
+
|
|
50
61
|
const assigned = ASSIGNED_SECRET.exec(content);
|
|
51
62
|
return assigned && !isPlaceholder(assigned[2] ?? '') ? 'senha ou token atribuído no código' : null;
|
|
52
63
|
}
|