@aksp/opencrew 1.8.0 → 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.
Files changed (35) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +45 -12
  3. package/package.json +1 -1
  4. package/src/commands/update.js +9 -3
  5. package/src/lib/blocos.js +7 -3
  6. package/src/lib/resumo.js +3 -0
  7. package/templates/AGENTS.md +1 -0
  8. package/templates/_opencrew/.opencrew-version +1 -1
  9. package/templates/_opencrew/core/prompts/discovery.prompt.md +1 -1
  10. package/templates/_opencrew/core/prompts/entrega.prompt.md +90 -16
  11. package/templates/_opencrew/core/prompts/export.prompt.md +5 -81
  12. package/templates/_opencrew/core/runner.pipeline.md +20 -20
  13. package/templates/_opencrew/core/scripts/entrega/argumentos.mjs +13 -9
  14. package/templates/_opencrew/core/scripts/entrega/canais.mjs +5 -3
  15. package/templates/_opencrew/core/scripts/entrega/comparar.mjs +104 -0
  16. package/templates/_opencrew/core/scripts/entrega/copia.mjs +138 -0
  17. package/templates/_opencrew/core/scripts/entrega/destino.mjs +98 -0
  18. package/templates/_opencrew/core/scripts/entrega/fora.mjs +4 -3
  19. package/templates/_opencrew/core/scripts/entrega/guardar.mjs +60 -0
  20. package/templates/_opencrew/core/scripts/entrega/leiame.mjs +52 -16
  21. package/templates/_opencrew/core/scripts/entrega/leitor.mjs +16 -3
  22. package/templates/_opencrew/core/scripts/entrega/lembrar.mjs +65 -0
  23. package/templates/_opencrew/core/scripts/entrega/passos.mjs +10 -5
  24. package/templates/_opencrew/core/scripts/entrega/pendencias.mjs +16 -6
  25. package/templates/_opencrew/core/scripts/entrega/ressalvas.mjs +78 -0
  26. package/templates/_opencrew/core/scripts/entrega/resumo.mjs +29 -0
  27. package/templates/_opencrew/core/scripts/entrega/retrato.mjs +42 -0
  28. package/templates/_opencrew/core/scripts/entrega/separar.mjs +6 -3
  29. package/templates/_opencrew/core/scripts/entregar.mjs +64 -39
  30. package/templates/_opencrew/core/scripts/verificar/argumentos.mjs +4 -4
  31. package/templates/_opencrew/core/scripts/verificar/entradas.mjs +26 -0
  32. package/templates/_opencrew/core/scripts/verificar/gravacao.mjs +41 -0
  33. package/templates/_opencrew/core/scripts/verificar/relatorio.mjs +10 -3
  34. package/templates/_opencrew/core/scripts/verificar.mjs +17 -27
  35. package/templates/gitignore +1 -0
@@ -1,28 +1,43 @@
1
1
  #!/usr/bin/env node
2
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.
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.
4
5
  // Uso (na pasta do projeto): node _opencrew/core/scripts/entregar.mjs --crew "crews/<crew>"
5
- // --run "<id>" --arquivo "<caminho=formato>[,<caminho=formato>…]" [--vai-publicar <canal>] [--ajuda]
6
+ // --run "<id>" --arquivo "<caminho=formato>[,<caminho=formato>…]" [--destino "<pasta>"]
7
+ // [--lembrar-destino "<pasta>"|nao] [--aceitar-pendencias] [--vai-publicar <canal>] [--ajuda]
6
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`.
7
13
  // --vai-publicar: canal que a crew publica sozinha (pode repetir); o LEIA-ME avisa.
8
- // Grava só em crews/<crew>/output/<run>/: a pasta `entrega/` (refeita do zero a cada chamada,
9
- // montada em `entrega.tmp/`) e, ao lado, `verificacao-entrega.md`.
10
- // Última linha da saída (o runner lê esta linha): ENTREGA:OK (nenhuma pendência) ou
11
- // ENTREGA:INCOMPLETA (algum canal não está pronto, ou a gravação falhou).
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).
12
22
  // Código de saída: 0 sempre que a linha ENTREGA: sai, e em --ajuda · 1 = erro de uso ou erro que
13
23
  // impediu a entrega inteira; com código 1 não há linha ENTREGA: e nada é escrito.
14
- // Spec: fase-u3a1-pasta-de-entrega.md (repositório do OpenCrew).
24
+ // Specs: fase-u3a1-pasta-de-entrega.md e fase-u3a2-entrega-no-projeto.md (repositório do OpenCrew).
15
25
  import { existsSync, readdirSync, statSync } from 'node:fs';
16
26
  import path from 'node:path';
17
27
  import { MSG as COMUM, erroDeUso, ehPrincipal, relativoAoProjeto } from './comum.mjs';
18
28
  import { formatarRelatorio, verificar } from './verificar.mjs';
19
29
  import { USO, lerArgs, lerLista, limpar, runValido } from './entrega/argumentos.mjs';
20
- import { CANAIS, OUTROS, canalDoFormato, ehCanal, ehDeServico } from './entrega/canais.mjs';
21
- import { separar } from './entrega/separar.mjs';
22
- import { nomear } from './entrega/nomes.mjs';
23
- import { alertasDeTamanho, frasesDePendencia, naoConferido, pendenciasPorPasta } from './entrega/pendencias.mjs';
24
- import { TEXTO, montarLeiame } from './entrega/leiame.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';
25
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';
26
41
 
27
42
  const MSG = {
28
43
  desconhecida: (opcao) => `Opção desconhecida: ${limpar(opcao)}.`,
@@ -30,7 +45,6 @@ const MSG = {
30
45
  nenhumArquivo: 'Nenhum arquivo da lista foi encontrado.',
31
46
  deServico: (arquivo) => `${arquivo} é arquivo de serviço e não entra na entrega.`,
32
47
  semCanal: (canal) => `Canal não encontrado nesta entrega: ${limpar(canal)}.`,
33
- falhaDeEscrita: (arquivo) => `Não consegui gravar ${arquivo}. Feche o arquivo, ou espere a sincronização da pasta, e rode de novo.`,
34
48
  };
35
49
 
36
50
  const ehPasta = (p) => existsSync(p) && statSync(p).isDirectory();
@@ -47,7 +61,9 @@ function erroDeChamada(raiz, args, pedidos) {
47
61
  if (!existsSync(path.join(crew, 'crew.yaml'))) return { mensagem: COMUM.crewNaoEncontrada(args.crew) };
48
62
  const saida = path.join(crew, 'output');
49
63
  if (!runValido(args.run) || !ehPasta(path.join(saida, args.run))) return { mensagem: MSG.semExecucao(args.run, execucoes(saida)) };
50
- return null;
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;
51
67
  }
52
68
 
53
69
  /** Cada item da lista, uma vez: onde fica, se existe, se é de serviço e qual é o canal do formato. */
@@ -65,48 +81,57 @@ async function classificar(raiz, pedidos) {
65
81
  }
66
82
 
67
83
  /** Verifica na origem, separa por canal e monta tudo o que a entrega mostra; não escreve nada. */
68
- async function montar(raiz, args, itens) {
84
+ async function montar(raiz, args, itens, aceitas) {
69
85
  const arquivosDaLista = itens.map((i) => (i.formato ? { arquivo: i.arquivo, formato: i.formato } : i.arquivo));
70
86
  const verificacao = await verificar({ raiz, crew: args.crew, arquivos: arquivosDaLista, semPadraoDeBlog: true });
71
87
  const { arquivos, avisos } = nomear(await separar(itens));
72
- const pendencias = pendenciasPorPasta(raiz, itens, verificacao);
73
- const presentes = new Set([...arquivos.map((a) => a.pasta), ...pendencias.keys()]);
88
+ const todas = pendenciasPorPasta(raiz, itens, verificacao);
89
+ const presentes = new Set([...arquivos.map((a) => a.pasta), ...todas.keys()]);
74
90
  const dados = {
75
- crew: path.basename(path.resolve(raiz, args.crew)), run: args.run, arquivos, avisos, pendencias,
91
+ crew: path.basename(path.resolve(raiz, args.crew)), run: args.run, arquivos, avisos,
92
+ ...separarPendencias(todas, aceitas, args.aceitar),
76
93
  pastas: Object.keys(CANAIS).filter((c) => presentes.has(c)),
77
94
  alertas: await alertasDeTamanho(raiz, itens, arquivos, verificacao),
78
95
  naoConferido: naoConferido(raiz, itens, verificacao),
79
96
  vaiPublicar: args.vaiPublicar,
80
97
  };
81
- return { dados, leiame: montarLeiame(dados), relatorio: `${formatarRelatorio(verificacao)}\n` };
98
+ return { dados, relatorio: `${formatarRelatorio(verificacao)}\n` };
82
99
  }
83
100
 
84
- /** O resumo da tela: a pasta, a situação de cada canal, o que falta, os avisos e o LEIA-ME. */
85
- function resumo(execucao, { crew, run, pastas, pendencias, avisos, alertas }, deServico) {
86
- const comPendencia = [...pastas, OUTROS].filter((p) => pendencias.has(p));
87
- const notas = [...alertas, ...deServico.map((i) => MSG.deServico(i.rel)), ...avisos.map((a) => a.tela ?? a.texto)];
88
- return [
89
- `Entrega da execução ${run} da crew ${crew}`,
90
- `Pasta: ${execucao}/entrega`,
91
- ...pastas.map((p) => `- ${CANAIS[p]}: ${pendencias.has(p) ? TEXTO.naoPronto : TEXTO.pronto}`),
92
- ...frasesDePendencia(comPendencia, pendencias).flatMap((frase, i) => [frase, ...pendencias.get(comPendencia[i]).map((l) => `- ${l}`)]),
93
- ...(notas.length ? ['Avisos:', ...notas.map((l) => `- ${l}`)] : []),
94
- `LEIA-ME: ${execucao}/entrega/LEIA-ME.md`,
95
- ];
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';
96
118
  }
97
119
 
98
120
  async function entregar(raiz, args, itens, escrever) {
99
- const validos = itens.filter((i) => !i.servico);
100
121
  const execucao = relativoAoProjeto(raiz, path.join(args.crew, 'output', args.run));
101
- const { dados, leiame, relatorio } = await montar(raiz, args, validos);
102
- const falha = await gravarEntrega(path.resolve(raiz, execucao), dados.arquivos, { leiame, relatorio });
103
- if (falha) {
104
- escrever(MSG.falhaDeEscrita(relativoAoProjeto(raiz, falha)));
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);
105
126
  escrever('ENTREGA:INCOMPLETA');
106
127
  return 0;
107
128
  }
108
- for (const linha of resumo(execucao, dados, itens.filter((i) => i.servico))) escrever(linha);
109
- escrever(dados.pendencias.size ? 'ENTREGA:INCOMPLETA' : 'ENTREGA:OK');
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));
110
135
  return 0;
111
136
  }
112
137
 
@@ -138,4 +163,4 @@ export async function main(argv, { cwd = process.cwd(), escrever = (s) => proces
138
163
 
139
164
  if (ehPrincipal(import.meta.url)) {
140
165
  main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
141
- }
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
- // Spec: fase-r1-reparos-1-6-1.md, §3.
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
+ }
@@ -1,7 +1,10 @@
1
1
  // Relatório do verificador em markdown (PT-BR). A última linha é o status que o runner lê.
2
- import { NAO_MEDIDO, NAO_VERIFICADO } from './regras.mjs';
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 resumo = [plural(r.bloqueios, 'bloqueio', 'bloqueios'), plural(r.alertas, 'alerta', 'alertas'), plural(r.naoMedidos ?? 0, 'não medido', 'não medidos')];
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
  '',
@@ -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 e fase-r2-update-e-envio-seguros.md
15
- // (regra 23: "dentro do projeto" pelo texto ou pelo lugar real), no repositório do OpenCrew.
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',
@@ -139,7 +123,8 @@ function resumir(arquivos, naoTexto, notas) {
139
123
  * título do frontmatter não é medido como blog (fase-u3a1-pasta-de-entrega.md, regra 17)
140
124
  * @param {Function} [o.regraDeTeste] SÓ PARA TESTE: regra extra, chamada com `{ arquivo, formato,
141
125
  * texto }` em cada arquivo de texto; serve para simular uma regra que lança erro
142
- * @returns {Promise<object>} `{ arquivos, naoTexto, notas, bloqueios, alertas, naoMedidos, status }`
126
+ * @returns {Promise<object>} `{ arquivos, naoTexto, notas, bloqueios, aPreencher, alertas, naoMedidos,
127
+ * status }` · `aPreencher`: quantos dos `bloqueios` são [PREENCHER]
143
128
  * · `arquivos`: `[{ arquivo, formato, itens, fecho }]`, só os de texto; cada item é
144
129
  * `{ item, medido, limite, nivel, detalhe }`, com `nivel` bloqueio, alerta, ok ou null (linha
145
130
  * "Não medido" sem nível); para a mesma entrada, arquivo + item + detalhe não mudam
@@ -178,12 +163,17 @@ export async function main(argv, { cwd = process.cwd(), escrever = (s) => proces
178
163
  if (faltando.length) escrever(USO);
179
164
  return 1;
180
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;
181
168
  if (!caminhos.some((c) => existsSync(path.resolve(cwd, c)))) {
182
169
  for (const c of new Set(caminhos)) escrever(`Arquivo não encontrado: ${c}`);
183
170
  return 1;
184
171
  }
185
172
  try {
186
- escrever(formatarRelatorio(await verificar({ raiz: cwd, crew: args.crew, arquivos, formato: args.formato || 'blog-post' })));
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);
187
177
  return 0;
188
178
  } catch (erroGeral) {
189
179
  // Fora da lista de arquivos (memória da crew ou `company.md` ilegível): nada foi verificado.
@@ -1,3 +1,4 @@
1
+ # gerenciado pelo OpenCrew: suas linhas ficam fora deste bloco
1
2
  node_modules/
2
3
  .env
3
4
  *.log