@aksp/opencrew 1.7.1 → 1.9.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.
- package/CHANGELOG.md +129 -0
- package/README.md +87 -4
- package/package.json +1 -1
- package/src/commands/update.js +9 -3
- package/src/lib/blocos.js +7 -3
- package/src/lib/resumo.js +3 -0
- package/templates/AGENTS.md +4 -0
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/prompts/discovery.prompt.md +1 -1
- package/templates/_opencrew/core/prompts/entrega.prompt.md +205 -0
- package/templates/_opencrew/core/prompts/export.prompt.md +5 -81
- package/templates/_opencrew/core/runner.pipeline.md +32 -23
- package/templates/_opencrew/core/scripts/entrega/argumentos.mjs +50 -0
- package/templates/_opencrew/core/scripts/entrega/canais.mjs +53 -0
- package/templates/_opencrew/core/scripts/entrega/comparar.mjs +104 -0
- package/templates/_opencrew/core/scripts/entrega/copia.mjs +138 -0
- package/templates/_opencrew/core/scripts/entrega/destino.mjs +98 -0
- package/templates/_opencrew/core/scripts/entrega/fora.mjs +50 -0
- package/templates/_opencrew/core/scripts/entrega/gravar.mjs +83 -0
- package/templates/_opencrew/core/scripts/entrega/guardar.mjs +60 -0
- package/templates/_opencrew/core/scripts/entrega/leiame.mjs +125 -0
- package/templates/_opencrew/core/scripts/entrega/leitor.mjs +133 -0
- package/templates/_opencrew/core/scripts/entrega/lembrar.mjs +65 -0
- package/templates/_opencrew/core/scripts/entrega/longas.mjs +109 -0
- package/templates/_opencrew/core/scripts/entrega/nomes.mjs +78 -0
- package/templates/_opencrew/core/scripts/entrega/passos.mjs +104 -0
- package/templates/_opencrew/core/scripts/entrega/pendencias.mjs +97 -0
- package/templates/_opencrew/core/scripts/entrega/ressalvas.mjs +78 -0
- package/templates/_opencrew/core/scripts/entrega/resumo.mjs +29 -0
- package/templates/_opencrew/core/scripts/entrega/retrato.mjs +42 -0
- package/templates/_opencrew/core/scripts/entrega/separar.mjs +106 -0
- package/templates/_opencrew/core/scripts/entrega/texto.mjs +95 -0
- package/templates/_opencrew/core/scripts/entregar.mjs +166 -0
- package/templates/_opencrew/core/scripts/verificar/argumentos.mjs +4 -4
- package/templates/_opencrew/core/scripts/verificar/entradas.mjs +26 -0
- package/templates/_opencrew/core/scripts/verificar/gravacao.mjs +41 -0
- package/templates/_opencrew/core/scripts/verificar/pecas.mjs +4 -4
- package/templates/_opencrew/core/scripts/verificar/relatorio.mjs +10 -3
- package/templates/_opencrew/core/scripts/verificar/secoes.mjs +22 -2
- package/templates/_opencrew/core/scripts/verificar.mjs +21 -29
- package/templates/gitignore +1 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Texto da entrega: o que sai do markdown num arquivo para colar, o que é bloco de serviço e as
|
|
2
|
+
// seções de rótulo de um corpo. As expressões rodam linha a linha, sobre texto do usuário.
|
|
3
|
+
// Spec: fase-u3a-entrega-por-canal.md, regra 6, citada pela fatia 1 (repositório do OpenCrew).
|
|
4
|
+
import { rotuloDe } from '../verificar/secoes.mjs';
|
|
5
|
+
import { semBom } from '../verificar/leitura.mjs';
|
|
6
|
+
|
|
7
|
+
/** Sem o BOM e com LF. */
|
|
8
|
+
export const emLf = (texto) => semBom(texto).replace(/\r\n?/g, '\n');
|
|
9
|
+
|
|
10
|
+
/** Texto em markdown que vai para um arquivo: aparado, com uma quebra de linha no fim. */
|
|
11
|
+
export function fecharMd(texto) {
|
|
12
|
+
const aparado = texto.trim();
|
|
13
|
+
return aparado ? `${aparado}\n` : '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Texto para colar: sem espaço no fim das linhas, sem linhas em branco seguidas, aparado. */
|
|
17
|
+
export const fechar = (texto) => fecharMd(texto.replace(/[ \t]+$/gm, '').replace(/\n{3,}/g, '\n\n'));
|
|
18
|
+
|
|
19
|
+
/** Sem os comentários HTML (`<!-- … -->`); o que não fecha fica como está. */
|
|
20
|
+
export function semComentarios(texto) {
|
|
21
|
+
let saida = '';
|
|
22
|
+
let i = 0;
|
|
23
|
+
for (;;) {
|
|
24
|
+
const abre = texto.indexOf('<!--', i);
|
|
25
|
+
const fecha = abre < 0 ? -1 : texto.indexOf('-->', abre + 4);
|
|
26
|
+
if (fecha < 0) return saida + texto.slice(i);
|
|
27
|
+
saida += texto.slice(i, abre);
|
|
28
|
+
i = fecha + 3;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Rótulo de bloco de serviço: termina em `NOTES`, `NOTE` ou `CHECKLIST`, ou é `FORMAT`. */
|
|
33
|
+
export const ehServico = (rotulo) => rotulo === 'FORMAT' || /(?:^|\s)(?:NOTES?|CHECKLIST)$/.test(rotulo);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Corpo sem os blocos de serviço: os comentários HTML e as seções de rótulo de serviço, cada uma
|
|
37
|
+
* até o próximo rótulo. As outras linhas de rótulo ficam.
|
|
38
|
+
*/
|
|
39
|
+
export function semServico(corpo) {
|
|
40
|
+
const linhas = [];
|
|
41
|
+
let fora = false;
|
|
42
|
+
for (const linha of semComentarios(corpo).split('\n')) {
|
|
43
|
+
const rotulo = rotuloDe(linha);
|
|
44
|
+
if (rotulo) fora = ehServico(rotulo);
|
|
45
|
+
if (!fora) linhas.push(linha);
|
|
46
|
+
}
|
|
47
|
+
return linhas.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** As seções de rótulo de um corpo, na ordem: `[{ rotulo, texto }]`; antes do primeiro, `rotulo` null. */
|
|
51
|
+
export function secoesDeRotulo(corpo) {
|
|
52
|
+
const secoes = [{ rotulo: null, linhas: [] }];
|
|
53
|
+
for (const linha of corpo.split('\n')) {
|
|
54
|
+
const rotulo = rotuloDe(linha);
|
|
55
|
+
if (rotulo) secoes.push({ rotulo, linhas: [] });
|
|
56
|
+
else secoes.at(-1).linhas.push(linha);
|
|
57
|
+
}
|
|
58
|
+
return secoes.map((s) => ({ rotulo: s.rotulo, texto: s.linhas.join('\n').trim() }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// O que nunca perde marcador: URL, e-mail, @usuário e #hashtag. Fica guardado enquanto a linha é
|
|
62
|
+
// limpa e volta depois, igual.
|
|
63
|
+
const PROTEGIDO = /(?:https?:\/\/|www\.|wa\.me\/)[^\s*]+|[\p{L}\p{N}._%+-]+@[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+|(?<![\p{L}\p{N}_])[@#][\p{L}\p{N}_]+/gu;
|
|
64
|
+
const [ABRE, FECHA] = [String.fromCharCode(0xe000), String.fromCharCode(0xe001)];
|
|
65
|
+
const GUARDADO = new RegExp(`${ABRE}(\\d+)${FECHA}`, 'g');
|
|
66
|
+
const LINK = /\[([^\]\n]+)\]\(\s*([^)\s]+)(?:\s+"[^"\n]*")?\s*\)/g;
|
|
67
|
+
// O marcador só sai em par e colado ao texto que marca; nunca no meio de palavra.
|
|
68
|
+
const NEGRITO = /\*\*(?=[^\s*])([^\n]+?)(?<=[^\s*])\*\*/g;
|
|
69
|
+
const MARCADORES = [
|
|
70
|
+
NEGRITO,
|
|
71
|
+
/(?<![\p{L}\p{N}_])__(?=[^\s_])([^\n]+?)(?<=[^\s_])__(?![\p{L}\p{N}_])/gu,
|
|
72
|
+
/(?<![\p{L}\p{N}*])\*(?=[^\s*])([^*\n]+?)(?<=[^\s*])\*(?![\p{L}\p{N}*])/gu,
|
|
73
|
+
/(?<![\p{L}\p{N}_])_(?=[^\s_])([^_\n]+?)(?<=[^\s_])_(?![\p{L}\p{N}_])/gu,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/** `[texto](url)` vira `texto: url` (ou só a URL); negrito e itálico saem. No WhatsApp, `**x**` vira `*x*`. */
|
|
77
|
+
function limparLinha(linha, whatsapp) {
|
|
78
|
+
const guardados = [];
|
|
79
|
+
const comLinks = linha.replace(LINK, (_, texto, url) => (texto.trim() === url ? url : `${texto}: ${url}`));
|
|
80
|
+
const mascarada = comLinks.replace(PROTEGIDO, (achado) => `${ABRE}${guardados.push(achado) - 1}${FECHA}`);
|
|
81
|
+
const limpa = whatsapp ? mascarada.replace(NEGRITO, '*$1*') : MARCADORES.reduce((t, rx) => t.replace(rx, '$1'), mascarada);
|
|
82
|
+
return limpa.replace(GUARDADO, (_, n) => guardados[Number(n)]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Texto pronto para colar: as mesmas palavras, na mesma ordem, sem as linhas de rótulo, sem a
|
|
87
|
+
* linha `---`, sem os `#` de título e sem a sintaxe de negrito, itálico e link. Marcador de lista
|
|
88
|
+
* e citação ficam.
|
|
89
|
+
* @param {string} texto
|
|
90
|
+
* @param {{ whatsapp?: boolean }} [opcoes] no WhatsApp, `*x*` e `_x_` ficam e `**x**` vira `*x*`
|
|
91
|
+
*/
|
|
92
|
+
export function paraColar(texto, { whatsapp = false } = {}) {
|
|
93
|
+
const linhas = emLf(texto).split('\n').filter((linha) => linha.trim() !== '---' && !rotuloDe(linha));
|
|
94
|
+
return fechar(linhas.map((linha) => limparLinha(linha.replace(/^#{1,6}\s+/, ''), whatsapp)).join('\n'));
|
|
95
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Entrega de uma execução: separa os arquivos aprovados por canal, com texto pronto para colar e
|
|
3
|
+
// um LEIA-ME que diz o que fazer com cada arquivo; e copia o que está pronto para a pasta do
|
|
4
|
+
// projeto que o usuário escolheu.
|
|
5
|
+
// Uso (na pasta do projeto): node _opencrew/core/scripts/entregar.mjs --crew "crews/<crew>"
|
|
6
|
+
// --run "<id>" --arquivo "<caminho=formato>[,<caminho=formato>…]" [--destino "<pasta>"]
|
|
7
|
+
// [--lembrar-destino "<pasta>"|nao] [--aceitar-pendencias] [--vai-publicar <canal>] [--ajuda]
|
|
8
|
+
// --arquivo: a lista do verificador (o "=formato" é opcional); entra o que estiver nela e existir.
|
|
9
|
+
// --destino: pasta do projeto que recebe a cópia, só nesta chamada (vale sobre o crew.yaml).
|
|
10
|
+
// --lembrar-destino: grava a pasta (ou "nao") em `entrega.destino` do crew.yaml e já vale agora.
|
|
11
|
+
// --aceitar-pendencias: as pendências deste momento viram ressalvas ("entregar assim mesmo"); a
|
|
12
|
+
// ressalva vale enquanto a pendência dela existir: a que some sai do `ressalvas.json`.
|
|
13
|
+
// --vai-publicar: canal que a crew publica sozinha (pode repetir); o LEIA-ME avisa.
|
|
14
|
+
// Grava em crews/<crew>/output/<run>/: a pasta `entrega/` (refeita do zero a cada chamada, montada
|
|
15
|
+
// em `entrega.tmp/`), `verificacao-entrega.md`, `ressalvas.json` (as pendências aceitas que ainda
|
|
16
|
+
// existem) e `copia.json` (o retrato do que foi copiado, para a comparação seguinte). Fora
|
|
17
|
+
// dali, só o combinado: a cópia em `<destino>/<run>/` (nunca por cima do que já está lá; se a
|
|
18
|
+
// entrega mudou, `<run>-reentrega-2`…) e, com --lembrar-destino, o crew.yaml (com crew.yaml.bak).
|
|
19
|
+
// Última linha da saída (o runner lê esta linha): ENTREGA:OK (nenhuma pendência),
|
|
20
|
+
// ENTREGA:COM_RESSALVA (toda pendência foi aceita) ou ENTREGA:INCOMPLETA (algum canal não está
|
|
21
|
+
// pronto, o destino foi recusado ou uma gravação falhou).
|
|
22
|
+
// Código de saída: 0 sempre que a linha ENTREGA: sai, e em --ajuda · 1 = erro de uso ou erro que
|
|
23
|
+
// impediu a entrega inteira; com código 1 não há linha ENTREGA: e nada é escrito.
|
|
24
|
+
// Specs: fase-u3a1-pasta-de-entrega.md e fase-u3a2-entrega-no-projeto.md (repositório do OpenCrew).
|
|
25
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { MSG as COMUM, erroDeUso, ehPrincipal, relativoAoProjeto } from './comum.mjs';
|
|
28
|
+
import { formatarRelatorio, verificar } from './verificar.mjs';
|
|
29
|
+
import { USO, lerArgs, lerLista, limpar, runValido } from './entrega/argumentos.mjs';
|
|
30
|
+
import { CANAIS, canalDoFormato, ehCanal, ehDeServico } from './entrega/canais.mjs';
|
|
31
|
+
import { MSG as DESTINO, escolherDestino, validarDestino } from './entrega/destino.mjs';
|
|
32
|
+
import { MSG as COPIA, guardar } from './entrega/guardar.mjs';
|
|
33
|
+
import { gravarEntrega } from './entrega/gravar.mjs';
|
|
34
|
+
import { montarLeiame } from './entrega/leiame.mjs';
|
|
35
|
+
import { lembrarDestino } from './entrega/lembrar.mjs';
|
|
36
|
+
import { nomear } from './entrega/nomes.mjs';
|
|
37
|
+
import { alertasDeTamanho, naoConferido, pendenciasPorPasta } from './entrega/pendencias.mjs';
|
|
38
|
+
import { ARQUIVO as RESSALVAS, MSG as RESSALVA, gravarRessalvas, lerRessalvas, separarPendencias } from './entrega/ressalvas.mjs';
|
|
39
|
+
import { resumo } from './entrega/resumo.mjs';
|
|
40
|
+
import { separar } from './entrega/separar.mjs';
|
|
41
|
+
|
|
42
|
+
const MSG = {
|
|
43
|
+
desconhecida: (opcao) => `Opção desconhecida: ${limpar(opcao)}.`,
|
|
44
|
+
semExecucao: (run, lista) => `Execução não encontrada: ${limpar(run)}. Execuções desta crew: ${lista.join(', ') || 'nenhuma'}.`,
|
|
45
|
+
nenhumArquivo: 'Nenhum arquivo da lista foi encontrado.',
|
|
46
|
+
deServico: (arquivo) => `${arquivo} é arquivo de serviço e não entra na entrega.`,
|
|
47
|
+
semCanal: (canal) => `Canal não encontrado nesta entrega: ${limpar(canal)}.`,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const ehPasta = (p) => existsSync(p) && statSync(p).isDirectory();
|
|
51
|
+
const tipoNoDisco = (p) => (!existsSync(p) ? 'ausente' : statSync(p).isDirectory() ? 'pasta' : 'arquivo');
|
|
52
|
+
const execucoes = (saida) => (ehPasta(saida) ? readdirSync(saida, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort() : []);
|
|
53
|
+
|
|
54
|
+
/** Erro de uso, na ordem da §6: `{ mensagem, comUso }`, ou null quando a chamada está certa. */
|
|
55
|
+
function erroDeChamada(raiz, args, pedidos) {
|
|
56
|
+
if (args.desconhecida) return { mensagem: MSG.desconhecida(args.desconhecida), comUso: true };
|
|
57
|
+
const faltando = [['--crew', args.crew], ['--run', args.run], ['--arquivo', pedidos.length]].filter(([, v]) => !v).map(([opcao]) => opcao);
|
|
58
|
+
const comum = erroDeUso({ raiz, faltando, crew: args.crew, caminhos: pedidos.map((p) => p.arquivo) });
|
|
59
|
+
if (comum) return { mensagem: comum, comUso: faltando.length > 0 };
|
|
60
|
+
const crew = path.resolve(raiz, args.crew);
|
|
61
|
+
if (!existsSync(path.join(crew, 'crew.yaml'))) return { mensagem: COMUM.crewNaoEncontrada(args.crew) };
|
|
62
|
+
const saida = path.join(crew, 'output');
|
|
63
|
+
if (!runValido(args.run) || !ehPasta(path.join(saida, args.run))) return { mensagem: MSG.semExecucao(args.run, execucoes(saida)) };
|
|
64
|
+
// O destino a lembrar é validado antes de qualquer escrita: recusado, o crew.yaml fica intacto.
|
|
65
|
+
const lembrar = args.lembrarDestino == null ? null : validarDestino(raiz, args.lembrarDestino);
|
|
66
|
+
return lembrar?.tipo === 'recusado' ? { mensagem: DESTINO.recusado(lembrar.valor) } : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Cada item da lista, uma vez: onde fica, se existe, se é de serviço e qual é o canal do formato. */
|
|
70
|
+
async function classificar(raiz, pedidos) {
|
|
71
|
+
const itens = [];
|
|
72
|
+
const vistos = new Set();
|
|
73
|
+
for (const { arquivo, formato } of pedidos) {
|
|
74
|
+
const abs = path.resolve(raiz, arquivo);
|
|
75
|
+
if (vistos.has(`${abs}|${formato ?? ''}`)) continue;
|
|
76
|
+
vistos.add(`${abs}|${formato ?? ''}`);
|
|
77
|
+
const rel = relativoAoProjeto(raiz, arquivo);
|
|
78
|
+
itens.push({ arquivo, formato, abs, rel, tipo: tipoNoDisco(abs), servico: ehDeServico(rel), canal: await canalDoFormato(raiz, formato) });
|
|
79
|
+
}
|
|
80
|
+
return itens;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Verifica na origem, separa por canal e monta tudo o que a entrega mostra; não escreve nada. */
|
|
84
|
+
async function montar(raiz, args, itens, aceitas) {
|
|
85
|
+
const arquivosDaLista = itens.map((i) => (i.formato ? { arquivo: i.arquivo, formato: i.formato } : i.arquivo));
|
|
86
|
+
const verificacao = await verificar({ raiz, crew: args.crew, arquivos: arquivosDaLista, semPadraoDeBlog: true });
|
|
87
|
+
const { arquivos, avisos } = nomear(await separar(itens));
|
|
88
|
+
const todas = pendenciasPorPasta(raiz, itens, verificacao);
|
|
89
|
+
const presentes = new Set([...arquivos.map((a) => a.pasta), ...todas.keys()]);
|
|
90
|
+
const dados = {
|
|
91
|
+
crew: path.basename(path.resolve(raiz, args.crew)), run: args.run, arquivos, avisos,
|
|
92
|
+
...separarPendencias(todas, aceitas, args.aceitar),
|
|
93
|
+
pastas: Object.keys(CANAIS).filter((c) => presentes.has(c)),
|
|
94
|
+
alertas: await alertasDeTamanho(raiz, itens, arquivos, verificacao),
|
|
95
|
+
naoConferido: naoConferido(raiz, itens, verificacao),
|
|
96
|
+
vaiPublicar: args.vaiPublicar,
|
|
97
|
+
};
|
|
98
|
+
return { dados, relatorio: `${formatarRelatorio(verificacao)}\n` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** O que a entrega grava, na ordem: crew.yaml, ressalvas, a cópia e, por último, `entrega/`. */
|
|
102
|
+
async function gravarTudo(raiz, args, validos, pastas) {
|
|
103
|
+
const falhas = [args.lembrarDestino == null ? null : await lembrarDestino(raiz, pastas.crew, validarDestino(raiz, args.lembrarDestino))];
|
|
104
|
+
const lidas = await lerRessalvas(pastas.execucao);
|
|
105
|
+
const { dados, relatorio } = await montar(raiz, args, validos, lidas.aceitas);
|
|
106
|
+
// Ilegível, o arquivo fica como está — só um aceite novo o regrava.
|
|
107
|
+
if (!lidas.ilegivel || (args.aceitar && dados.ressalvas.size)) falhas.push(await gravarRessalvas(pastas.execucao, dados.ressalvas));
|
|
108
|
+
const copia = await guardar(raiz, await escolherDestino(raiz, pastas.crew, args), dados, pastas.execucao);
|
|
109
|
+
const leiame = montarLeiame({ ...dados, copiaEm: copia.pasta });
|
|
110
|
+
const daEntrega = await gravarEntrega(pastas.execucao, dados.arquivos, { leiame, relatorio });
|
|
111
|
+
const naoGravados = falhas.filter(Boolean).map((f) => COPIA.falhaDeEscrita(relativoAoProjeto(raiz, f)));
|
|
112
|
+
return { dados, copia, lidas, naoGravados, daEntrega: daEntrega && COPIA.falhaDeEscrita(relativoAoProjeto(raiz, daEntrega)) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function linhaFinal(dados, incompleta) {
|
|
116
|
+
if (incompleta || dados.pendencias.size) return 'ENTREGA:INCOMPLETA';
|
|
117
|
+
return dados.ressalvas.size ? 'ENTREGA:COM_RESSALVA' : 'ENTREGA:OK';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function entregar(raiz, args, itens, escrever) {
|
|
121
|
+
const execucao = relativoAoProjeto(raiz, path.join(args.crew, 'output', args.run));
|
|
122
|
+
const pastas = { crew: path.resolve(raiz, args.crew), execucao: path.resolve(raiz, execucao) };
|
|
123
|
+
const r = await gravarTudo(raiz, args, itens.filter((i) => !i.servico), pastas);
|
|
124
|
+
if (r.daEntrega) {
|
|
125
|
+
for (const linha of [...r.naoGravados, r.daEntrega]) escrever(linha);
|
|
126
|
+
escrever('ENTREGA:INCOMPLETA');
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
const notas = [
|
|
130
|
+
...(r.lidas.ilegivel ? [RESSALVA.ilegivel(`${execucao}/${RESSALVAS}`)] : []), ...r.dados.novas,
|
|
131
|
+
...itens.filter((i) => i.servico).map((i) => MSG.deServico(i.rel)),
|
|
132
|
+
];
|
|
133
|
+
for (const linha of resumo(execucao, r.dados, notas, [...r.naoGravados, ...r.copia.linhas])) escrever(linha);
|
|
134
|
+
escrever(linhaFinal(r.dados, r.copia.falhou || r.naoGravados.length > 0));
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {string[]} argv
|
|
140
|
+
* @param {object} [deps] `cwd` (a pasta do projeto) e `escrever`
|
|
141
|
+
* @returns {Promise<number>} 0 = a linha `ENTREGA:` saiu (ou `--ajuda`) · 1 = erro de uso, ou erro
|
|
142
|
+
* que impediu a entrega inteira (uma linha em PT-BR, sem linha `ENTREGA:`, nada escrito)
|
|
143
|
+
*/
|
|
144
|
+
export async function main(argv, { cwd = process.cwd(), escrever = (s) => process.stdout.write(`${s}\n`) } = {}) {
|
|
145
|
+
const args = lerArgs(argv);
|
|
146
|
+
const sair = (...linhas) => linhas.forEach((l) => escrever(l)) ?? 1;
|
|
147
|
+
if (args.ajuda) return sair(USO) && 0;
|
|
148
|
+
const pedidos = lerLista(args.arquivos);
|
|
149
|
+
const erro = erroDeChamada(cwd, args, pedidos);
|
|
150
|
+
if (erro) return sair(erro.mensagem, ...(erro.comUso ? [USO] : []));
|
|
151
|
+
try {
|
|
152
|
+
const itens = await classificar(cwd, pedidos);
|
|
153
|
+
const validos = itens.filter((i) => !i.servico);
|
|
154
|
+
if (!validos.some((i) => i.tipo === 'arquivo')) return sair(MSG.nenhumArquivo);
|
|
155
|
+
const canais = new Set(validos.map((i) => i.canal));
|
|
156
|
+
const fora = args.vaiPublicar.find((canal) => !ehCanal(canal) || !canais.has(canal));
|
|
157
|
+
if (fora !== undefined) return sair(MSG.semCanal(fora));
|
|
158
|
+
return await entregar(cwd, args, itens, escrever);
|
|
159
|
+
} catch (erroGeral) {
|
|
160
|
+
return sair(`Não consegui montar a entrega: ${limpar(erroGeral?.message ?? erroGeral)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (ehPrincipal(import.meta.url)) {
|
|
165
|
+
main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
|
|
166
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Linha de comando do verificador: as opções, os itens `caminho=formato` e a linha de uso.
|
|
2
|
-
//
|
|
2
|
+
// Specs: fase-r1-reparos-1-6-1.md, §3, e fase-u3a2-entrega-no-projeto.md, §3 (`--relatorio`).
|
|
3
3
|
|
|
4
4
|
// Uma linha só, e sem o texto do status: em erro de uso a saída não tem linha de status.
|
|
5
|
-
export const USO = 'Uso: node _opencrew/core/scripts/verificar.mjs --crew crews/<nome> --arquivo "<caminho=formato>[,<caminho=formato>…]" [--formato blog-post|blog-seo]'
|
|
5
|
+
export const USO = 'Uso: node _opencrew/core/scripts/verificar.mjs --crew crews/<nome> --arquivo "<caminho=formato>[,<caminho=formato>…]" [--formato blog-post|blog-seo] [--relatorio "<caminho>"]'
|
|
6
6
|
+ ' — o "=formato" é opcional; o relatório termina em OK, BLOQUEADA ou AGUARDANDO_USUARIO; código de saída: 0 = verificou, 1 = erro de uso';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Opções da linha de comando: `{ crew, formato, arquivos }`. `--arquivo` repetido soma à lista, e
|
|
9
|
+
* Opções da linha de comando: `{ crew, formato, relatorio, arquivos }`. `--arquivo` repetido soma à lista, e
|
|
10
10
|
* o que vem solto logo depois da lista (sem `--`) é mais um item dela: nenhum arquivo citado fica
|
|
11
11
|
* sem verificação.
|
|
12
12
|
*/
|
|
@@ -14,7 +14,7 @@ export function lerArgs(argv) {
|
|
|
14
14
|
const args = { arquivos: [] };
|
|
15
15
|
let naLista = false;
|
|
16
16
|
for (let i = 0; i < argv.length; i++) {
|
|
17
|
-
const opcao = argv[i].match(/^--(crew|arquivo|formato)$/)?.[1];
|
|
17
|
+
const opcao = argv[i].match(/^--(crew|arquivo|formato|relatorio)$/)?.[1];
|
|
18
18
|
if (opcao && i + 1 < argv.length) {
|
|
19
19
|
if (opcao === 'arquivo') args.arquivos.push(argv[++i]);
|
|
20
20
|
else args[opcao] = argv[++i];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// As entradas da lista do verificador: cada item com o formato declarado, uma vez só, e o nome
|
|
2
|
+
// com que o arquivo aparece no relatório.
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { relativoAoProjeto } from '../comum.mjs';
|
|
5
|
+
|
|
6
|
+
/** Item da lista → `{ arquivo, formato }`; só o objeto traz formato declarado. */
|
|
7
|
+
export function normalizar(entrada) {
|
|
8
|
+
if (typeof entrada === 'string') return { arquivo: entrada, formato: null };
|
|
9
|
+
return { arquivo: entrada.arquivo, formato: entrada.formato || null };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** O mesmo arquivo citado duas vezes, com o mesmo formato, é verificado (e contado) uma vez só. */
|
|
13
|
+
export function semRepetidas(raiz, entradas) {
|
|
14
|
+
const vistas = new Set();
|
|
15
|
+
return entradas.filter((e) => {
|
|
16
|
+
const chave = `${path.resolve(raiz, e.arquivo)}|${e.formato ?? ''}`;
|
|
17
|
+
return !vistas.has(chave) && vistas.add(chave);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** No relatório, o absoluto de dentro do projeto (também por link, junção ou nome curto) sai como o relativo. */
|
|
22
|
+
export function nomeNoRelatorio(raiz, arquivo) {
|
|
23
|
+
const relativo = relativoAoProjeto(raiz, arquivo);
|
|
24
|
+
const comoEscrito = !path.isAbsolute(arquivo) && path.resolve(raiz, relativo) === path.resolve(raiz, arquivo);
|
|
25
|
+
return comoEscrito ? arquivo : relativo;
|
|
26
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// O relatório do ciclo gravado pelo próprio verificador (`--relatorio`): só dentro de
|
|
2
|
+
// `crews/<crew>/output/`, com nome `verificacao-*.md`, em pasta que já existe. O script não cria
|
|
3
|
+
// pasta e não grava em nenhum outro lugar.
|
|
4
|
+
// Spec: fase-u3a2-entrega-no-projeto.md, §3 e regra 35 (repositório do OpenCrew).
|
|
5
|
+
import { existsSync, promises as fs, statSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { realDentroDe } from '../comum.mjs';
|
|
8
|
+
|
|
9
|
+
const emUmaLinha = (valor) => String(valor).replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
10
|
+
|
|
11
|
+
export const MSG = {
|
|
12
|
+
invalido: (valor) => `O relatório só pode ser gravado em crews/<crew>/output/, com nome verificacao-….md. Recebi: ${emUmaLinha(valor)}.`,
|
|
13
|
+
naoGravou: (caminho) => `⚠️ Não consegui gravar o relatório em ${caminho}.`,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const NOME = /^verificacao-.*\.md$/i;
|
|
17
|
+
const ehPasta = (p) => existsSync(p) && statSync(p).isDirectory();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Onde o relatório pode ser gravado: o caminho absoluto, ou null quando o valor está fora da regra
|
|
21
|
+
* (fora de `crews/<crew>/output/`, pelo texto ou pelo lugar real; outro nome; pasta que não existe).
|
|
22
|
+
*/
|
|
23
|
+
export function caminhoDoRelatorio(raiz, crew, valor) {
|
|
24
|
+
if (!String(valor).trim()) return null;
|
|
25
|
+
const saida = path.resolve(raiz, crew, 'output');
|
|
26
|
+
const alvo = path.resolve(raiz, valor);
|
|
27
|
+
const rel = path.relative(saida, alvo);
|
|
28
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel) || !NOME.test(path.basename(alvo))) return null;
|
|
29
|
+
const pasta = path.dirname(alvo);
|
|
30
|
+
return ehPasta(pasta) && realDentroDe(saida, pasta) ? alvo : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Grava o que o script imprime (o relatório e a quebra de linha do fim). @returns {Promise<boolean>} gravou? */
|
|
34
|
+
export async function gravarRelatorio(alvo, relatorio) {
|
|
35
|
+
try {
|
|
36
|
+
await fs.writeFile(alvo, `${relatorio}\n`, 'utf8');
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -12,7 +12,7 @@ export const FORMATOS_DA_TABELA = [...BLOG, 'instagram-feed', 'linkedin-post', '
|
|
|
12
12
|
const FORMATO_DA_PECA = { legenda: 'instagram-feed', slides: 'instagram-feed', post: 'linkedin-post', tweet: 'twitter-post' };
|
|
13
13
|
// O que cada rótulo faz, por formato declarado. Sem formato declarado valem todos: os
|
|
14
14
|
// inconfundíveis abrem peça e os outros só se juntam a uma peça já aberta.
|
|
15
|
-
const ROTULOS = {
|
|
15
|
+
export const ROTULOS = {
|
|
16
16
|
'instagram-feed': { CAPTION: 'legenda', SLIDES: 'slides', HASHTAGS: 'hashtags' },
|
|
17
17
|
'linkedin-post': { HOOK: 'post', BODY: 'parte', INSIGHTS: 'parte', CTA: 'parte', HASHTAGS: 'hashtags' },
|
|
18
18
|
'twitter-post': { TWEET: 'tweet' },
|
|
@@ -159,8 +159,8 @@ function fechar(entradas) {
|
|
|
159
159
|
* Lê as peças de um texto, na ordem em que aparecem.
|
|
160
160
|
* @param {string} texto conteúdo de um arquivo `.md` ou `.txt`, já sem o BOM
|
|
161
161
|
* @param {string|null} [formato] formato declarado (`caminho=formato`); null = sem formato declarado
|
|
162
|
-
* @param {{ formatoDeBlog?: string }} [opcoes] sem formato declarado, o formato em que o
|
|
163
|
-
* frontmatter é medido (`--formato`)
|
|
162
|
+
* @param {{ formatoDeBlog?: string|null }} [opcoes] sem formato declarado, o formato em que o
|
|
163
|
+
* título do frontmatter é medido (`--formato`); null = não é peça (a entrega chama assim)
|
|
164
164
|
* @returns {object[]} peças `{ tipo, formato, texto, origem, cabecalho, ordem }`
|
|
165
165
|
* · `tipo`: titulo, meta, legenda, hashtags, slides, post ou tweet
|
|
166
166
|
* · `formato`: o formato cujo limite vale para a peça
|
|
@@ -180,7 +180,7 @@ export function lerPecas(texto, formato = null, { formatoDeBlog = 'blog-post' }
|
|
|
180
180
|
if (BLOG.includes(tabela)) pecasDoBlog(e, texto, trechos, tabela, true);
|
|
181
181
|
else {
|
|
182
182
|
// Fora da tabela os rótulos não são lidos; sem formato declarado, título no frontmatter é blog.
|
|
183
|
-
if (formato == null) pecasDoBlog(e, texto, trechos, formatoDeBlog, false);
|
|
183
|
+
if (formato == null && formatoDeBlog) pecasDoBlog(e, texto, trechos, formatoDeBlog, false);
|
|
184
184
|
const rotulos = tabela ? ROTULOS[tabela] : formato == null ? TODOS_OS_ROTULOS : {};
|
|
185
185
|
for (const trecho of trechos) pecasDoTrecho(e, trecho, rotulos);
|
|
186
186
|
pecaDoArquivoInteiro(e, marcas);
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Relatório do verificador em markdown (PT-BR). A última linha é o status que o runner lê.
|
|
2
|
-
|
|
2
|
+
// [PREENCHER] sai como "A preencher", contado à parte: bloqueia como antes, mas só o usuário resolve
|
|
3
|
+
// (fase-u3a2-entrega-no-projeto.md, regra 36). Sem [PREENCHER], o relatório é o de sempre.
|
|
4
|
+
import { FALTA_INFO, NAO_MEDIDO, NAO_VERIFICADO } from './regras.mjs';
|
|
3
5
|
|
|
4
6
|
const ROTULO = { bloqueio: '❌ Bloqueio', alerta: '⚠️ Alerta', ok: '✅ OK' };
|
|
7
|
+
const A_PREENCHER = '✏️ A preencher';
|
|
5
8
|
const plural = (n, um, varios) => `${n} ${n === 1 ? um : varios}`;
|
|
6
9
|
const ehNaoMedido = (i) => i.item === NAO_MEDIDO || i.item === NAO_VERIFICADO;
|
|
7
10
|
|
|
@@ -14,7 +17,7 @@ function tabela(medidos) {
|
|
|
14
17
|
/** "Não medido" e "Não verificado" começam por ⚠️ quando são alerta e por ⚪ quando não têm nível. */
|
|
15
18
|
function linhaDoAchado(i) {
|
|
16
19
|
if (ehNaoMedido(i)) return `- ${i.nivel === 'alerta' ? '⚠️' : '⚪'} ${i.item} — ${i.detalhe}`;
|
|
17
|
-
return `- ${ROTULO[i.nivel]} — ${i.item}: "${i.detalhe}"`;
|
|
20
|
+
return `- ${i.item === FALTA_INFO ? A_PREENCHER : ROTULO[i.nivel]} — ${i.item}: "${i.detalhe}"`;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
function secaoDoArquivo(a) {
|
|
@@ -30,7 +33,11 @@ export function formatarRelatorio(r) {
|
|
|
30
33
|
const naoTexto = r.naoTexto ?? [];
|
|
31
34
|
const notas = [...r.notas];
|
|
32
35
|
if (naoTexto.length) notas.push(`⚪ ${NAO_VERIFICADO} — não é texto (${naoTexto.length}): ${naoTexto.join(', ')}`);
|
|
33
|
-
const
|
|
36
|
+
const aPreencher = r.aPreencher ?? 0;
|
|
37
|
+
const resumo = [
|
|
38
|
+
plural(r.bloqueios - aPreencher, 'bloqueio', 'bloqueios'), ...(aPreencher ? [`${aPreencher} a preencher`] : []),
|
|
39
|
+
plural(r.alertas, 'alerta', 'alertas'), plural(r.naoMedidos ?? 0, 'não medido', 'não medidos'),
|
|
40
|
+
];
|
|
34
41
|
return [
|
|
35
42
|
'## Verificação automática',
|
|
36
43
|
'',
|
|
@@ -110,18 +110,36 @@ function abrir(trechos, trecho) {
|
|
|
110
110
|
return trechos.at(-1);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Peças a que algum cabeçalho de baixo do cabeçalho de nível 1 da posição `i` é candidato; a
|
|
115
|
+
* seção dele vai até o próximo de nível 1 ou até um rótulo.
|
|
116
|
+
*/
|
|
117
|
+
function tiposSobOTitulo(marcas, i, formato) {
|
|
118
|
+
const tipos = new Set();
|
|
119
|
+
let pilha = [marcas[i]];
|
|
120
|
+
for (let j = i + 1; j < marcas.length && !marcas[j].rotulo && marcas[j].nivel !== 1; j++) {
|
|
121
|
+
if (!marcas[j].nivel) continue;
|
|
122
|
+
pilha = pilha.filter((c) => c.nivel < marcas[j].nivel);
|
|
123
|
+
for (const tipo of candidaturas(marcas[j].norm, pilha.map((c) => c.norm), formato)) tipos.add(tipo);
|
|
124
|
+
pilha.push(marcas[j]);
|
|
125
|
+
}
|
|
126
|
+
return tipos;
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
/**
|
|
114
130
|
* Percorre as marcas e devolve os trechos, na ordem do texto: `{ rotulo, linhas }` para cada
|
|
115
131
|
* linha de rótulo e `{ titulo, tipos, canal, linhas }` para cada cabeçalho candidato a peça
|
|
116
132
|
* (`canal`: na seção de hashtags, o formato do canal que o cabeçalho cita; senão null).
|
|
117
133
|
* A seção de cabeçalho vai até o próximo cabeçalho do mesmo nível ou acima, ou até um rótulo; a
|
|
118
134
|
* de rótulo, até o próximo rótulo ou até um cabeçalho candidato (regra 2).
|
|
135
|
+
* O cabeçalho de nível 1 que tem, abaixo dele, outro candidato à mesma peça é o título do
|
|
136
|
+
* arquivo: não abre essa peça (fase-u3a1-pasta-de-entrega.md, regra 4).
|
|
119
137
|
*/
|
|
120
138
|
export function lerTrechos(marcas, formato) {
|
|
121
139
|
const trechos = [];
|
|
122
140
|
let pilha = []; // cabeçalhos abertos, do mais externo ao mais interno
|
|
123
141
|
let rotulo = null; // seção de rótulo aberta
|
|
124
|
-
for (const m of marcas) {
|
|
142
|
+
for (const [i, m] of marcas.entries()) {
|
|
125
143
|
if (m.rotulo) {
|
|
126
144
|
pilha = [];
|
|
127
145
|
rotulo = abrir(trechos, { rotulo: m.rotulo });
|
|
@@ -129,7 +147,9 @@ export function lerTrechos(marcas, formato) {
|
|
|
129
147
|
}
|
|
130
148
|
if (m.nivel) {
|
|
131
149
|
pilha = pilha.filter((c) => c.nivel < m.nivel);
|
|
132
|
-
const
|
|
150
|
+
const todos = candidaturas(m.norm, pilha.map((c) => c.norm), formato);
|
|
151
|
+
const abaixo = m.nivel === 1 && todos.length ? tiposSobOTitulo(marcas, i, formato) : null;
|
|
152
|
+
const tipos = abaixo ? todos.filter((tipo) => !abaixo.has(tipo)) : todos;
|
|
133
153
|
const canal = tipos[0] === 'hashtags' ? canalDe(m.norm) : null;
|
|
134
154
|
const trecho = tipos.length ? abrir(trechos, { titulo: m.titulo, tipos, canal }) : null;
|
|
135
155
|
pilha.push({ nivel: m.nivel, norm: m.norm, trecho });
|
|
@@ -1,9 +1,10 @@
|
|
|
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 "<caminho=formato>[,<caminho=formato>…]" [--formato blog-post|blog-seo]
|
|
3
|
+
// Uso: node _opencrew/core/scripts/verificar.mjs --crew crews/<nome> --arquivo "<caminho=formato>[,<caminho=formato>…]" [--formato blog-post|blog-seo] [--relatorio "<caminho>"]
|
|
4
4
|
// caminho=formato: o formato declarado de cada arquivo (o `format:` do passo que o gerou); o
|
|
5
5
|
// "=formato" é opcional. --formato só escolhe os limites de blog do item sem "=formato" que tem
|
|
6
|
-
// título no frontmatter.
|
|
6
|
+
// título no frontmatter. --relatorio grava no arquivo dado (crews/<crew>/output/…/verificacao-*.md)
|
|
7
|
+
// exatamente o que o script imprime.
|
|
7
8
|
// Os limites vêm do frontmatter `constraints:` dos best-practices (fonte única).
|
|
8
9
|
// Última linha da saída (o runner lê esta linha): VERIFICACAO:OK, VERIFICACAO:BLOQUEADA ou
|
|
9
10
|
// VERIFICACAO:AGUARDANDO_USUARIO (os únicos bloqueios são [PREENCHER], que só o usuário resolve).
|
|
@@ -11,19 +12,22 @@
|
|
|
11
12
|
// obrigatória faltando, pasta sem `_opencrew/`, crew inexistente, crew ou caminho fora do
|
|
12
13
|
// projeto, nenhum caminho da lista existe) ou erro que impediu a verificação inteira; com
|
|
13
14
|
// código 1 não há linha VERIFICACAO:.
|
|
14
|
-
// Specs: fase-u1-revisor-com-dentes.md, fase-r1-reparos-1-6-1.md
|
|
15
|
-
// (regra 23: "dentro do projeto" pelo texto ou pelo lugar real)
|
|
15
|
+
// Specs: fase-u1-revisor-com-dentes.md, fase-r1-reparos-1-6-1.md, fase-r2-update-e-envio-seguros.md
|
|
16
|
+
// (regra 23: "dentro do projeto" pelo texto ou pelo lugar real) e fase-u3a2-entrega-no-projeto.md
|
|
17
|
+
// (regras 35 e 36), no repositório do OpenCrew.
|
|
16
18
|
import { existsSync } from 'node:fs';
|
|
17
19
|
import path from 'node:path';
|
|
18
20
|
import { erroDeUso, ehPrincipal, relativoAoProjeto } from './comum.mjs';
|
|
19
21
|
import { USO, lerArgs, lerItemDaLista } from './verificar/argumentos.mjs';
|
|
20
22
|
import { lerItem } from './verificar/arquivos.mjs';
|
|
23
|
+
import { nomeNoRelatorio, normalizar, semRepetidas } from './verificar/entradas.mjs';
|
|
21
24
|
import { lerLimites, lerDominioDoSite, semFrontmatter } from './verificar/leitura.mjs';
|
|
22
25
|
import { lerProibicoes } from './verificar/proibicoes.mjs';
|
|
23
26
|
import { lerPecas } from './verificar/pecas.mjs';
|
|
24
27
|
import { medirPecas } from './verificar/medicao.mjs';
|
|
25
28
|
import { regrasGerais, temVariavel, item, FALTA_INFO, NAO_MEDIDO, NAO_VERIFICADO } from './verificar/regras.mjs';
|
|
26
29
|
import { formatarRelatorio } from './verificar/relatorio.mjs';
|
|
30
|
+
import { MSG as GRAVACAO, caminhoDoRelatorio, gravarRelatorio } from './verificar/gravacao.mjs';
|
|
27
31
|
|
|
28
32
|
export { formatarRelatorio };
|
|
29
33
|
|
|
@@ -88,27 +92,6 @@ async function verificarArquivo({ arquivo, formato }, ctx, regraDeTeste) {
|
|
|
88
92
|
return { itens, fecho: fechoDe(itens, medidas, formato) };
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
/** Item da lista → `{ arquivo, formato }`; só o objeto traz formato declarado. */
|
|
92
|
-
function normalizar(entrada) {
|
|
93
|
-
if (typeof entrada === 'string') return { arquivo: entrada, formato: null };
|
|
94
|
-
return { arquivo: entrada.arquivo, formato: entrada.formato || null };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** O mesmo arquivo citado duas vezes, com o mesmo formato, é verificado (e contado) uma vez só. */
|
|
98
|
-
function semRepetidas(raiz, entradas) {
|
|
99
|
-
const vistas = new Set();
|
|
100
|
-
return entradas.filter((e) => {
|
|
101
|
-
const chave = `${path.resolve(raiz, e.arquivo)}|${e.formato ?? ''}`;
|
|
102
|
-
return !vistas.has(chave) && vistas.add(chave);
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** No relatório, o absoluto de dentro do projeto (também por link, junção ou nome curto) sai como o relativo. */
|
|
107
|
-
function nomeNoRelatorio(raiz, arquivo) {
|
|
108
|
-
const relativo = relativoAoProjeto(raiz, arquivo);
|
|
109
|
-
const comoEscrito = !path.isAbsolute(arquivo) && path.resolve(raiz, relativo) === path.resolve(raiz, arquivo);
|
|
110
|
-
return comoEscrito ? arquivo : relativo;
|
|
111
|
-
}
|
|
112
95
|
|
|
113
96
|
function resumir(arquivos, naoTexto, notas) {
|
|
114
97
|
const todos = arquivos.flatMap((a) => a.itens);
|
|
@@ -121,6 +104,7 @@ function resumir(arquivos, naoTexto, notas) {
|
|
|
121
104
|
naoTexto,
|
|
122
105
|
notas: [...notas],
|
|
123
106
|
bloqueios: bloqueios.length,
|
|
107
|
+
aPreencher: bloqueios.length - reais,
|
|
124
108
|
alertas: todos.filter((i) => i.nivel === 'alerta').length,
|
|
125
109
|
naoMedidos: todos.filter((i) => i.item === NAO_MEDIDO || i.item === NAO_VERIFICADO).length,
|
|
126
110
|
status: reais ? 'BLOQUEADA' : bloqueios.length ? 'AGUARDANDO_USUARIO' : 'OK',
|
|
@@ -135,20 +119,23 @@ function resumir(arquivos, naoTexto, notas) {
|
|
|
135
119
|
* @param {Array<string|{ arquivo: string, formato?: string }>} o.arquivos caminho (sem formato
|
|
136
120
|
* declarado) ou `{ arquivo, formato }` (formato declarado)
|
|
137
121
|
* @param {string} [o.formato] limites de blog do item sem formato declarado que tem título no frontmatter
|
|
122
|
+
* @param {boolean} [o.semPadraoDeBlog] só a entrega passa true: no item sem formato declarado, o
|
|
123
|
+
* título do frontmatter não é medido como blog (fase-u3a1-pasta-de-entrega.md, regra 17)
|
|
138
124
|
* @param {Function} [o.regraDeTeste] SÓ PARA TESTE: regra extra, chamada com `{ arquivo, formato,
|
|
139
125
|
* texto }` em cada arquivo de texto; serve para simular uma regra que lança erro
|
|
140
|
-
* @returns {Promise<object>} `{ arquivos, naoTexto, notas, bloqueios, alertas, naoMedidos,
|
|
126
|
+
* @returns {Promise<object>} `{ arquivos, naoTexto, notas, bloqueios, aPreencher, alertas, naoMedidos,
|
|
127
|
+
* status }` · `aPreencher`: quantos dos `bloqueios` são [PREENCHER]
|
|
141
128
|
* · `arquivos`: `[{ arquivo, formato, itens, fecho }]`, só os de texto; cada item é
|
|
142
129
|
* `{ item, medido, limite, nivel, detalhe }`, com `nivel` bloqueio, alerta, ok ou null (linha
|
|
143
130
|
* "Não medido" sem nível); para a mesma entrada, arquivo + item + detalhe não mudam
|
|
144
131
|
* · `naoTexto`: caminhos que não são texto · `naoMedidos`: linhas "Não medido"/"Não verificado"
|
|
145
132
|
* · `status`: OK, BLOQUEADA ou AGUARDANDO_USUARIO
|
|
146
133
|
*/
|
|
147
|
-
export async function verificar({ raiz, crew, arquivos, formato = 'blog-post', regraDeTeste }) {
|
|
134
|
+
export async function verificar({ raiz, crew, arquivos, formato = 'blog-post', semPadraoDeBlog = false, regraDeTeste }) {
|
|
148
135
|
const entradas = semRepetidas(raiz, arquivos.map(normalizar));
|
|
149
136
|
const erro = erroDeUso({ raiz, crew, caminhos: entradas.map((e) => e.arquivo) });
|
|
150
137
|
if (erro) throw new Error(erro);
|
|
151
|
-
const ctx = await prepararContexto(raiz, crew, formato);
|
|
138
|
+
const ctx = await prepararContexto(raiz, crew, semPadraoDeBlog ? null : formato);
|
|
152
139
|
const resultado = [];
|
|
153
140
|
const naoTexto = [];
|
|
154
141
|
for (const entrada of entradas) {
|
|
@@ -176,12 +163,17 @@ export async function main(argv, { cwd = process.cwd(), escrever = (s) => proces
|
|
|
176
163
|
if (faltando.length) escrever(USO);
|
|
177
164
|
return 1;
|
|
178
165
|
}
|
|
166
|
+
const alvo = args.relatorio == null ? null : caminhoDoRelatorio(cwd, args.crew, args.relatorio);
|
|
167
|
+
if (args.relatorio != null && !alvo) return escrever(GRAVACAO.invalido(args.relatorio)) ?? 1;
|
|
179
168
|
if (!caminhos.some((c) => existsSync(path.resolve(cwd, c)))) {
|
|
180
169
|
for (const c of new Set(caminhos)) escrever(`Arquivo não encontrado: ${c}`);
|
|
181
170
|
return 1;
|
|
182
171
|
}
|
|
183
172
|
try {
|
|
184
|
-
|
|
173
|
+
const relatorio = formatarRelatorio(await verificar({ raiz: cwd, crew: args.crew, arquivos, formato: args.formato || 'blog-post' }));
|
|
174
|
+
// O relatório sai na tela mesmo quando o arquivo não pôde ser gravado.
|
|
175
|
+
if (alvo && !(await gravarRelatorio(alvo, relatorio))) escrever(GRAVACAO.naoGravou(relativoAoProjeto(cwd, alvo)));
|
|
176
|
+
escrever(relatorio);
|
|
185
177
|
return 0;
|
|
186
178
|
} catch (erroGeral) {
|
|
187
179
|
// Fora da lista de arquivos (memória da crew ou `company.md` ilegível): nada foi verificado.
|
package/templates/gitignore
CHANGED