@aksp/opencrew 1.11.0 → 1.13.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 (36) hide show
  1. package/CHANGELOG.md +66 -3
  2. package/README.md +7 -3
  3. package/package.json +1 -1
  4. package/templates/AGENTS.md +10 -8
  5. package/templates/_opencrew/.opencrew-version +1 -1
  6. package/templates/_opencrew/core/best-practices/_catalog.yaml +5 -0
  7. package/templates/_opencrew/core/best-practices/texto-livre.md +23 -0
  8. package/templates/_opencrew/core/formato-da-crew.md +2 -1
  9. package/templates/_opencrew/core/prompts/build.prompt.md +3 -3
  10. package/templates/_opencrew/core/prompts/design.prompt.md +5 -5
  11. package/templates/_opencrew/core/prompts/discovery.prompt.md +8 -6
  12. package/templates/_opencrew/core/prompts/documento.prompt.md +9 -5
  13. package/templates/_opencrew/core/prompts/entrega.prompt.md +7 -2
  14. package/templates/_opencrew/core/prompts/repair.prompt.md +1 -1
  15. package/templates/_opencrew/core/runner/contrato-de-saida.md +33 -0
  16. package/templates/_opencrew/core/runner/escritorio.md +37 -0
  17. package/templates/_opencrew/core/runner/fim-da-execucao.md +110 -0
  18. package/templates/_opencrew/core/runner/fontes-pendentes.md +21 -0
  19. package/templates/_opencrew/core/runner/memoria.md +55 -0
  20. package/templates/_opencrew/core/runner/selecao-de-agentes.md +79 -0
  21. package/templates/_opencrew/core/runner/tarefas-do-agente.md +34 -0
  22. package/templates/_opencrew/core/runner.pipeline.md +45 -373
  23. package/templates/_opencrew/core/scripts/caminho/argumentos.mjs +3 -1
  24. package/templates/_opencrew/core/scripts/caminho/nucleo.mjs +16 -0
  25. package/templates/_opencrew/core/scripts/caminho.mjs +11 -8
  26. package/templates/_opencrew/core/scripts/conferir-fontes/relatorio.mjs +4 -2
  27. package/templates/_opencrew/core/scripts/conferir-fontes.mjs +22 -16
  28. package/templates/_opencrew/core/scripts/conserto/achados.mjs +1 -1
  29. package/templates/_opencrew/core/scripts/documento/markdown.mjs +2 -1
  30. package/templates/_opencrew/core/scripts/documento/pacote.mjs +1 -0
  31. package/templates/_opencrew/core/scripts/documento/perfil.mjs +14 -0
  32. package/templates/_opencrew/core/scripts/entrega/pendencias.mjs +4 -1
  33. package/templates/_opencrew/core/scripts/entrega/separar.mjs +2 -1
  34. package/templates/_opencrew/core/scripts/verificar/documento.mjs +20 -0
  35. package/templates/_opencrew/core/scripts/verificar/medicao.mjs +2 -1
  36. package/templates/_opencrew/core/scripts/verificar.mjs +3 -1
@@ -2,7 +2,8 @@
2
2
  // Caminho da execução de uma crew: diz onde cada passo grava, de onde lê e se o arquivo gravado
3
3
  // está lá. Quem calcula é este script, igual em qualquer sistema — a IA não monta o caminho.
4
4
  // Uso (na pasta do projeto): node _opencrew/core/scripts/caminho.mjs <crew> <ação> --run <id> [opções]
5
- // pasta --run <id> cria crews/<crew>/output/<id>/
5
+ // pasta [--run <id>] cria crews/<crew>/output/<id>/; sem --run, o id é a
6
+ // data e a hora do computador (AAAA-MM-DD-HHmmss)
6
7
  // saida --run <id> --arquivo <declarado> onde o passo grava (abre a pasta de versão seguinte)
7
8
  // entrada --run <id> --arquivo <declarado> a saída mais nova desse arquivo
8
9
  // conferir --arquivo <caminho já resolvido> [--secoes N] [--tldr]
@@ -17,7 +18,7 @@
17
18
  import path from 'node:path';
18
19
  import { MSG, dentroDoProjeto, ehPrincipal, realDentroDe } from './comum.mjs';
19
20
  import { USO, erroDeArgumentos, lerArgs, limpar } from './caminho/argumentos.mjs';
20
- import { MOTIVO, daMaisNova, motivoDeReprovacao, naExecucao, normalizar, proximaVersao } from './caminho/nucleo.mjs';
21
+ import { MOTIVO, daMaisNova, motivoDeReprovacao, naExecucao, normalizar, novoRun, proximaVersao } from './caminho/nucleo.mjs';
21
22
  import { criarPasta, ehPasta, lerTexto, pastasDe, temConteudo } from './caminho/disco.mjs';
22
23
 
23
24
  const ok = (caminho) => `CAMINHO:OK ${caminho}`;
@@ -31,7 +32,8 @@ const reprovado = (motivo) => `CAMINHO:REPROVADO ${motivo}`;
31
32
  */
32
33
  function erroDoArquivo(raiz, crew, { acao, run, arquivo }) {
33
34
  if (!dentroDoProjeto(raiz, arquivo)) return MSG.foraDoProjeto(limpar(arquivo));
34
- const local = acao === 'conferir' ? null : naExecucao(arquivo, crew, run);
35
+ // `pasta` não usa arquivo, e `conferir` recebe o caminho já resolvido: nenhum dos dois o põe na execução.
36
+ const local = acao === 'conferir' || acao === 'pasta' ? null : naExecucao(arquivo, crew, run);
35
37
  if (!local) return null;
36
38
  if (!local.nome) return `Falta o nome do arquivo em --arquivo: ${limpar(arquivo)}`;
37
39
  const execucao = path.resolve(raiz, 'crews', crew, 'output', run);
@@ -88,23 +90,24 @@ function conferir(raiz, { arquivo, secoes, tldr }) {
88
90
  return motivo ? reprovado(motivo) : ok(caminho);
89
91
  }
90
92
 
91
- function responder(raiz, crew, args) {
93
+ function responder(raiz, crew, args, agora) {
92
94
  if (args.acao === 'saida') return saida(raiz, crew, args);
93
95
  if (args.acao === 'entrada') return entrada(raiz, crew, args);
94
96
  if (args.acao === 'conferir') return conferir(raiz, args);
95
- const pasta = `crews/${crew}/output/${args.run}`;
97
+ const run = args.run || novoRun(agora(), pastasDe(path.resolve(raiz, 'crews', crew, 'output')));
98
+ const pasta = `crews/${crew}/output/${run}`;
96
99
  criarPasta(path.resolve(raiz, pasta));
97
100
  return ok(pasta);
98
101
  }
99
102
 
100
103
  /**
101
104
  * @param {string[]} argv
102
- * @param {object} [deps] `cwd` (a pasta do projeto) e `escrever`
105
+ * @param {object} [deps] `cwd` (a pasta do projeto), `escrever` e `agora` (o relógio, para o id da execução)
103
106
  * @returns {number} 0 = a linha `CAMINHO:` saiu · 1 = erro de uso, ou falha ao ler ou criar pasta
104
107
  * (a linha de uso e o motivo, ou só o erro; sem linha `CAMINHO:`)
105
108
  */
106
109
  export function main(argv, deps = {}) {
107
- const { cwd = process.cwd(), escrever = (s) => process.stdout.write(`${s}\n`) } = deps;
110
+ const { cwd = process.cwd(), escrever = (s) => process.stdout.write(`${s}\n`), agora = () => new Date() } = deps;
108
111
  const args = lerArgs(argv);
109
112
  const local = localizar(cwd, args);
110
113
  if (local.erro) {
@@ -113,7 +116,7 @@ export function main(argv, deps = {}) {
113
116
  return 1;
114
117
  }
115
118
  try {
116
- escrever(responder(cwd, local.crew, args));
119
+ escrever(responder(cwd, local.crew, args, agora));
117
120
  return 0;
118
121
  } catch (erro) {
119
122
  escrever(`Não consegui resolver o caminho: ${limpar(erro?.message ?? erro)}`);
@@ -7,7 +7,8 @@ const plural = (n, um, varios) => `${n} ${n === 1 ? um : varios}`;
7
7
 
8
8
  export const MSG = {
9
9
  nadaACorrigir: 'Nada a corrigir.',
10
- corrigidos: (n) => `${plural(n, 'caminho corrigido', 'caminhos corrigidos')} (cópia .bak ao lado de cada arquivo alterado).\n`,
10
+ corrigido: (arquivo, copia) => `Corrigi: ${arquivo} (cópia: ${copia})`,
11
+ comoCorrigir: 'Para trocar os caminhos que têm sugestão, rode de novo com --corrigir (cada arquivo alterado ganha uma cópia).',
11
12
  semCorrecaoAutomatica: (n) => `Não há correção automática para ${n} pendência(s): escolha um candidato ou corrija o caminho na crew.`,
12
13
  buscaParcial: (limite) => `Procurei só nos primeiros ${limite} itens do projeto; pode existir um arquivo com esse nome que eu não vi.`,
13
14
  linkParaFora: (arquivo) => `Não corrigi \`${arquivo}\`: é um link que aponta para fora da crew. O caminho citado nele continua como estava.`,
@@ -59,6 +60,7 @@ export function formatar(r) {
59
60
  }
60
61
  if (apontados.length) linhas.push(''); // sem pendência nem alerta, uma linha em branco só
61
62
  const alertas = contar('nao-portatil') + contar('nao-conferido');
62
- linhas.push(`**Resumo: ${r.refs.length} fontes — ${contar('ok')} ok, ${contar('faltando')} pendentes, ${alertas} alertas**`, '');
63
+ const pendentes = contar('faltando');
64
+ linhas.push(`**Resumo: ${plural(r.refs.length, 'fonte', 'fontes')} — ${contar('ok')} ok, ${plural(pendentes, 'pendente', 'pendentes')}, ${plural(alertas, 'alerta', 'alertas')}**`, '');
63
65
  return linhas.join('\n');
64
66
  }
@@ -87,18 +87,17 @@ export async function conferir({ raiz, crew, limite = LIMITE_DA_BUSCA }) {
87
87
  return { crew, raiz, refs, status, buscaParcial: Boolean(ctx.indice?.parcial), limite };
88
88
  }
89
89
 
90
+ /** @returns {Promise<string>} o caminho da cópia que ficou */
90
91
  async function copiaDeSeguranca(arquivo) {
91
92
  const bak = existsSync(`${arquivo}.bak`) ? `${arquivo}.bak-${new Date().toISOString().replace(/[:.]/g, '-')}` : `${arquivo}.bak`;
92
93
  await copyFile(arquivo, bak);
94
+ return bak;
93
95
  }
94
96
 
95
- /** Regrava a citação num arquivo; a cópia .bak é feita uma vez por arquivo. */
97
+ /** Regrava a citação num arquivo; a cópia .bak é feita uma vez por arquivo (`tocados`: arquivo → cópia). */
96
98
  async function regravar(arquivo, item, tocados) {
97
99
  const texto = await readFile(arquivo, 'utf8');
98
- if (!tocados.has(arquivo)) {
99
- await copiaDeSeguranca(arquivo);
100
- tocados.add(arquivo);
101
- }
100
+ if (!tocados.has(arquivo)) tocados.set(arquivo, await copiaDeSeguranca(arquivo));
102
101
  await writeFile(arquivo, trocarCitacao(texto, item, path.basename(arquivo) === 'crew.yaml'));
103
102
  }
104
103
 
@@ -118,9 +117,10 @@ function guardaDeEscrita(raiz, crew) {
118
117
  * Troca, nos arquivos da crew, cada caminho com sugestão única — só a citação que a coleta leu,
119
118
  * nunca um pedaço de outro texto. O que a guarda de escrita barra não muda: `avisar` recebe uma
120
119
  * linha por arquivo pulado, ou uma só quando a crew inteira é um link para fora do projeto.
120
+ * `gravado` recebe cada arquivo alterado e a cópia que ficou dele.
121
121
  * @returns quantos caminhos foram gravados, em ao menos um arquivo
122
122
  */
123
- export async function corrigir({ resultado, avisar = () => {} }) {
123
+ export async function corrigir({ resultado, avisar = () => {}, gravado = () => {} }) {
124
124
  const { raiz, crew } = resultado;
125
125
  const comSugestao = resultado.refs.filter((i) => i.sugestao && i.estado !== 'ok');
126
126
  const podeGravar = comSugestao.length ? guardaDeEscrita(raiz, crew) : null;
@@ -128,7 +128,7 @@ export async function corrigir({ resultado, avisar = () => {} }) {
128
128
  if (comSugestao.length) avisar(MSG.crewLigadaParaFora(crew));
129
129
  return 0;
130
130
  }
131
- const tocados = new Set();
131
+ const tocados = new Map();
132
132
  const pulados = new Set();
133
133
  let gravados = 0;
134
134
  for (const item of comSugestao) {
@@ -138,19 +138,22 @@ export async function corrigir({ resultado, avisar = () => {} }) {
138
138
  if (dentro.length) gravados += 1;
139
139
  }
140
140
  for (const arquivo of pulados) avisar(MSG.linkParaFora(relativoAoProjeto(raiz, arquivo)));
141
+ for (const [arquivo, copia] of tocados) gravado(relativoAoProjeto(raiz, arquivo), path.basename(copia));
141
142
  return gravados;
142
143
  }
143
144
 
144
- /** --corrigir: troca o que tem sugestão única, diz o que pulou e quantas pendências ficam sem correção. */
145
+ /**
146
+ * --corrigir: troca o que tem sugestão única e diz cada arquivo alterado, com a cópia; o relatório
147
+ * sai uma vez só — o de depois, quando algo mudou. Diz também o que pulou e o que ficou sem correção.
148
+ */
145
149
  async function corrigirEAvisar(r, escrever) {
146
150
  const pulados = [];
147
- const n = await corrigir({ resultado: r, avisar: (linha) => pulados.push(linha) });
148
- let atual = r;
149
- if (n) {
150
- escrever(MSG.corrigidos(n));
151
- atual = await conferir({ raiz: r.raiz, crew: r.crew });
152
- escrever(formatar(atual));
153
- }
151
+ const feitos = [];
152
+ const n = await corrigir({ resultado: r, avisar: (linha) => pulados.push(linha), gravado: (arquivo, copia) => feitos.push(MSG.corrigido(arquivo, copia)) });
153
+ const atual = n ? await conferir({ raiz: r.raiz, crew: r.crew }) : r;
154
+ for (const linha of feitos) escrever(linha);
155
+ if (n) escrever('');
156
+ escrever(formatar(atual));
154
157
  for (const linha of pulados) escrever(linha);
155
158
  const semSugestao = atual.refs.filter((i) => i.estado === 'faltando' && !i.sugestao).length;
156
159
  if (semSugestao) escrever(MSG.semCorrecaoAutomatica(semSugestao));
@@ -177,8 +180,11 @@ export async function main(argv, { cwd = process.cwd(), escrever = (s) => proces
177
180
  }
178
181
  try {
179
182
  let r = await conferir({ raiz: cwd, crew });
180
- escrever(formatar(r));
181
183
  if (argv.includes('--corrigir')) r = await corrigirEAvisar(r, escrever);
184
+ else {
185
+ escrever(formatar(r));
186
+ if (r.refs.some((i) => i.sugestao && i.estado !== 'ok')) escrever(MSG.comoCorrigir);
187
+ }
182
188
  escrever(`FONTES:${r.status}`);
183
189
  return 0;
184
190
  } catch (erroDeLeitura) {
@@ -52,7 +52,7 @@ function formato(crew) {
52
52
  const n = passos.length;
53
53
  return [
54
54
  `${n} ${plural(n, 'passo que a revisão confere não diz', 'passos que a revisão confere não dizem')} o formato do texto.`,
55
- `Sem o formato, o verificador mede ${plural(n, 'esse texto', 'cada um')} como post de blog.`,
55
+ 'Sem o formato, o redator não recebe o guia desse tipo de texto, e o verificador procura no arquivo peças de rede (legenda, post, título de blog).',
56
56
  `Passos: ${passos.map((p) => `${p.numero} (${saidaDe(p)})`).join(', ')}`,
57
57
  'Para consertar: --aplicar "formato:<passo>=<formato>"',
58
58
  ];
@@ -37,6 +37,7 @@ function lerTabela(linhas, i, estado) {
37
37
  while (fim < linhas.length && linhas[fim].trim() && linhas[fim].includes('|')) fim++;
38
38
  const brutas = [linhas[i], ...linhas.slice(i + 2, fim)].map(celulas);
39
39
  const colunas = Math.max(...brutas.map((l) => l.length));
40
+ estado.avisos.celulas += brutas.slice(1).filter((l) => l.length > brutas[0].length).length;
40
41
  const completas = brutas.map((l) => [...l, ...Array(colunas - l.length).fill('')]);
41
42
  estado.por({ tipo: 'tabela', linhas: completas.map((l) => l.map((c) => lerLinha(c, estado.avisos))) });
42
43
  return fim;
@@ -56,7 +57,7 @@ function lerBloco(linha, avisos) {
56
57
 
57
58
  /** Onde os blocos se juntam: a quebra de página pedida vale para o próximo bloco, uma vez só. */
58
59
  function novoEstado() {
59
- const estado = { blocos: [], avisos: { imagens: 0, desconhecidas: 0, semFim: 0, invalidos: 0 }, quebra: false };
60
+ const estado = { blocos: [], avisos: { imagens: 0, desconhecidas: 0, semFim: 0, invalidos: 0, celulas: 0 }, quebra: false };
60
61
  estado.por = (bloco) => {
61
62
  estado.blocos.push(estado.quebra ? { ...bloco, quebra: true } : bloco);
62
63
  estado.quebra = false;
@@ -22,6 +22,7 @@ function avisosDe(c) {
22
22
  c.imagens && plural(c.imagens, '1 imagem não incluída: o Word não leva imagem no texto.', '{n} imagens não incluídas: o Word não leva imagem no texto.'),
23
23
  c.desconhecidas && plural(c.desconhecidas, '1 linha com marcação desconhecida (`:::`) ficou como texto.', '{n} linhas com marcação desconhecida (`:::`) ficaram como texto.'),
24
24
  c.semFim && plural(c.semFim, 'Bloco de assinaturas sem a linha `:::` no fim: ficou como texto.', '{n} blocos de assinaturas sem a linha `:::` no fim: ficaram como texto.'),
25
+ c.celulas && plural(c.celulas, '1 linha de tabela com mais células que o cabeçalho: a coluna a mais ficou sem título.', '{n} linhas de tabela com mais células que o cabeçalho: a coluna a mais ficou sem título.'),
25
26
  c.invalidos && plural(c.invalidos, '1 caractere inválido removido.', '{n} caracteres inválidos removidos.'),
26
27
  ].filter(Boolean);
27
28
  }
@@ -10,6 +10,7 @@ export const PADRAO = Object.freeze({
10
10
 
11
11
  export const MSG = {
12
12
  chave: (n, chave) => `Perfil, linha ${n}: não conheço a chave ${chave}.`,
13
+ grafia: (n, certa) => `Perfil, linha ${n}: a chave se escreve ${certa}: em minúsculas, sem acento, no começo da linha e sem espaço antes dos dois-pontos.`,
13
14
  valor: (n, chave, esperado, valor) => `Perfil, linha ${n}: ${chave} precisa ser ${esperado}. Recebi: ${valor}.`,
14
15
  };
15
16
 
@@ -38,6 +39,17 @@ const CHAVES = {
38
39
  tamanho_corpo_pt: { esperado: 'um número de 8 a 14 (aceita meio ponto)', ler: (valor) => (numero(valor) >= 8 && numero(valor) <= 14 && Number.isInteger(numero(valor) * 2) ? numero(valor) : undefined) },
39
40
  };
40
41
 
42
+ // Linha que parece "chave: valor" mas não foi lida como chave: maiúscula, acento ou espaço antes.
43
+ const QUASE = /^[ \t]*([\p{L}0-9_]+)[ \t]*:(?!\/\/)/u;
44
+ const semAcento = (s) => s.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase();
45
+
46
+ /** A chave conhecida que a linha quis escrever, quando não a escreveu do jeito certo; senão, null. */
47
+ function chaveQuaseCerta(linha) {
48
+ const [, escrita] = QUASE.exec(linha) ?? [];
49
+ const certa = escrita ? semAcento(escrita) : '';
50
+ return Object.hasOwn(CHAVES, certa) && !linha.startsWith(`${certa}:`) ? certa : null;
51
+ }
52
+
41
53
  /** Tira as aspas em volta do valor, se as duas pontas têm a mesma. */
42
54
  function semAspas(valor) {
43
55
  const v = valor.trim();
@@ -58,6 +70,8 @@ export function lerPerfil(bruto) {
58
70
  const semBom = bruto.charCodeAt(0) === 0xfeff ? bruto.slice(1) : bruto;
59
71
  for (const [i, linha] of semBom.split(/\r\n?|\n/).entries()) {
60
72
  const [, chave, escrito = ''] = LINHA.exec(linha.trimEnd()) ?? [];
73
+ const quase = chave ? null : chaveQuaseCerta(linha);
74
+ if (quase) return { perfil, linhas, erro: MSG.grafia(i + 1, quase) };
61
75
  if (!chave) continue;
62
76
  if (!Object.hasOwn(CHAVES, chave)) return { perfil, linhas, erro: MSG.chave(i + 1, chave) };
63
77
  linhas[chave] = i + 1;
@@ -14,6 +14,8 @@ export const MSG = {
14
14
  };
15
15
  /** A lista fixa do que o script nunca confere. */
16
16
  export const NUNCA_CONFERIDO = ['Links e fatos citados no texto.', 'Texto dentro das imagens.', 'Aparência final em cada rede.'];
17
+ /** Canal que não é de rede: nele não há imagem a publicar nem aparência de rede a conferir. */
18
+ const SEM_REDE = new Set(['documentos']);
17
19
 
18
20
  const ehNaoMedido = (i) => i.item === NAO_MEDIDO || i.item === NAO_VERIFICADO;
19
21
  const origem = (arquivo) => `origem: \`${arquivo}\``;
@@ -63,7 +65,8 @@ export function naoConferido(raiz, itens, r) {
63
65
  }
64
66
  }
65
67
  if (r.naoTexto.length) linhas.push(`${NAO_VERIFICADO} — não é texto: ${r.naoTexto.join(', ')}`);
66
- return [...linhas, ...NUNCA_CONFERIDO];
68
+ const temRede = itens.some((i) => i.canal && !SEM_REDE.has(i.canal));
69
+ return [...linhas, ...(temRede ? NUNCA_CONFERIDO : NUNCA_CONFERIDO.slice(0, 1))];
67
70
  }
68
71
 
69
72
  const LIMITE = { legenda: 'caption_max_chars', post: 'post_max_chars', tweet: 'tweet_max_chars' };
@@ -80,7 +80,8 @@ function separarPecas(item, texto, itens, p) {
80
80
  async function separarItem(item, itens, p) {
81
81
  if (ehEditavel(item, itens)) return p.copias.push(copia(item, EDITAVEIS, 'editavel'));
82
82
  if (!item.canal) {
83
- p.avisos.push({ pasta: OUTROS, tela: MSG.semCanal(item.rel) });
83
+ // `texto-livre` é o formato de quem não tem canal: estar em `outros/` é o esperado, não um aviso.
84
+ if (item.formato !== 'texto-livre') p.avisos.push({ pasta: OUTROS, tela: MSG.semCanal(item.rel) });
84
85
  return p.copias.push(copia(item, OUTROS, 'copia'));
85
86
  }
86
87
  // Documento oficial: vai inteiro, sem procura de peças; `documentos.mjs` converte o texto em Word.
@@ -0,0 +1,20 @@
1
+ // O que o Word não vai converter, visto antes do revisor: no texto de formato `documento-oficial`
2
+ // o verificador lê as mesmas marcações que o conversor e põe um alerta para cada tipo de sobra.
3
+ // Alerta, nunca bloqueio: quem decide é o revisor. Spec: fase-u5a-polimento-do-uso.md, regra 6.
4
+ import { lerMarkdown } from '../documento/markdown.mjs';
5
+ import { item } from './regras.mjs';
6
+
7
+ export const FORMATO_DE_DOCUMENTO = 'documento-oficial';
8
+ const NOME = 'Documento Word';
9
+ const plural = (n, um, varios) => (n === 1 ? um : varios.replace('{n}', n));
10
+ const FRASES = [
11
+ ['imagens', '1 imagem não entra no documento', '{n} imagens não entram no documento'],
12
+ ['desconhecidas', '1 marcação `:::` desconhecida vai como texto', '{n} marcações `:::` desconhecidas vão como texto'],
13
+ ['semFim', '1 bloco de assinaturas sem a linha `:::` do fim vai como texto', '{n} blocos de assinaturas sem a linha `:::` do fim vão como texto'],
14
+ ];
15
+
16
+ /** Os alertas de um texto que vai virar Word; lista vazia quando tudo converte. */
17
+ export function alertasDeDocumento(texto) {
18
+ const { avisos } = lerMarkdown(texto);
19
+ return FRASES.filter(([chave]) => avisos[chave]).map(([chave, um, varios]) => item(NOME, null, null, 'alerta', plural(avisos[chave], um, varios)));
20
+ }
@@ -108,7 +108,8 @@ function naoMedidos({ pecas, corpo, formato, limites }, semLimite, medidas) {
108
108
  for (const cabecalho of BLOG.includes(formato) ? secoesDeOutroCanal(corpo) : []) {
109
109
  linhas.push(item(NAO_MEDIDO, null, null, null, `seção de outro canal num arquivo de blog: ${curto(cabecalho)}`));
110
110
  }
111
- if (formato && !tipos && limites[formato] && !medidas) {
111
+ // Formato que não declara limite nenhum (documento-oficial, texto-livre) não tem o que medir.
112
+ if (formato && !tipos && Object.keys(limites[formato] ?? {}).length && !medidas) {
112
113
  linhas.push(item(NAO_MEDIDO, null, null, null, `o verificador ainda não mede os limites do formato ${formato}`));
113
114
  }
114
115
  return linhas;
@@ -25,6 +25,7 @@ import { lerLimites, lerDominioDoSite, semFrontmatter } from './verificar/leitur
25
25
  import { lerProibicoes } from './verificar/proibicoes.mjs';
26
26
  import { lerPecas } from './verificar/pecas.mjs';
27
27
  import { medirPecas } from './verificar/medicao.mjs';
28
+ import { FORMATO_DE_DOCUMENTO, alertasDeDocumento } from './verificar/documento.mjs';
28
29
  import { regrasGerais, temVariavel, item, FALTA_INFO, NAO_MEDIDO, NAO_VERIFICADO } from './verificar/regras.mjs';
29
30
  import { formatarRelatorio } from './verificar/relatorio.mjs';
30
31
  import { MSG as GRAVACAO, caminhoDoRelatorio, gravarRelatorio } from './verificar/gravacao.mjs';
@@ -86,7 +87,8 @@ async function verificarArquivo({ arquivo, formato }, ctx, regraDeTeste) {
86
87
  const envio = /^(email|whatsapp)-/.test(formato ?? '');
87
88
  if (envio && temVariavel(lido.texto)) ctx.notas.add(NOTA_VARIAVEL);
88
89
  const gerais = regrasGerais(lido.texto, ctx.proibidos, { variavelBloqueia: !envio });
89
- const extras = regraDeTeste ? (await regraDeTeste({ arquivo, formato, texto: lido.texto })) ?? [] : [];
90
+ const doWord = formato === FORMATO_DE_DOCUMENTO && lido.comPecas ? alertasDeDocumento(lido.texto) : [];
91
+ const extras = [...doWord, ...(regraDeTeste ? (await regraDeTeste({ arquivo, formato, texto: lido.texto })) ?? [] : [])];
90
92
  const naoMedido = (i) => i.item === NAO_MEDIDO;
91
93
  const itens = [...medidos.filter((i) => !naoMedido(i)), ...gerais, ...extras, ...medidos.filter(naoMedido)];
92
94
  return { itens, fecho: fechoDe(itens, medidas, formato) };