@aksp/opencrew 1.10.0 → 1.12.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 (40) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +15 -4
  3. package/package.json +1 -1
  4. package/src/commands/init.js +4 -5
  5. package/src/commands/update.js +8 -0
  6. package/src/lib/resumo.js +5 -1
  7. package/templates/AGENTS.md +16 -9
  8. package/templates/_opencrew/.opencrew-version +1 -1
  9. package/templates/_opencrew/core/architect.agent.yaml +25 -16
  10. package/templates/_opencrew/core/best-practices/_catalog.yaml +5 -0
  11. package/templates/_opencrew/core/best-practices/texto-livre.md +23 -0
  12. package/templates/_opencrew/core/formato-da-crew.md +162 -0
  13. package/templates/_opencrew/core/prompts/build.prompt.md +35 -59
  14. package/templates/_opencrew/core/prompts/design.prompt.md +16 -16
  15. package/templates/_opencrew/core/prompts/discovery.prompt.md +28 -8
  16. package/templates/_opencrew/core/prompts/documento.prompt.md +9 -5
  17. package/templates/_opencrew/core/prompts/entrega.prompt.md +7 -2
  18. package/templates/_opencrew/core/prompts/repair.prompt.md +75 -84
  19. package/templates/_opencrew/core/runner.pipeline.md +25 -28
  20. package/templates/_opencrew/core/scripts/caminho/argumentos.mjs +3 -1
  21. package/templates/_opencrew/core/scripts/caminho/nucleo.mjs +16 -0
  22. package/templates/_opencrew/core/scripts/caminho.mjs +11 -8
  23. package/templates/_opencrew/core/scripts/conferir-fontes/relatorio.mjs +4 -2
  24. package/templates/_opencrew/core/scripts/conferir-fontes.mjs +22 -16
  25. package/templates/_opencrew/core/scripts/conserto/achados.mjs +158 -0
  26. package/templates/_opencrew/core/scripts/conserto/aplicar.mjs +156 -0
  27. package/templates/_opencrew/core/scripts/conserto/argumentos.mjs +51 -0
  28. package/templates/_opencrew/core/scripts/conserto/crew.mjs +126 -0
  29. package/templates/_opencrew/core/scripts/conserto/edicoes.mjs +133 -0
  30. package/templates/_opencrew/core/scripts/conserto/gravar.mjs +55 -0
  31. package/templates/_opencrew/core/scripts/conserto.mjs +82 -0
  32. package/templates/_opencrew/core/scripts/documento/markdown.mjs +2 -1
  33. package/templates/_opencrew/core/scripts/documento/pacote.mjs +1 -0
  34. package/templates/_opencrew/core/scripts/documento/perfil.mjs +14 -0
  35. package/templates/_opencrew/core/scripts/entrega/pendencias.mjs +4 -1
  36. package/templates/_opencrew/core/scripts/entrega/separar.mjs +2 -1
  37. package/templates/_opencrew/core/scripts/verificar/documento.mjs +20 -0
  38. package/templates/_opencrew/core/scripts/verificar/medicao.mjs +2 -1
  39. package/templates/_opencrew/core/scripts/verificar/proibicoes.mjs +30 -5
  40. package/templates/_opencrew/core/scripts/verificar.mjs +3 -1
@@ -0,0 +1,55 @@
1
+ // A gravação do conserto: o que impede de gravar é visto antes de tocar em qualquer arquivo; cada
2
+ // arquivo alterado ganha antes a cópia `.bak` (a que já existe fica).
3
+ // Spec: fase-u4a-conserto-de-crews.md, regras 1 e 2 (repositório do OpenCrew).
4
+ import { accessSync, constants, existsSync, writeFileSync } from 'node:fs';
5
+ import { relativoAoProjeto } from '../comum.mjs';
6
+
7
+ const MSG = {
8
+ naoUtf8: (arquivo) => `Não altero ${arquivo}: o arquivo não está em UTF-8. Salve-o em UTF-8 e rode de novo.`,
9
+ somenteLeitura: (arquivo) => `Não consigo gravar em ${arquivo} (arquivo protegido ou aberto em outro programa). Nada foi gravado.`,
10
+ parouNoMeio: (gravados, arquivo, motivo) => `A gravação parou em ${arquivo}: ${motivo}. Já gravados: ${gravados.join(', ') || 'nenhum'} (a cópia .bak de cada um tem o texto de antes).`,
11
+ };
12
+
13
+ const INVALIDO = String.fromCharCode(0xfffd); // o que sobra de um byte que não é UTF-8
14
+
15
+ function podeGravar(arquivo) {
16
+ try {
17
+ accessSync(arquivo, constants.W_OK);
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /** O que impede a gravação, visto antes de tocar em qualquer arquivo: texto que não é UTF-8, arquivo protegido. */
25
+ export function erroAntesDeGravar(raiz, mudancas) {
26
+ const rel = (m) => relativoAoProjeto(raiz, m.arquivo);
27
+ const estranho = mudancas.find((m) => m.antes?.includes(INVALIDO));
28
+ if (estranho) return MSG.naoUtf8(rel(estranho));
29
+ const fechado = mudancas.find((m) => m.antes !== null && !podeGravar(m.arquivo));
30
+ return fechado ? MSG.somenteLeitura(rel(fechado)) : null;
31
+ }
32
+
33
+ /**
34
+ * Grava as mudanças; antes de cada arquivo, a cópia `.bak` do que estava lá (se ainda não existe).
35
+ * @returns {string[]} as linhas do relatório, com os caminhos relativos ao projeto
36
+ */
37
+ export function gravar(raiz, mudancas) {
38
+ const linhas = [];
39
+ const gravados = [];
40
+ for (const { arquivo, antes, depois } of mudancas) {
41
+ const rel = relativoAoProjeto(raiz, arquivo);
42
+ const copia = `${arquivo}.bak`;
43
+ const jaHavia = existsSync(copia);
44
+ try {
45
+ if (antes !== null && !jaHavia) writeFileSync(copia, antes, 'utf8');
46
+ writeFileSync(arquivo, depois, 'utf8');
47
+ } catch (falha) {
48
+ throw new Error(MSG.parouNoMeio(gravados, rel, falha?.code ?? falha?.message ?? falha), { cause: falha });
49
+ }
50
+ gravados.push(rel);
51
+ linhas.push(`Gravei: ${rel}`);
52
+ if (antes !== null) linhas.push(`Cópia: ${rel}.bak${jaHavia ? ' (mantive a cópia que já existia)' : ''}`);
53
+ }
54
+ return linhas;
55
+ }
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ // Conserto de uma crew que já existe: mostra o que falta para as melhorias do runtime valerem nela
3
+ // e, com --aplicar, grava um item por vez. Quem grava em `crews/` é este script, não a IA.
4
+ // Uso (na pasta do projeto):
5
+ // node _opencrew/core/scripts/conserto.mjs --crew "crews/<crew>" só lê
6
+ // node _opencrew/core/scripts/conserto.mjs --crew "crews/<crew>" --aplicar "<item>" grava
7
+ // Itens de --aplicar: manifesto · nome:<agente>=<Nome Sobrenome> · formato:<passo>=<formato> ·
8
+ // fonte:<caminho>=<para que> · proibicao:<n>=<trecho> · proibicao:<n>=revisao-humana ·
9
+ // irreversivel:<passo> (ver --ajuda).
10
+ // Só grava `crew.yaml`, `crew-party.csv`, `agents/*.agent.md`, `pipeline/steps/*.md` e
11
+ // `_memory/memories.md` da crew, e a cópia `<arquivo>.bak` de cada um (a cópia que já existe
12
+ // não é sobrescrita). Nunca apaga.
13
+ // Última linha da saída: CONSERTO:OK (nada a consertar) · CONSERTO:PENDENTE (há achados) ·
14
+ // CONSERTO:APLICADO (tudo o que foi pedido está gravado) · CONSERTO:ERRO (nada foi gravado).
15
+ // Código de saída: 0, menos com CONSERTO:ERRO (1).
16
+ // Spec: fase-u4a-conserto-de-crews.md (repositório do OpenCrew).
17
+ import { existsSync } from 'node:fs';
18
+ import path from 'node:path';
19
+ import { ehPrincipal, erroDeUso } from './comum.mjs';
20
+ import { diagnosticar } from './conserto/achados.mjs';
21
+ import { planejar } from './conserto/aplicar.mjs';
22
+ import { AJUDA, lerArgs, lerItem, limpar } from './conserto/argumentos.mjs';
23
+ import { lerCrew } from './conserto/crew.mjs';
24
+ import { gravar } from './conserto/gravar.mjs';
25
+
26
+ const MSG = {
27
+ estranho: (arg) => `Argumento desconhecido: ${limpar(arg)}. Veja as opções com --ajuda.`,
28
+ semYaml: (crew) => `A pasta ${limpar(crew)} não tem \`crew.yaml\`: não é uma crew.`,
29
+ itemDesconhecido: (item) => `Item desconhecido em --aplicar: ${limpar(item)}. Veja os itens com --ajuda.`,
30
+ emDia: (nome) => `A crew "${nome}" está em dia: não há o que consertar.`,
31
+ titulo: (nome, n) => `Conserto da crew "${nome}" — ${n} ${n === 1 ? 'achado' : 'achados'}`,
32
+ jaEstava: (item) => `Já estava assim: ${item}`,
33
+ falha: (motivo) => `Não consegui consertar: ${String(motivo).replace(/\s+/g, ' ').trim()}`,
34
+ };
35
+
36
+ /** O que está errado na chamada, antes de ler a crew; ou null. */
37
+ function erroDaChamada(raiz, args) {
38
+ if (args.estranho !== undefined) return MSG.estranho(args.estranho);
39
+ const deUso = erroDeUso({ raiz, faltando: args.crew ? [] : ['--crew'], crew: args.crew });
40
+ if (deUso) return deUso;
41
+ if (!existsSync(path.resolve(raiz, args.crew, 'crew.yaml'))) return MSG.semYaml(args.crew);
42
+ const desconhecido = args.itens.find((i) => !lerItem(i));
43
+ return desconhecido === undefined ? null : MSG.itemDesconhecido(desconhecido);
44
+ }
45
+
46
+ /** As linhas do diagnóstico: um bloco por achado e a linha de situação. */
47
+ function relatorio(crew) {
48
+ const achados = diagnosticar(crew);
49
+ if (!achados.length) return [MSG.emDia(crew.nome), 'CONSERTO:OK'];
50
+ const blocos = achados.flatMap(({ codigo, linhas: [titulo, ...resto] }) => ['', `[${codigo}] ${titulo}`, ...resto.map((l) => ` ${l}`)]);
51
+ return [MSG.titulo(crew.nome, achados.length), ...blocos, '', 'CONSERTO:PENDENTE'];
52
+ }
53
+
54
+ /** As linhas de um `--aplicar`: o que foi gravado, as cópias e a linha de situação. */
55
+ function aplicar(raiz, crew, itens) {
56
+ const plano = planejar(crew, itens.map(lerItem));
57
+ if (plano.erro) return [plano.erro, 'CONSERTO:ERRO'];
58
+ return [...gravar(raiz, plano.mudancas), ...plano.iguais.map(MSG.jaEstava), 'CONSERTO:APLICADO'];
59
+ }
60
+
61
+ /**
62
+ * @param {string[]} argv
63
+ * @param {object} [deps] `cwd` (a pasta do projeto) e `escrever`
64
+ * @returns {number} 0 = a crew foi lida (e, com --aplicar, gravada) · 1 = CONSERTO:ERRO
65
+ */
66
+ export function main(argv, deps = {}) {
67
+ const { cwd = process.cwd(), escrever = (s) => process.stdout.write(`${s}\n`) } = deps;
68
+ const args = lerArgs(argv);
69
+ if (args.ajuda) return AJUDA.forEach((l) => escrever(l)) ?? 0;
70
+ let linhas;
71
+ try {
72
+ const erro = erroDaChamada(cwd, args);
73
+ const crew = erro ? null : lerCrew(cwd, args.crew);
74
+ linhas = erro ? [erro, 'CONSERTO:ERRO'] : args.itens.length ? aplicar(cwd, crew, args.itens) : relatorio(crew);
75
+ } catch (falha) {
76
+ linhas = [MSG.falha(falha?.message ?? falha), 'CONSERTO:ERRO'];
77
+ }
78
+ linhas.forEach((l) => escrever(l));
79
+ return linhas.at(-1) === 'CONSERTO:ERRO' ? 1 : 0;
80
+ }
81
+
82
+ if (ehPrincipal(import.meta.url)) process.exitCode = main(process.argv.slice(2));
@@ -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;
@@ -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 resto = linhas.slice(inicio + 1);
78
- const fim = resto.findIndex((l) => acima.test(l) || l.trim() === '---');
79
- return resto.slice(0, fim < 0 ? resto.length : fim).filter((l) => /^\s*(?:[-*+]|\d{1,3}[.)])\s+/.test(l));
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
  }
@@ -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) };