@aksp/opencrew 1.6.0 → 1.6.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/CHANGELOG.md +76 -0
- package/README.md +19 -5
- package/package.json +1 -1
- package/src/cli.js +2 -1
- package/src/commands/init.js +26 -18
- package/src/lib/migrations.js +40 -3
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/prompts/build.prompt.md +2 -0
- package/templates/_opencrew/core/runner.pipeline.md +54 -26
- package/templates/_opencrew/core/scripts/comum.mjs +48 -0
- package/templates/_opencrew/core/scripts/conferir-fontes/busca.mjs +82 -0
- package/templates/_opencrew/core/scripts/conferir-fontes/coleta.mjs +104 -0
- package/templates/_opencrew/core/scripts/conferir-fontes/relatorio.mjs +52 -0
- package/templates/_opencrew/core/scripts/conferir-fontes.mjs +82 -136
- package/templates/_opencrew/core/scripts/verificar/argumentos.mjs +33 -0
- package/templates/_opencrew/core/scripts/verificar/arquivos.mjs +50 -0
- package/templates/_opencrew/core/scripts/verificar/html.mjs +52 -0
- package/templates/_opencrew/core/scripts/verificar/leitura.mjs +138 -71
- package/templates/_opencrew/core/scripts/verificar/medicao.mjs +142 -0
- package/templates/_opencrew/core/scripts/verificar/pecas.mjs +190 -0
- package/templates/_opencrew/core/scripts/verificar/proibicoes.mjs +100 -0
- package/templates/_opencrew/core/scripts/verificar/regras.mjs +142 -92
- package/templates/_opencrew/core/scripts/verificar/relatorio.mjs +43 -0
- package/templates/_opencrew/core/scripts/verificar/secoes.mjs +145 -0
- package/templates/_opencrew/core/scripts/verificar.mjs +162 -88
|
@@ -1,118 +1,192 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Verificador automático do OpenCrew — mede o texto ANTES do revisor.
|
|
3
|
-
// Uso: node _opencrew/core/scripts/verificar.mjs --crew crews/<nome> --arquivo <
|
|
3
|
+
// Uso: node _opencrew/core/scripts/verificar.mjs --crew crews/<nome> --arquivo "<caminho=formato>[,<caminho=formato>…]" [--formato blog-post|blog-seo]
|
|
4
|
+
// caminho=formato: o formato declarado de cada arquivo (o `format:` do passo que o gerou); o
|
|
5
|
+
// "=formato" é opcional. --formato só escolhe os limites de blog do item sem "=formato" que tem
|
|
6
|
+
// título no frontmatter.
|
|
4
7
|
// Os limites vêm do frontmatter `constraints:` dos best-practices (fonte única).
|
|
5
|
-
// Última linha da saída
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
+
// Última linha da saída (o runner lê esta linha): VERIFICACAO:OK, VERIFICACAO:BLOQUEADA ou
|
|
9
|
+
// VERIFICACAO:AGUARDANDO_USUARIO (os únicos bloqueios são [PREENCHER], que só o usuário resolve).
|
|
10
|
+
// Código de saída: 0 = verificou (ao menos um caminho da lista existe) · 1 = erro de uso (opção
|
|
11
|
+
// obrigatória faltando, pasta sem `_opencrew/`, crew inexistente, crew ou caminho fora do
|
|
12
|
+
// projeto, nenhum caminho da lista existe) ou erro que impediu a verificação inteira; com
|
|
13
|
+
// código 1 não há linha VERIFICACAO:.
|
|
14
|
+
// Specs: fase-u1-revisor-com-dentes.md e fase-r1-reparos-1-6-1.md (repositório do OpenCrew).
|
|
8
15
|
import { existsSync } from 'node:fs';
|
|
9
16
|
import path from 'node:path';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
17
|
+
import { erroDeUso, ehPrincipal } from './comum.mjs';
|
|
18
|
+
import { USO, lerArgs, lerItemDaLista } from './verificar/argumentos.mjs';
|
|
19
|
+
import { lerItem } from './verificar/arquivos.mjs';
|
|
20
|
+
import { lerLimites, lerDominioDoSite, semFrontmatter } from './verificar/leitura.mjs';
|
|
21
|
+
import { lerProibicoes } from './verificar/proibicoes.mjs';
|
|
22
|
+
import { lerPecas } from './verificar/pecas.mjs';
|
|
23
|
+
import { medirPecas } from './verificar/medicao.mjs';
|
|
24
|
+
import { regrasGerais, temVariavel, item, FALTA_INFO, NAO_MEDIDO, NAO_VERIFICADO } from './verificar/regras.mjs';
|
|
25
|
+
import { formatarRelatorio } from './verificar/relatorio.mjs';
|
|
13
26
|
|
|
14
|
-
|
|
27
|
+
export { formatarRelatorio };
|
|
15
28
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
const NAO_LIDO = { ausente: 'arquivo não encontrado', pasta: 'é uma pasta', 'nao-utf8': 'o arquivo não está em UTF-8' };
|
|
30
|
+
const NOTA_VARIAVEL = 'Variável de personalização {{…}}: confira se a sua ferramenta de envio troca pelo dado real.';
|
|
31
|
+
const SEM_PROBLEMA = '✅ Nada a apontar.';
|
|
32
|
+
const SO_GERAIS = (formato) => `⚪ Nada a apontar nas checagens gerais (${formato ? '' : 'formato não informado: '}limites não medidos)`;
|
|
33
|
+
const naoVerificado = (motivo) => item(NAO_VERIFICADO, null, null, 'alerta', motivo);
|
|
34
|
+
const plural = (n, um, varios) => (n === 1 ? um : varios);
|
|
35
|
+
|
|
36
|
+
/** O que vale para a execução inteira: notas, proibições, site e os limites já lidos. */
|
|
37
|
+
async function prepararContexto(raiz, crew, formatoDeBlog) {
|
|
38
|
+
const notas = new Set();
|
|
23
39
|
const proibicoes = await lerProibicoes(raiz, crew);
|
|
24
|
-
if (!proibicoes.existe) notas.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
40
|
+
if (!proibicoes.existe) notas.add('Sem proibições registradas (a crew não tem memories.md).');
|
|
41
|
+
const n = proibicoes.semAspas;
|
|
42
|
+
if (n) notas.add(`${n} ${plural(n, 'proibição', 'proibições')} sem termo entre aspas ${plural(n, 'não é verificada', 'não são verificadas')} automaticamente — escreva o termo entre aspas na memória para virar trava.`);
|
|
43
|
+
if (proibicoes.preferidos.length) notas.add(`Termos lidos como preferidos (não bloqueiam): ${proibicoes.preferidos.map((t) => `"${t}"`).join(', ')}`);
|
|
44
|
+
return { raiz, formatoDeBlog, notas, proibidos: proibicoes.termos, dominio: await lerDominioDoSite(raiz), limites: {} };
|
|
45
|
+
}
|
|
30
46
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
];
|
|
39
|
-
resultado.push({ arquivo: rel, itens });
|
|
47
|
+
/** Limites dos formatos pedidos, lidos uma vez por execução; cada aviso vira nota. */
|
|
48
|
+
async function limitesDe(ctx, formatos) {
|
|
49
|
+
for (const id of formatos) {
|
|
50
|
+
if (id in ctx.limites) continue;
|
|
51
|
+
const { limites, nota } = await lerLimites(ctx.raiz, id);
|
|
52
|
+
ctx.limites[id] = limites;
|
|
53
|
+
if (nota) ctx.notas.add(nota);
|
|
40
54
|
}
|
|
41
|
-
|
|
55
|
+
return ctx.limites;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Procura e mede as peças de um `.md` ou `.txt` (regras 3 a 6). */
|
|
59
|
+
async function medir(texto, formato, ctx) {
|
|
60
|
+
const pecas = lerPecas(texto, formato, { formatoDeBlog: ctx.formatoDeBlog });
|
|
61
|
+
const formatos = new Set([...(formato ? [formato] : []), ...pecas.map((p) => p.formato)]);
|
|
62
|
+
const limites = await limitesDe(ctx, formatos);
|
|
63
|
+
return medirPecas({ pecas, corpo: semFrontmatter(texto), formato, limites, dominio: ctx.dominio });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Linha que fecha o arquivo sem bloqueio, alerta nem "Não medido"; null quando há o que apontar. */
|
|
67
|
+
function fechoDe(itens, medidas, formato) {
|
|
68
|
+
const temProblema = itens.some((i) => i.nivel === 'bloqueio' || i.nivel === 'alerta' || i.item === NAO_MEDIDO);
|
|
69
|
+
if (temProblema) return null;
|
|
70
|
+
return medidas ? SEM_PROBLEMA : SO_GERAIS(formato);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @returns {Promise<object|null>} o resultado do arquivo, ou null quando ele não é texto */
|
|
74
|
+
async function verificarArquivo({ arquivo, formato }, ctx, regraDeTeste) {
|
|
75
|
+
const lido = await lerItem(path.resolve(ctx.raiz, arquivo));
|
|
76
|
+
if (lido.tipo === 'nao-texto') return null;
|
|
77
|
+
if (lido.tipo !== 'texto') return { itens: [naoVerificado(NAO_LIDO[lido.tipo])], fecho: null };
|
|
78
|
+
const { itens: medidos, medidas } = lido.comPecas ? await medir(lido.texto, formato, ctx) : { itens: [], medidas: 0 };
|
|
79
|
+
// Sem procura de peças (.html, .csv…) o formato declarado ainda é lido, pelas notas dele.
|
|
80
|
+
if (formato && !lido.comPecas) await limitesDe(ctx, [formato]);
|
|
81
|
+
const envio = /^(email|whatsapp)-/.test(formato ?? '');
|
|
82
|
+
if (envio && temVariavel(lido.texto)) ctx.notas.add(NOTA_VARIAVEL);
|
|
83
|
+
const gerais = regrasGerais(lido.texto, ctx.proibidos, { variavelBloqueia: !envio });
|
|
84
|
+
const extras = regraDeTeste ? (await regraDeTeste({ arquivo, formato, texto: lido.texto })) ?? [] : [];
|
|
85
|
+
const naoMedido = (i) => i.item === NAO_MEDIDO;
|
|
86
|
+
const itens = [...medidos.filter((i) => !naoMedido(i)), ...gerais, ...extras, ...medidos.filter(naoMedido)];
|
|
87
|
+
return { itens, fecho: fechoDe(itens, medidas, formato) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Item da lista → `{ arquivo, formato }`; só o objeto traz formato declarado. */
|
|
91
|
+
function normalizar(entrada) {
|
|
92
|
+
if (typeof entrada === 'string') return { arquivo: entrada, formato: null };
|
|
93
|
+
return { arquivo: entrada.arquivo, formato: entrada.formato || null };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** O mesmo arquivo citado duas vezes, com o mesmo formato, é verificado (e contado) uma vez só. */
|
|
97
|
+
function semRepetidas(raiz, entradas) {
|
|
98
|
+
const vistas = new Set();
|
|
99
|
+
return entradas.filter((e) => {
|
|
100
|
+
const chave = `${path.resolve(raiz, e.arquivo)}|${e.formato ?? ''}`;
|
|
101
|
+
return !vistas.has(chave) && vistas.add(chave);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Caminho absoluto de dentro do projeto aparece no relatório como o relativo. */
|
|
106
|
+
function nomeNoRelatorio(raiz, arquivo) {
|
|
107
|
+
return path.isAbsolute(arquivo) ? path.relative(raiz, arquivo).split(path.sep).join('/') : arquivo;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resumir(arquivos, naoTexto, notas) {
|
|
111
|
+
const todos = arquivos.flatMap((a) => a.itens);
|
|
42
112
|
const bloqueios = todos.filter((i) => i.nivel === 'bloqueio');
|
|
43
|
-
const alertas = todos.filter((i) => i.nivel === 'alerta').length;
|
|
44
113
|
// [PREENCHER] só o usuário resolve: não força REJECT (o redator não tem o dado), mas a
|
|
45
114
|
// aprovação final não fecha sem ele.
|
|
46
115
|
const reais = bloqueios.filter((i) => i.item !== FALTA_INFO).length;
|
|
47
|
-
|
|
48
|
-
|
|
116
|
+
return {
|
|
117
|
+
arquivos,
|
|
118
|
+
naoTexto,
|
|
119
|
+
notas: [...notas],
|
|
120
|
+
bloqueios: bloqueios.length,
|
|
121
|
+
alertas: todos.filter((i) => i.nivel === 'alerta').length,
|
|
122
|
+
naoMedidos: todos.filter((i) => i.item === NAO_MEDIDO || i.item === NAO_VERIFICADO).length,
|
|
123
|
+
status: reais ? 'BLOQUEADA' : bloqueios.length ? 'AGUARDANDO_USUARIO' : 'OK',
|
|
124
|
+
};
|
|
49
125
|
}
|
|
50
126
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
for (let i = 0; i < argv.length; i++) {
|
|
82
|
-
const m = argv[i].match(/^--(crew|arquivo|formato)$/);
|
|
83
|
-
if (m && i + 1 < argv.length) args[m[1]] = argv[++i];
|
|
127
|
+
/**
|
|
128
|
+
* Verifica os arquivos de saída de uma crew.
|
|
129
|
+
* @param {object} o
|
|
130
|
+
* @param {string} o.raiz pasta do projeto (a que tem `_opencrew/`)
|
|
131
|
+
* @param {string} o.crew pasta da crew, dentro do projeto
|
|
132
|
+
* @param {Array<string|{ arquivo: string, formato?: string }>} o.arquivos caminho (sem formato
|
|
133
|
+
* declarado) ou `{ arquivo, formato }` (formato declarado)
|
|
134
|
+
* @param {string} [o.formato] limites de blog do item sem formato declarado que tem título no frontmatter
|
|
135
|
+
* @param {Function} [o.regraDeTeste] SÓ PARA TESTE: regra extra, chamada com `{ arquivo, formato,
|
|
136
|
+
* texto }` em cada arquivo de texto; serve para simular uma regra que lança erro
|
|
137
|
+
* @returns {Promise<object>} `{ arquivos, naoTexto, notas, bloqueios, alertas, naoMedidos, status }`
|
|
138
|
+
* · `arquivos`: `[{ arquivo, formato, itens, fecho }]`, só os de texto; cada item é
|
|
139
|
+
* `{ item, medido, limite, nivel, detalhe }`, com `nivel` bloqueio, alerta, ok ou null (linha
|
|
140
|
+
* "Não medido" sem nível); para a mesma entrada, arquivo + item + detalhe não mudam
|
|
141
|
+
* · `naoTexto`: caminhos que não são texto · `naoMedidos`: linhas "Não medido"/"Não verificado"
|
|
142
|
+
* · `status`: OK, BLOQUEADA ou AGUARDANDO_USUARIO
|
|
143
|
+
*/
|
|
144
|
+
export async function verificar({ raiz, crew, arquivos, formato = 'blog-post', regraDeTeste }) {
|
|
145
|
+
const entradas = semRepetidas(raiz, arquivos.map(normalizar));
|
|
146
|
+
const erro = erroDeUso({ raiz, crew, caminhos: entradas.map((e) => e.arquivo) });
|
|
147
|
+
if (erro) throw new Error(erro);
|
|
148
|
+
const ctx = await prepararContexto(raiz, crew, formato);
|
|
149
|
+
const resultado = [];
|
|
150
|
+
const naoTexto = [];
|
|
151
|
+
for (const entrada of entradas) {
|
|
152
|
+
const arquivo = nomeNoRelatorio(raiz, entrada.arquivo);
|
|
153
|
+
const r = await verificarArquivo(entrada, ctx, regraDeTeste)
|
|
154
|
+
.catch((e) => ({ itens: [naoVerificado(`erro ao verificar: ${e.message}`)], fecho: null }));
|
|
155
|
+
if (r) resultado.push({ arquivo, formato: entrada.formato, ...r });
|
|
156
|
+
else naoTexto.push(arquivo);
|
|
84
157
|
}
|
|
85
|
-
return
|
|
158
|
+
return resumir(resultado, naoTexto, ctx.notas);
|
|
86
159
|
}
|
|
87
160
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
161
|
+
/**
|
|
162
|
+
* @returns {Promise<number>} 0 = verificou (ao menos um caminho da lista existe) · 1 = erro de uso,
|
|
163
|
+
* ou erro que impediu a verificação inteira (uma linha em PT-BR, sem linha de status)
|
|
164
|
+
*/
|
|
91
165
|
export async function main(argv, { cwd = process.cwd(), escrever = (s) => process.stdout.write(`${s}\n`) } = {}) {
|
|
92
166
|
const args = lerArgs(argv);
|
|
93
|
-
|
|
94
|
-
|
|
167
|
+
const arquivos = args.arquivos.join(',').split(',').map((s) => s.trim()).filter(Boolean).map(lerItemDaLista);
|
|
168
|
+
const caminhos = arquivos.map((a) => a.arquivo ?? a);
|
|
169
|
+
const faltando = [...(args.crew ? [] : ['--crew']), ...(arquivos.length ? [] : ['--arquivo'])];
|
|
170
|
+
const erro = erroDeUso({ raiz: cwd, faltando, crew: args.crew, caminhos });
|
|
171
|
+
if (erro) {
|
|
172
|
+
escrever(erro);
|
|
173
|
+
if (faltando.length) escrever(USO);
|
|
95
174
|
return 1;
|
|
96
175
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (path.relative(cwd, abs).startsWith('..') || path.isAbsolute(path.relative(cwd, abs))) {
|
|
101
|
-
escrever(`Caminho fora do projeto: ${rel}`);
|
|
102
|
-
return 1;
|
|
103
|
-
}
|
|
176
|
+
if (!caminhos.some((c) => existsSync(path.resolve(cwd, c)))) {
|
|
177
|
+
for (const c of new Set(caminhos)) escrever(`Arquivo não encontrado: ${c}`);
|
|
178
|
+
return 1;
|
|
104
179
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
180
|
+
try {
|
|
181
|
+
escrever(formatarRelatorio(await verificar({ raiz: cwd, crew: args.crew, arquivos, formato: args.formato || 'blog-post' })));
|
|
182
|
+
return 0;
|
|
183
|
+
} catch (erroGeral) {
|
|
184
|
+
// Fora da lista de arquivos (memória da crew ou `company.md` ilegível): nada foi verificado.
|
|
185
|
+
escrever(`Não consegui verificar: ${erroGeral.message}`);
|
|
108
186
|
return 1;
|
|
109
187
|
}
|
|
110
|
-
const r = await verificar({ raiz: cwd, crew: args.crew, arquivos, formato: args.formato || 'blog-post' });
|
|
111
|
-
escrever(formatarRelatorio(r));
|
|
112
|
-
return 0;
|
|
113
188
|
}
|
|
114
189
|
|
|
115
|
-
|
|
116
|
-
if (isMain) {
|
|
190
|
+
if (ehPrincipal(import.meta.url)) {
|
|
117
191
|
main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
|
|
118
192
|
}
|