@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontdata",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "Harness de engenharia com IA da Fontdata para Claude Code",
5
5
  "author": { "name": "Fontdata" },
6
6
  "commands": ["./templates/commands"],
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.0",
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|^#!|use (strict|client|server)/i;
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
- function comentario(linha, ext) {
46
- const t = linha.trim();
47
- if (!t) return null;
48
- if (ext === '.py' || ext === '.sh') return t.startsWith('#') ? t.replace(/^#+/, '').trim() : null;
49
- if (ext === '.sql') {
50
- if (t.startsWith('--')) return t.replace(/^-+/, '').trim();
51
- if (t.startsWith('/*') || t.startsWith('*')) return t.replace(/^[/*]+/, '').trim();
52
- return null;
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
- if (ext === '.pas' || ext === '.dpr') {
55
- if (t.startsWith('//')) return t.replace(/^\/+/, '').trim();
56
- if (t.startsWith('(*')) return t.replace(/^\(\*/, '').trim();
57
- if (t.startsWith('{') && !t.startsWith('{$')) return t.replace(/^\{/, '').trim();
58
- return null;
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
- if (t.startsWith('//')) return t.replace(/^\/+/, '').trim();
61
- if (t.startsWith('/*')) return t.replace(/^\/\*+/, '').trim();
62
- if (t.startsWith('*')) return t.replace(/^\*+\/?/, '').trim();
63
- return null;
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.map((l) => comentario(l, ext));
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] === null) { i++; continue; }
184
+ if (!ehTexto(ehComentario[i])) { i++; continue; }
81
185
  const inicio = i;
82
186
  const conteudo = [];
83
- while (i < linhas.length && ehComentario[i] !== null) { conteudo.push(ehComentario[i]); 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
- /^(\/\*\*|"""|'''|\/\/\/)/.test(linhas[inicio].trim()) &&
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((c) => c !== null).length;
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
  }