@aksp/opencrew 1.9.0 → 1.11.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 +112 -0
- package/README.md +98 -6
- package/package.json +1 -1
- package/src/commands/init.js +4 -5
- package/src/commands/update.js +8 -0
- package/src/lib/resumo.js +5 -1
- package/templates/AGENTS.md +17 -7
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/architect.agent.yaml +25 -16
- package/templates/_opencrew/core/best-practices/_catalog.yaml +5 -0
- package/templates/_opencrew/core/best-practices/documento-oficial.md +144 -0
- package/templates/_opencrew/core/formato-da-crew.md +162 -0
- package/templates/_opencrew/core/modelos/documento-oficial.md +42 -0
- package/templates/_opencrew/core/prompts/build.prompt.md +33 -57
- package/templates/_opencrew/core/prompts/design.prompt.md +12 -11
- package/templates/_opencrew/core/prompts/discovery.prompt.md +23 -5
- package/templates/_opencrew/core/prompts/documento.prompt.md +134 -0
- package/templates/_opencrew/core/prompts/entrega.prompt.md +5 -4
- package/templates/_opencrew/core/prompts/repair.prompt.md +75 -84
- package/templates/_opencrew/core/runner.pipeline.md +9 -12
- package/templates/_opencrew/core/scripts/conserto/achados.mjs +158 -0
- package/templates/_opencrew/core/scripts/conserto/aplicar.mjs +156 -0
- package/templates/_opencrew/core/scripts/conserto/argumentos.mjs +51 -0
- package/templates/_opencrew/core/scripts/conserto/crew.mjs +126 -0
- package/templates/_opencrew/core/scripts/conserto/edicoes.mjs +133 -0
- package/templates/_opencrew/core/scripts/conserto/gravar.mjs +55 -0
- package/templates/_opencrew/core/scripts/conserto.mjs +82 -0
- package/templates/_opencrew/core/scripts/documento/argumentos.mjs +50 -0
- package/templates/_opencrew/core/scripts/documento/corpo.mjs +51 -0
- package/templates/_opencrew/core/scripts/documento/estilos.mjs +42 -0
- package/templates/_opencrew/core/scripts/documento/gravar.mjs +44 -0
- package/templates/_opencrew/core/scripts/documento/linha.mjs +58 -0
- package/templates/_opencrew/core/scripts/documento/marcacoes.mjs +55 -0
- package/templates/_opencrew/core/scripts/documento/markdown.mjs +92 -0
- package/templates/_opencrew/core/scripts/documento/pacote.mjs +82 -0
- package/templates/_opencrew/core/scripts/documento/perfil.mjs +70 -0
- package/templates/_opencrew/core/scripts/documento/png.mjs +20 -0
- package/templates/_opencrew/core/scripts/documento/projeto.mjs +60 -0
- package/templates/_opencrew/core/scripts/documento/tabelas.mjs +57 -0
- package/templates/_opencrew/core/scripts/documento/timbre.mjs +64 -0
- package/templates/_opencrew/core/scripts/documento/xml.mjs +95 -0
- package/templates/_opencrew/core/scripts/documento/zip.mjs +86 -0
- package/templates/_opencrew/core/scripts/documento.mjs +150 -0
- package/templates/_opencrew/core/scripts/entrega/canais.mjs +7 -3
- package/templates/_opencrew/core/scripts/entrega/comparar.mjs +2 -2
- package/templates/_opencrew/core/scripts/entrega/copia.mjs +1 -1
- package/templates/_opencrew/core/scripts/entrega/documentos.mjs +104 -0
- package/templates/_opencrew/core/scripts/entrega/gravar.mjs +3 -3
- package/templates/_opencrew/core/scripts/entrega/leiame.mjs +3 -2
- package/templates/_opencrew/core/scripts/entrega/passos.mjs +12 -2
- package/templates/_opencrew/core/scripts/entrega/separar.mjs +3 -0
- package/templates/_opencrew/core/scripts/entregar.mjs +12 -5
- package/templates/_opencrew/core/scripts/verificar/proibicoes.mjs +30 -5
|
@@ -17,7 +17,7 @@ const ehPasta = async (p) => fs.stat(p).then((s) => s.isDirectory(), () => false
|
|
|
17
17
|
/** Grava um arquivo que ainda não existe ali: se existir, falha (nunca por cima). */
|
|
18
18
|
async function gravarNovo(alvo, a) {
|
|
19
19
|
await fs.mkdir(path.dirname(alvo), { recursive: true });
|
|
20
|
-
if (a.texto != null) await fs.writeFile(alvo, a.texto, { encoding: 'utf8', flag: 'wx' });
|
|
20
|
+
if ((a.bytes ?? a.texto) != null) await fs.writeFile(alvo, a.bytes ?? a.texto, { encoding: 'utf8', flag: 'wx' });
|
|
21
21
|
else await fs.copyFile(a.de, alvo, constants.COPYFILE_EXCL);
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Documentos na entrega: o item cujo formato tem a plataforma `documento` vai para a pasta
|
|
2
|
+
// `documentos/`. Texto (`.md` ou `.txt`) vira Word, inteiro, com o perfil de documento oficial do
|
|
3
|
+
// projeto, se existir: os mesmos bytes que o `documento.mjs` grava para o mesmo texto e o mesmo
|
|
4
|
+
// perfil. Outro tipo de arquivo é copiado como está, com aviso. Erro ao converter (inclusive
|
|
5
|
+
// perfil inválido) é pendência de `documentos`, que nunca vira ressalva: os outros canais seguem.
|
|
6
|
+
// Este módulo só lê: quem grava é `gravar.mjs`.
|
|
7
|
+
// Spec: fase-u3b-documento-word.md, decisão 9, regra 12 e §6 (repositório do OpenCrew).
|
|
8
|
+
import { existsSync, promises as fs, statSync } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { PERFIL, gerarDocx, lerPerfilDoProjeto } from '../documento.mjs';
|
|
11
|
+
import { limpar } from './argumentos.mjs';
|
|
12
|
+
|
|
13
|
+
/** A pasta da entrega e o tipo do arquivo que é (ou vai virar) um Word. */
|
|
14
|
+
export const DOCUMENTOS = 'documentos';
|
|
15
|
+
export const WORD = 'documento';
|
|
16
|
+
const TEXTO = /\.(md|txt)$/i;
|
|
17
|
+
|
|
18
|
+
export const MSG = {
|
|
19
|
+
copiado: (arquivo) => `${arquivo}: só converto .md ou .txt em Word. Copiei o arquivo como está.`,
|
|
20
|
+
erro: (arquivo, motivo) => `Não consegui gerar o Word de ${arquivo}: ${limpar(motivo).replace(/\.+$/, '')}.`,
|
|
21
|
+
aviso: (arquivo, aviso) => `${arquivo}: ${aviso}`,
|
|
22
|
+
semPerfil: 'Sem papel timbrado: este projeto não tem perfil de documento oficial. Para criar o seu: node _opencrew/core/scripts/documento.mjs --criar-perfil',
|
|
23
|
+
semTexto: 'o arquivo não tem texto para converter',
|
|
24
|
+
naoUtf8: 'o arquivo não está em UTF-8',
|
|
25
|
+
};
|
|
26
|
+
/** A linha de "O que não foi conferido" da entrega que tem um Word. */
|
|
27
|
+
export const NAO_CONFERIDO = 'Como o documento abre no Word.';
|
|
28
|
+
|
|
29
|
+
/** O tipo do arquivo em `documentos/`: texto que vira Word, ou cópia como está. */
|
|
30
|
+
export const tipoDoDocumento = (arquivo) => (TEXTO.test(arquivo) ? WORD : 'copia');
|
|
31
|
+
export const naoConferidoDoWord = (arquivos) => (arquivos.some((a) => a.pasta === DOCUMENTOS && a.tipo === WORD) ? [NAO_CONFERIDO] : []);
|
|
32
|
+
|
|
33
|
+
/** O perfil do projeto, lido uma vez: `{}` sem perfil, `{ perfil, logotipo }` ou `{ erro }`. */
|
|
34
|
+
async function perfilDoProjeto(raiz) {
|
|
35
|
+
const arquivo = path.join(raiz, PERFIL);
|
|
36
|
+
if (!existsSync(arquivo) || !statSync(arquivo).isFile()) return {};
|
|
37
|
+
try {
|
|
38
|
+
return await lerPerfilDoProjeto(raiz, PERFIL);
|
|
39
|
+
} catch (falha) {
|
|
40
|
+
return { erro: falha?.message ?? falha };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** O texto do arquivo, em UTF-8; null quando os bytes não são UTF-8. */
|
|
45
|
+
function decodificar(bytes) {
|
|
46
|
+
try {
|
|
47
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** O Word de um texto: `{ bytes, avisos }`, ou `{ erro }` com o motivo. */
|
|
54
|
+
async function converter(copia, lido, gerar) {
|
|
55
|
+
if (lido.erro) return { erro: lido.erro };
|
|
56
|
+
try {
|
|
57
|
+
const texto = decodificar(await fs.readFile(copia.de));
|
|
58
|
+
if (texto === null) return { erro: MSG.naoUtf8 };
|
|
59
|
+
const documento = gerar({ texto, perfil: lido.perfil, logotipo: lido.logotipo });
|
|
60
|
+
return documento.vazio ? { erro: MSG.semTexto } : documento;
|
|
61
|
+
} catch (falha) {
|
|
62
|
+
return { erro: falha?.message ?? falha };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A cópia do texto vira o arquivo Word: cada origem é um grupo, para dois nomes iguais não se perderem. */
|
|
67
|
+
const comoWord = (copia, bytes) => ({ ...copia, nome: copia.nome.replace(TEXTO, '.docx'), de: null, bytes, pastaDeOrigem: copia.de });
|
|
68
|
+
const aviso = (texto) => ({ pasta: DOCUMENTOS, texto });
|
|
69
|
+
const pendencia = (linha) => ({ linha, chave: null, preencher: null });
|
|
70
|
+
|
|
71
|
+
/** Um arquivo de `documentos/` → o que entra em `copias`, `avisos` ou `pendencias`. */
|
|
72
|
+
async function tratar(copia, lido, gerar, saida) {
|
|
73
|
+
if (copia.tipo !== WORD) {
|
|
74
|
+
saida.avisos.push(aviso(MSG.copiado(copia.origem)));
|
|
75
|
+
return saida.copias.push(copia);
|
|
76
|
+
}
|
|
77
|
+
const r = await converter(copia, lido, gerar);
|
|
78
|
+
if (r.erro) return saida.pendencias.push(pendencia(MSG.erro(copia.origem, r.erro)));
|
|
79
|
+
saida.avisos.push(...r.avisos.map((a) => aviso(MSG.aviso(copia.origem, a))));
|
|
80
|
+
return saida.copias.push(comoWord(copia, r.bytes));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Converte os textos que `separar` pôs em `documentos/`. Sem nenhum, devolve o que recebeu e não
|
|
85
|
+
* lê o perfil.
|
|
86
|
+
* @param {string} raiz a pasta do projeto
|
|
87
|
+
* @param {{ unidades: object[], copias: object[], avisos: object[] }} produtos o que `separar` devolve
|
|
88
|
+
* @param {function} [gerar] quem monta o Word (`gerarDocx`); os testes trocam para injetar um erro
|
|
89
|
+
* @returns {Promise<{ produtos: object, pendencias: object[] }>} `produtos`: os mesmos, com o Word
|
|
90
|
+
* (`{ …, nome: '<nome>.docx', bytes }`) no lugar de cada texto convertido e os avisos da conversão ·
|
|
91
|
+
* `pendencias`: uma por texto que não foi convertido, `{ linha, chave: null, preencher: null }`
|
|
92
|
+
*/
|
|
93
|
+
export async function converterDocumentos(raiz, produtos, gerar = gerarDocx) {
|
|
94
|
+
const saida = { copias: [], avisos: [...produtos.avisos], pendencias: [] };
|
|
95
|
+
if (!produtos.copias.some((c) => c.pasta === DOCUMENTOS)) return { produtos, pendencias: [] };
|
|
96
|
+
const lido = produtos.copias.some((c) => c.pasta === DOCUMENTOS && c.tipo === WORD) ? await perfilDoProjeto(raiz) : {};
|
|
97
|
+
for (const copia of produtos.copias) {
|
|
98
|
+
if (copia.pasta === DOCUMENTOS) await tratar(copia, lido, gerar, saida);
|
|
99
|
+
else saida.copias.push(copia);
|
|
100
|
+
}
|
|
101
|
+
// Sem perfil, o Word sai sem cabeçalho: a entrega diz, para ninguém procurar um timbre que não existe.
|
|
102
|
+
if (!lido.perfil && !lido.erro && saida.copias.some((c) => c.pasta === DOCUMENTOS && c.bytes)) saida.avisos.push(aviso(MSG.semPerfil));
|
|
103
|
+
return { produtos: { ...produtos, copias: saida.copias, avisos: saida.avisos }, pendencias: saida.pendencias };
|
|
104
|
+
}
|
|
@@ -25,10 +25,10 @@ async function limparSobras({ entrega, tmp, antiga }) {
|
|
|
25
25
|
if ((await tipoDe(tmp)) === 'pasta') await apagar(tmp);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/** Grava um arquivo da entrega na pasta temporária: o texto
|
|
28
|
+
/** Grava um arquivo da entrega na pasta temporária: os bytes ou o texto gerados, ou os mesmos bytes da origem. */
|
|
29
29
|
async function gravarArquivo(destino, a) {
|
|
30
30
|
await fs.mkdir(path.dirname(destino), { recursive: true });
|
|
31
|
-
if (a.texto != null) await fs.writeFile(destino, a.texto, 'utf8');
|
|
31
|
+
if ((a.bytes ?? a.texto) != null) await fs.writeFile(destino, a.bytes ?? a.texto, 'utf8');
|
|
32
32
|
else await fs.copyFile(a.de, destino);
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -51,7 +51,7 @@ async function trocar({ entrega, tmp, antiga }, passo) {
|
|
|
51
51
|
/**
|
|
52
52
|
* Refaz `entrega/` do zero e grava, ao lado, o relatório da verificação.
|
|
53
53
|
* @param {string} execucao pasta da execução (`crews/<crew>/output/<run>/`), absoluta
|
|
54
|
-
* @param {object[]} arquivos `{ pasta, nome, texto | de }`
|
|
54
|
+
* @param {object[]} arquivos `{ pasta, nome, texto | bytes | de }`
|
|
55
55
|
* @param {{ leiame: string, relatorio: string }} textos
|
|
56
56
|
* @returns {Promise<string|null>} null quando gravou tudo; senão, o caminho que não foi gravado
|
|
57
57
|
*/
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// (ressalvas, a cópia e "Sobre esta pasta"), no repositório do OpenCrew.
|
|
6
6
|
import { CANAIS, EDITAVEIS, OUTROS } from './canais.mjs';
|
|
7
7
|
import { frasesDePendencia } from './pendencias.mjs';
|
|
8
|
-
import { passosDe } from './passos.mjs';
|
|
8
|
+
import { NAO_EDITE_O_WORD, passosDe } from './passos.mjs';
|
|
9
9
|
|
|
10
10
|
export const TEXTO = {
|
|
11
11
|
vaiPublicar: 'Esta crew publica este canal sozinha. Antes de postar à mão, confira se já saiu.',
|
|
@@ -64,7 +64,8 @@ function secaoDoCanal(d, pasta) {
|
|
|
64
64
|
// Na cópia, o canal que não foi copiado traz só a situação e o que falta.
|
|
65
65
|
if (d.ehCopia && pendencias.length) return secao(CANAIS[pasta], [situacao, ...bloco('Pendências:', pendencias)]);
|
|
66
66
|
const arquivos = d.arquivos.filter((a) => a.pasta === pasta);
|
|
67
|
-
const
|
|
67
|
+
const daPasta = passosDe(pasta, arquivos, pendencias.length > 0, d.ressalvas.has(pasta));
|
|
68
|
+
const passos = daPasta.filter((p) => !d.ehCopia || p !== NAO_EDITE_O_WORD).map((p, i) => `${i + 1}. ${p}`);
|
|
68
69
|
return secao(CANAIS[pasta], [
|
|
69
70
|
...(d.vaiPublicar.includes(pasta) ? [TEXTO.vaiPublicar] : []),
|
|
70
71
|
situacao,
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
export const ANTES_DE_POSTAR = 'Antes de postar, resolva o que está em Pendências. Corrija no arquivo de origem e peça para montar a entrega de novo: o que você mudar nesta pasta se perde.';
|
|
8
8
|
/** O primeiro passo do canal entregue com ressalva. */
|
|
9
9
|
export const CONFIRA_AS_RESSALVAS = 'Antes de postar, confira as ressalvas em "Antes de usar".';
|
|
10
|
-
|
|
10
|
+
/** O passo de `documentos` que só vale na pasta da entrega (a cópia não é refeita). Spec: fase-u3b-documento-word.md, §6. */
|
|
11
|
+
export const NAO_EDITE_O_WORD = 'Não edite o Word dentro desta pasta: ela é refeita a cada entrega. Para mexer, copie o arquivo para outra pasta do projeto.';
|
|
12
|
+
const MARKDOWN ='Este texto está em markdown: se o seu editor não aceitar, ajuste títulos, negrito e links depois de colar.';
|
|
11
13
|
|
|
12
14
|
// Cada passo: [o que o faz aparecer, o texto]. null = sempre que o canal tem arquivo; um tipo de
|
|
13
15
|
// arquivo = só quando a entrega tem arquivo desse tipo no canal; `tipo:um` e `tipo:varios` = só
|
|
@@ -58,6 +60,12 @@ const PASSOS = {
|
|
|
58
60
|
[null, 'Confira e publique.'],
|
|
59
61
|
],
|
|
60
62
|
youtube: [['roteiro', '`youtube/{arquivo}` é o roteiro para gravar: não é texto para colar.']],
|
|
63
|
+
documentos: [
|
|
64
|
+
['documento', 'Abra `documentos/{arquivo}` no Word e confira: cabeçalho, páginas, tabelas e assinaturas.'],
|
|
65
|
+
['documento', NAO_EDITE_O_WORD],
|
|
66
|
+
['documento', 'Para ter um PDF: abra o documento no Word e use Arquivo → Salvar como → PDF.'],
|
|
67
|
+
['documento', 'O Word é uma cópia do texto. O que você mudar nele não volta sozinho: altere o texto e gere de novo.'],
|
|
68
|
+
],
|
|
61
69
|
};
|
|
62
70
|
// "texto": o roteiro e o arquivo que foi inteiro (carrossel em texto, peça não encontrada).
|
|
63
71
|
const TIPOS = { texto: ['roteiro', 'inteiro'] };
|
|
@@ -100,5 +108,7 @@ function doQuadro(pasta, arquivos) {
|
|
|
100
108
|
export function passosDe(pasta, arquivos, comPendencia = false, comRessalva = false) {
|
|
101
109
|
const passos = doQuadro(pasta, arquivos);
|
|
102
110
|
const antes = comPendencia ? ANTES_DE_POSTAR : comRessalva ? CONFIRA_AS_RESSALVAS : null;
|
|
103
|
-
|
|
111
|
+
// Documento não se posta: imprime-se, assina-se, protocola-se.
|
|
112
|
+
const frase = pasta === 'documentos' ? antes?.replace('Antes de postar', 'Antes de usar') : antes;
|
|
113
|
+
return frase && passos.length ? [frase, ...passos] : passos;
|
|
104
114
|
}
|
|
@@ -6,6 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { lerItem } from '../verificar/arquivos.mjs';
|
|
7
7
|
import { semFrontmatter } from '../verificar/leitura.mjs';
|
|
8
8
|
import { EDITAVEIS, OUTROS } from './canais.mjs';
|
|
9
|
+
import { DOCUMENTOS, tipoDoDocumento } from './documentos.mjs';
|
|
9
10
|
import { avisoDeFora, blocosFora } from './fora.mjs';
|
|
10
11
|
import { PRINCIPAL, hashtagsSoltas, pecasDoLeitor } from './leitor.mjs';
|
|
11
12
|
import { pecasDoBlog, pecasDoEmail, pecasDoWhatsapp, tweetsDaThread } from './longas.mjs';
|
|
@@ -82,6 +83,8 @@ async function separarItem(item, itens, p) {
|
|
|
82
83
|
p.avisos.push({ pasta: OUTROS, tela: MSG.semCanal(item.rel) });
|
|
83
84
|
return p.copias.push(copia(item, OUTROS, 'copia'));
|
|
84
85
|
}
|
|
86
|
+
// Documento oficial: vai inteiro, sem procura de peças; `documentos.mjs` converte o texto em Word.
|
|
87
|
+
if (item.canal === DOCUMENTOS) return p.copias.push(copia(item, DOCUMENTOS, tipoDoDocumento(item.abs)));
|
|
85
88
|
const lido = ['.md', '.txt'].includes(extensao(item)) ? await lerItem(item.abs) : null;
|
|
86
89
|
if (lido?.tipo !== 'texto') return p.copias.push(copia(item, item.canal, ehImagem(item) ? 'imagem' : 'copia'));
|
|
87
90
|
const texto = emLf(lido.texto);
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// --aceitar-pendencias: as pendências deste momento viram ressalvas ("entregar assim mesmo"); a
|
|
12
12
|
// ressalva vale enquanto a pendência dela existir: a que some sai do `ressalvas.json`.
|
|
13
13
|
// --vai-publicar: canal que a crew publica sozinha (pode repetir); o LEIA-ME avisa.
|
|
14
|
+
// Item de formato com a plataforma `documento` (`.md` ou `.txt`) sai em `entrega/documentos/`, em
|
|
15
|
+
// Word, com o perfil de documento oficial do projeto (fase-u3b-documento-word.md, regra 12).
|
|
14
16
|
// Grava em crews/<crew>/output/<run>/: a pasta `entrega/` (refeita do zero a cada chamada, montada
|
|
15
17
|
// em `entrega.tmp/`), `verificacao-entrega.md`, `ressalvas.json` (as pendências aceitas que ainda
|
|
16
18
|
// existem) e `copia.json` (o retrato do que foi copiado, para a comparação seguinte). Fora
|
|
@@ -29,6 +31,7 @@ import { formatarRelatorio, verificar } from './verificar.mjs';
|
|
|
29
31
|
import { USO, lerArgs, lerLista, limpar, runValido } from './entrega/argumentos.mjs';
|
|
30
32
|
import { CANAIS, canalDoFormato, ehCanal, ehDeServico } from './entrega/canais.mjs';
|
|
31
33
|
import { MSG as DESTINO, escolherDestino, validarDestino } from './entrega/destino.mjs';
|
|
34
|
+
import { DOCUMENTOS, converterDocumentos, naoConferidoDoWord } from './entrega/documentos.mjs';
|
|
32
35
|
import { MSG as COPIA, guardar } from './entrega/guardar.mjs';
|
|
33
36
|
import { gravarEntrega } from './entrega/gravar.mjs';
|
|
34
37
|
import { montarLeiame } from './entrega/leiame.mjs';
|
|
@@ -84,15 +87,18 @@ async function classificar(raiz, pedidos) {
|
|
|
84
87
|
async function montar(raiz, args, itens, aceitas) {
|
|
85
88
|
const arquivosDaLista = itens.map((i) => (i.formato ? { arquivo: i.arquivo, formato: i.formato } : i.arquivo));
|
|
86
89
|
const verificacao = await verificar({ raiz, crew: args.crew, arquivos: arquivosDaLista, semPadraoDeBlog: true });
|
|
87
|
-
const
|
|
90
|
+
const separados = await converterDocumentos(raiz, await separar(itens), args.gerarDocx);
|
|
91
|
+
const { arquivos, avisos } = nomear(separados.produtos);
|
|
88
92
|
const todas = pendenciasPorPasta(raiz, itens, verificacao);
|
|
93
|
+
// O texto que não virou Word é pendência de `documentos` (fase-u3b-documento-word.md, regra 12).
|
|
94
|
+
if (separados.pendencias.length) todas.set(DOCUMENTOS, [...(todas.get(DOCUMENTOS) ?? []), ...separados.pendencias]);
|
|
89
95
|
const presentes = new Set([...arquivos.map((a) => a.pasta), ...todas.keys()]);
|
|
90
96
|
const dados = {
|
|
91
97
|
crew: path.basename(path.resolve(raiz, args.crew)), run: args.run, arquivos, avisos,
|
|
92
98
|
...separarPendencias(todas, aceitas, args.aceitar),
|
|
93
99
|
pastas: Object.keys(CANAIS).filter((c) => presentes.has(c)),
|
|
94
100
|
alertas: await alertasDeTamanho(raiz, itens, arquivos, verificacao),
|
|
95
|
-
naoConferido: naoConferido(raiz, itens, verificacao),
|
|
101
|
+
naoConferido: [...naoConferido(raiz, itens, verificacao), ...naoConferidoDoWord(arquivos)],
|
|
96
102
|
vaiPublicar: args.vaiPublicar,
|
|
97
103
|
};
|
|
98
104
|
return { dados, relatorio: `${formatarRelatorio(verificacao)}\n` };
|
|
@@ -137,12 +143,13 @@ async function entregar(raiz, args, itens, escrever) {
|
|
|
137
143
|
|
|
138
144
|
/**
|
|
139
145
|
* @param {string[]} argv
|
|
140
|
-
* @param {object} [deps] `cwd` (a pasta do projeto) e `
|
|
146
|
+
* @param {object} [deps] `cwd` (a pasta do projeto), `escrever` e `gerarDocx` (quem monta o Word
|
|
147
|
+
* de um documento oficial; sem ele, o do `documento.mjs`)
|
|
141
148
|
* @returns {Promise<number>} 0 = a linha `ENTREGA:` saiu (ou `--ajuda`) · 1 = erro de uso, ou erro
|
|
142
149
|
* que impediu a entrega inteira (uma linha em PT-BR, sem linha `ENTREGA:`, nada escrito)
|
|
143
150
|
*/
|
|
144
|
-
export async function main(argv, { cwd = process.cwd(), escrever = (s) => process.stdout.write(`${s}\n`) } = {}) {
|
|
145
|
-
const args = lerArgs(argv);
|
|
151
|
+
export async function main(argv, { cwd = process.cwd(), escrever = (s) => process.stdout.write(`${s}\n`), gerarDocx } = {}) {
|
|
152
|
+
const args = { ...lerArgs(argv), gerarDocx };
|
|
146
153
|
const sair = (...linhas) => linhas.forEach((l) => escrever(l)) ?? 1;
|
|
147
154
|
if (args.ajuda) return sair(USO) && 0;
|
|
148
155
|
const pedidos = lerLista(args.arquivos);
|
|
@@ -12,6 +12,8 @@ const MARCADORES = new Set(['prefira', 'preferir', 'use', 'usar', 'utilize', 'ut
|
|
|
12
12
|
const NEGACOES = new Set('nao nunca nem jamais sem evite evitar proibido proibida proibidos proibidas vetado vetada parar pare deixar deixe'.split(' '));
|
|
13
13
|
// Com estas expressões a linha está escrita ao contrário: todos os termos continuam proibidos.
|
|
14
14
|
const SEM_TROCA = /(?<![\p{L}\p{N}])(?:em vez de|ao inves de|no lugar de)(?![\p{L}\p{N}])/u;
|
|
15
|
+
// Começo de um item de lista: `-`, `*`, `+` ou numerado.
|
|
16
|
+
const MARCA_DE_ITEM = /^\s*(?:[-*+]|\d{1,3}[.)])\s+/;
|
|
15
17
|
|
|
16
18
|
/** Fichas da linha, na ordem: termos entre aspas, e palavras e setas de fora das aspas. */
|
|
17
19
|
function fichas(linha) {
|
|
@@ -74,9 +76,32 @@ function itensDaSecao(texto) {
|
|
|
74
76
|
const inicio = titulos.findIndex((t) => t && semAcento(t[2]).replace(/^[^\p{L}\p{N}]+/u, '').startsWith('proibicoes explicitas'));
|
|
75
77
|
if (inicio < 0) return [];
|
|
76
78
|
const acima = new RegExp(`^#{1,${titulos[inicio][1].length}}\\s`);
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
return
|
|
79
|
+
const fim = linhas.findIndex((l, i) => i > inicio && (acima.test(l) || l.trim() === '---'));
|
|
80
|
+
const daSecao = linhas.map((texto, indice) => ({ texto, indice })).slice(inicio + 1, fim < 0 ? linhas.length : fim);
|
|
81
|
+
return daSecao.filter((l) => MARCA_DE_ITEM.test(l.texto));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Item que o usuário deixou para o revisor: termina em "(revisão humana)" ou começa por "Sem trava
|
|
86
|
+
* automática". Não é trava de texto nem pendência. Spec: fase-u4a-conserto-de-crews.md, regra 8.
|
|
87
|
+
*/
|
|
88
|
+
export function ehRevisaoHumana(linha) {
|
|
89
|
+
const item = semAcento(linha.replace(MARCA_DE_ITEM, '')).trim();
|
|
90
|
+
return item.endsWith('(revisao humana)') || item.startsWith('sem trava automatica');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Os itens da seção de proibições, numerados a partir de 1 na ordem do arquivo.
|
|
95
|
+
* @returns {Array<{ n: number, indice: number, texto: string, pendente: boolean }>} `indice`: a
|
|
96
|
+
* linha no arquivo · `texto`: o item sem a marca de lista · `pendente`: sem termo entre aspas e
|
|
97
|
+
* sem a marca de revisão humana
|
|
98
|
+
*/
|
|
99
|
+
export function itensDeProibicao(texto) {
|
|
100
|
+
return itensDaSecao(texto).map((l, i) => {
|
|
101
|
+
const { proibidos, preferidos } = lerLinha(l.texto);
|
|
102
|
+
const pendente = !proibidos.length && !preferidos.length && !ehRevisaoHumana(l.texto);
|
|
103
|
+
return { n: i + 1, indice: l.indice, texto: l.texto.replace(MARCA_DE_ITEM, '').trim(), pendente };
|
|
104
|
+
});
|
|
80
105
|
}
|
|
81
106
|
|
|
82
107
|
/**
|
|
@@ -88,9 +113,9 @@ export async function lerProibicoes(raiz, crew) {
|
|
|
88
113
|
const arquivo = path.resolve(raiz, crew, '_memory', 'memories.md');
|
|
89
114
|
const r = { existe: existsSync(arquivo), termos: [], preferidos: [], semAspas: 0 };
|
|
90
115
|
if (!r.existe) return r;
|
|
91
|
-
for (const linha of itensDaSecao(await lerTexto(arquivo))) {
|
|
116
|
+
for (const { texto: linha } of itensDaSecao(await lerTexto(arquivo))) {
|
|
92
117
|
const { proibidos, preferidos } = lerLinha(linha);
|
|
93
|
-
if (!proibidos.length && !preferidos.length) r.semAspas += 1;
|
|
118
|
+
if (!proibidos.length && !preferidos.length && !ehRevisaoHumana(linha)) r.semAspas += 1;
|
|
94
119
|
r.termos.push(...proibidos);
|
|
95
120
|
r.preferidos.push(...preferidos);
|
|
96
121
|
}
|