@aksp/opencrew 1.12.0 → 1.14.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 +57 -0
- package/README.md +25 -3
- package/package.json +1 -1
- package/templates/AGENTS.md +3 -5
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/formato-da-crew.md +1 -0
- package/templates/_opencrew/core/prompts/build.prompt.md +2 -0
- package/templates/_opencrew/core/prompts/repair.prompt.md +4 -1
- package/templates/_opencrew/core/runner/contrato-de-saida.md +33 -0
- package/templates/_opencrew/core/runner/escritorio.md +37 -0
- package/templates/_opencrew/core/runner/fim-da-execucao.md +99 -0
- package/templates/_opencrew/core/runner/fontes-pendentes.md +21 -0
- package/templates/_opencrew/core/runner/memoria.md +58 -0
- package/templates/_opencrew/core/runner/retomar.md +51 -0
- package/templates/_opencrew/core/runner/selecao-de-agentes.md +79 -0
- package/templates/_opencrew/core/runner/tarefas-do-agente.md +34 -0
- package/templates/_opencrew/core/runner.pipeline.md +52 -364
- package/templates/_opencrew/core/scripts/caminho/argumentos.mjs +7 -4
- package/templates/_opencrew/core/scripts/caminho/crew.mjs +20 -0
- package/templates/_opencrew/core/scripts/caminho/disco.mjs +4 -3
- package/templates/_opencrew/core/scripts/caminho.mjs +62 -29
- package/templates/_opencrew/core/scripts/conserto/achados.mjs +2 -0
- package/templates/_opencrew/core/scripts/conserto/aplicar.mjs +2 -1
- package/templates/_opencrew/core/scripts/conserto/argumentos.mjs +2 -1
- package/templates/_opencrew/core/scripts/conserto/crew.mjs +4 -3
- package/templates/_opencrew/core/scripts/conserto/historico.mjs +102 -0
- package/templates/_opencrew/core/scripts/conserto.mjs +9 -7
- package/templates/_opencrew/core/scripts/entrega/leiame.mjs +4 -2
- package/templates/_opencrew/core/scripts/entregar.mjs +2 -0
- package/templates/_opencrew/core/scripts/estado/arquivo.mjs +17 -10
- package/templates/_opencrew/core/scripts/execucao/argumentos.mjs +62 -0
- package/templates/_opencrew/core/scripts/execucao/historico.mjs +76 -0
- package/templates/_opencrew/core/scripts/execucao/registro.mjs +115 -0
- package/templates/_opencrew/core/scripts/execucao/retomar.mjs +123 -0
- package/templates/_opencrew/core/scripts/execucao.mjs +123 -0
|
@@ -2,24 +2,32 @@
|
|
|
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>]
|
|
6
|
-
//
|
|
5
|
+
// pasta [--run <id>] [--tema "<texto>"] [--passos N]
|
|
6
|
+
// cria crews/<crew>/output/<id>/ e o registro da
|
|
7
|
+
// execução; sem --run, o id é a data e a hora do
|
|
8
|
+
// computador (AAAA-MM-DD-HHmmss)
|
|
7
9
|
// saida --run <id> --arquivo <declarado> onde o passo grava (abre a pasta de versão seguinte)
|
|
8
10
|
// entrada --run <id> --arquivo <declarado> a saída mais nova desse arquivo
|
|
9
|
-
// conferir --arquivo <caminho já resolvido> [--secoes N] [--tldr]
|
|
11
|
+
// conferir --arquivo <caminho já resolvido> [--passo N] [--secoes N] [--tldr]
|
|
12
|
+
// com --passo, o arquivo aprovado entra no registro
|
|
10
13
|
// <crew> é o nome da pasta em `crews/`. <declarado> é o `inputFile` ou `outputFile` do passo.
|
|
11
|
-
// Só cria pastas, e só dentro de crews/<crew>/output/<id
|
|
14
|
+
// Só cria pastas, e só dentro de crews/<crew>/output/<id>/. O único arquivo que grava é o registro
|
|
15
|
+
// da execução (`crews/<crew>/output/<id>/execucao.json`); nunca altera nem apaga outro arquivo.
|
|
16
|
+
// Registro que não pôde ser gravado não muda a linha CAMINHO: — o aviso sai antes dela.
|
|
12
17
|
// Última linha da saída (o runner lê esta linha): CAMINHO:OK <caminho>, CAMINHO:FALTA <caminho>
|
|
13
18
|
// ou CAMINHO:REPROVADO <motivo>; o caminho sai relativo à pasta do projeto, com `/`.
|
|
14
19
|
// Código de saída: 0 sempre que a linha CAMINHO: sai · 1 = erro de uso (ação ou opção faltando,
|
|
15
20
|
// pasta sem `_opencrew/`, crew inexistente ou fora do projeto); com código 1 não há linha
|
|
16
21
|
// CAMINHO: e nada é criado.
|
|
17
|
-
//
|
|
22
|
+
// Specs: fase-r3-runner-em-uso-real.md e fase-u5c-execucao-registrada.md, regras 1 a 5
|
|
23
|
+
// (repositório do OpenCrew).
|
|
18
24
|
import path from 'node:path';
|
|
19
25
|
import { MSG, dentroDoProjeto, ehPrincipal, realDentroDe } from './comum.mjs';
|
|
20
|
-
import { USO, erroDeArgumentos, lerArgs, limpar } from './caminho/argumentos.mjs';
|
|
26
|
+
import { RUN, USO, erroDeArgumentos, lerArgs, limpar } from './caminho/argumentos.mjs';
|
|
27
|
+
import { acharCrew } from './caminho/crew.mjs';
|
|
28
|
+
import { ARQUIVO, anotar, comPasso, limparTema } from './execucao/registro.mjs';
|
|
21
29
|
import { MOTIVO, daMaisNova, motivoDeReprovacao, naExecucao, normalizar, novoRun, proximaVersao } from './caminho/nucleo.mjs';
|
|
22
|
-
import { criarPasta,
|
|
30
|
+
import { criarPasta, lerTexto, pastasDe, temConteudo } from './caminho/disco.mjs';
|
|
23
31
|
|
|
24
32
|
const ok = (caminho) => `CAMINHO:OK ${caminho}`;
|
|
25
33
|
const falta = (caminho) => `CAMINHO:FALTA ${caminho}`;
|
|
@@ -50,15 +58,10 @@ function erroDoArquivo(raiz, crew, { acao, run, arquivo }) {
|
|
|
50
58
|
function localizar(raiz, args) {
|
|
51
59
|
const erro = erroDeArgumentos(args);
|
|
52
60
|
if (erro) return { erro };
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const pasta = path.resolve(base, nome);
|
|
58
|
-
if (path.dirname(pasta) !== base || !ehPasta(pasta)) return { erro: MSG.crewNaoEncontrada(limpar(args.crew)) };
|
|
59
|
-
const crew = path.basename(pasta);
|
|
60
|
-
const doArquivo = args.arquivo ? erroDoArquivo(raiz, crew, args) : null;
|
|
61
|
-
return doArquivo ? { erro: doArquivo } : { crew };
|
|
61
|
+
const achada = acharCrew(raiz, args.crew);
|
|
62
|
+
if (achada.erro) return achada;
|
|
63
|
+
const doArquivo = args.arquivo ? erroDoArquivo(raiz, achada.crew, args) : null;
|
|
64
|
+
return doArquivo ? { erro: doArquivo } : achada;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
/** Regras 2 e 3: o caminho em que o passo grava; a pasta dele é criada. */
|
|
@@ -90,23 +93,53 @@ function conferir(raiz, { arquivo, secoes, tldr }) {
|
|
|
90
93
|
return motivo ? reprovado(motivo) : ok(caminho);
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
/**
|
|
97
|
+
* A execução de um arquivo conferido: a pasta logo depois de `crews/<crew>/output/`, se ela é de
|
|
98
|
+
* execução (tem registro, ou o nome começa por data) e fica mesmo dentro de `output/` — uma pasta
|
|
99
|
+
* qualquer do usuário, ou um atalho que leva para fora, não ganha registro.
|
|
100
|
+
*/
|
|
101
|
+
function execucaoDe(raiz, caminho, crew) {
|
|
102
|
+
const saida = `crews/${crew}/output/`;
|
|
103
|
+
const [run, ...resto] = caminho.startsWith(saida) ? caminho.slice(saida.length).split('/') : [];
|
|
104
|
+
if (!resto.length || !RUN.test(run) || resto.includes('..')) return null;
|
|
105
|
+
const pasta = path.resolve(raiz, saida, run);
|
|
106
|
+
const ehExecucao = /^\d{4}-\d{2}-\d{2}/.test(run) || temConteudo(path.join(pasta, ARQUIVO));
|
|
107
|
+
return ehExecucao && realDentroDe(path.resolve(raiz, saida), pasta) ? run : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Regra 2 da U5c: o arquivo que passou na conferência entra no registro, como a saída do passo. */
|
|
111
|
+
async function conferirERegistrar(raiz, crew, args, em) {
|
|
112
|
+
const linha = conferir(raiz, args);
|
|
113
|
+
const [, caminho] = /^CAMINHO:OK (.*)$/.exec(linha) ?? [];
|
|
114
|
+
const run = caminho && args.passo ? execucaoDe(raiz, caminho, crew) : null;
|
|
115
|
+
if (!run) return [linha];
|
|
116
|
+
const pasta = path.resolve(raiz, 'crews', crew, 'output', run);
|
|
117
|
+
return [await anotar(pasta, { crew, run, em }, (registro) => comPasso(registro, { n: Number(args.passo), arquivo: caminho, em })), linha];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A pasta da execução; a que nasce agora ganha o registro (regras 1 e 3 da U5c: a que já existia fica como está). */
|
|
121
|
+
async function pasta(raiz, crew, args, agora) {
|
|
122
|
+
const run = args.run || novoRun(agora, pastasDe(path.resolve(raiz, 'crews', crew, 'output')));
|
|
123
|
+
const rel = `crews/${crew}/output/${run}`;
|
|
124
|
+
const nova = criarPasta(path.resolve(raiz, rel));
|
|
125
|
+
const base = { crew, run, tema: limparTema(args.tema), passos: args.passos ? Number(args.passos) : null, em: agora.toISOString() };
|
|
126
|
+
return [nova ? await anotar(path.resolve(raiz, rel), base, (registro) => registro) : null, ok(rel)];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function responder(raiz, crew, args, agora) {
|
|
130
|
+
if (args.acao === 'saida') return [saida(raiz, crew, args)];
|
|
131
|
+
if (args.acao === 'entrada') return [entrada(raiz, crew, args)];
|
|
132
|
+
if (args.acao === 'conferir') return conferirERegistrar(raiz, crew, args, agora.toISOString());
|
|
133
|
+
return pasta(raiz, crew, args, agora);
|
|
101
134
|
}
|
|
102
135
|
|
|
103
136
|
/**
|
|
104
137
|
* @param {string[]} argv
|
|
105
138
|
* @param {object} [deps] `cwd` (a pasta do projeto), `escrever` e `agora` (o relógio, para o id da execução)
|
|
106
|
-
* @returns {number} 0 = a linha `CAMINHO:` saiu · 1 = erro de uso, ou falha ao ler ou
|
|
107
|
-
* (a linha de uso e o motivo, ou só o erro; sem linha `CAMINHO:`)
|
|
139
|
+
* @returns {Promise<number>} 0 = a linha `CAMINHO:` saiu · 1 = erro de uso, ou falha ao ler ou
|
|
140
|
+
* criar pasta (a linha de uso e o motivo, ou só o erro; sem linha `CAMINHO:`)
|
|
108
141
|
*/
|
|
109
|
-
export function main(argv, deps = {}) {
|
|
142
|
+
export async function main(argv, deps = {}) {
|
|
110
143
|
const { cwd = process.cwd(), escrever = (s) => process.stdout.write(`${s}\n`), agora = () => new Date() } = deps;
|
|
111
144
|
const args = lerArgs(argv);
|
|
112
145
|
const local = localizar(cwd, args);
|
|
@@ -116,7 +149,7 @@ export function main(argv, deps = {}) {
|
|
|
116
149
|
return 1;
|
|
117
150
|
}
|
|
118
151
|
try {
|
|
119
|
-
|
|
152
|
+
(await responder(cwd, local.crew, args, agora())).filter(Boolean).forEach((linha) => escrever(linha));
|
|
120
153
|
return 0;
|
|
121
154
|
} catch (erro) {
|
|
122
155
|
escrever(`Não consegui resolver o caminho: ${limpar(erro?.message ?? erro)}`);
|
|
@@ -124,4 +157,4 @@ export function main(argv, deps = {}) {
|
|
|
124
157
|
}
|
|
125
158
|
}
|
|
126
159
|
|
|
127
|
-
if (ehPrincipal(import.meta.url)) process.exitCode = main(process.argv.slice(2));
|
|
160
|
+
if (ehPrincipal(import.meta.url)) process.exitCode = await main(process.argv.slice(2));
|
|
@@ -6,6 +6,7 @@ import { lerFrontmatter, semBom } from '../verificar/leitura.mjs';
|
|
|
6
6
|
import { itensDeProibicao } from '../verificar/proibicoes.mjs';
|
|
7
7
|
import { idDoAgente, lerBruto, listaDe } from './crew.mjs';
|
|
8
8
|
import { lerCsv } from './edicoes.mjs';
|
|
9
|
+
import { achadoDoHistorico } from './historico.mjs';
|
|
9
10
|
|
|
10
11
|
const EXPORTACAO = new Set(['pdf', 'csv', 'formatted-post']);
|
|
11
12
|
const plural = (n, um, varios) => (n === 1 ? um : varios);
|
|
@@ -146,6 +147,7 @@ const CONFERENCIAS = [
|
|
|
146
147
|
['nome-de-agente', nomeDeAgente], ['manifesto', manifesto], ['formato', formato], ['fontes', fontes],
|
|
147
148
|
['proibicao', proibicao], ['irreversivel', semMarca], ['sem-revisao', semRevisao],
|
|
148
149
|
['sem-aprovacao-final', semAprovacaoFinal], ['publica-antes', publicaAntes], ['passo-faltando', passoFaltando],
|
|
150
|
+
['historico', achadoDoHistorico],
|
|
149
151
|
];
|
|
150
152
|
|
|
151
153
|
/**
|
|
@@ -8,6 +8,7 @@ import { lerFrontmatter, semAcento, semBom } from '../verificar/leitura.mjs';
|
|
|
8
8
|
import { ehRevisaoHumana, itensDeProibicao, lerLinha } from '../verificar/proibicoes.mjs';
|
|
9
9
|
import { comChave, comFonte, comSufixo, comTitulo, montarCsv } from './edicoes.mjs';
|
|
10
10
|
import { erroAntesDeGravar } from './gravar.mjs';
|
|
11
|
+
import { historico } from './historico.mjs';
|
|
11
12
|
|
|
12
13
|
const MSG = {
|
|
13
14
|
semValor: (item) => `Sobra ou falta valor em --aplicar "${item.escrito}". Veja os itens com --ajuda.`,
|
|
@@ -134,7 +135,7 @@ function proibicao(crew, item, plano) {
|
|
|
134
135
|
return plano.trocar(memoria, crew.memoria, novo);
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
const ACOES = { manifesto, nome, formato, fonte, proibicao, irreversivel };
|
|
138
|
+
const ACOES = { manifesto, nome, formato, fonte, proibicao, irreversivel, historico };
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
141
|
* Valida os itens e monta as mudanças, sem gravar.
|
|
@@ -13,9 +13,10 @@ export const AJUDA = [
|
|
|
13
13
|
' proibicao:<n>=<trecho> o trecho do item n vira trava do verificador',
|
|
14
14
|
' proibicao:<n>=revisao-humana o item n fica para o revisor',
|
|
15
15
|
' irreversivel:<passo> marca o passo que publica ou envia',
|
|
16
|
+
' historico:<execução>=<tema> põe no histórico a execução que ficou sem linha',
|
|
16
17
|
];
|
|
17
18
|
|
|
18
|
-
export const TIPOS = ['manifesto', 'nome', 'formato', 'fonte', 'proibicao', 'irreversivel'];
|
|
19
|
+
export const TIPOS = ['manifesto', 'nome', 'formato', 'fonte', 'proibicao', 'irreversivel', 'historico'];
|
|
19
20
|
const OPCAO = /^--(crew|aplicar|ajuda)(?:=(.*))?$/s;
|
|
20
21
|
const ITEM = /^([a-z-]+)(?::([^=]*))?(?:=(.*))?$/s;
|
|
21
22
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Leitura de uma crew para o conserto: `crew.yaml`, os passos do pipeline, os agentes, o manifesto
|
|
2
|
-
// e
|
|
1
|
+
// Leitura de uma crew para o conserto: `crew.yaml`, os passos do pipeline, os agentes, o manifesto,
|
|
2
|
+
// a memória e o histórico (`runs.md`). A leitura é tolerante (aceita as formas que versões antigas gravaram) e nunca grava.
|
|
3
3
|
// Spec: fase-u4a-conserto-de-crews.md, regra 5 (repositório do OpenCrew).
|
|
4
4
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
@@ -121,6 +121,7 @@ export function lerCrew(raiz, crew) {
|
|
|
121
121
|
agentes: agentesDe(pasta),
|
|
122
122
|
csv: lerBruto(arquivo('crew-party.csv')),
|
|
123
123
|
memoria: lerBruto(arquivo('_memory', 'memories.md')),
|
|
124
|
-
|
|
124
|
+
runs: lerBruto(arquivo('_memory', 'runs.md')),
|
|
125
|
+
arquivos: { yaml: arquivo('crew.yaml'), csv: arquivo('crew-party.csv'), memoria: arquivo('_memory', 'memories.md'), runs: arquivo('_memory', 'runs.md') },
|
|
125
126
|
};
|
|
126
127
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// O histórico no conserto: pastas de execução sem linha no `runs.md` (achado), pasta vazia e linha
|
|
2
|
+
// sem pasta (só apontadas), e o item que grava a linha "Registrada depois". Nunca apaga nem cria
|
|
3
|
+
// registro de execução.
|
|
4
|
+
// Spec: fase-u5c-execucao-registrada.md, regras 12 e 13 (repositório do OpenCrew).
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { REGISTRADA_DEPOIS, comLinha, dataDe, linhaDe, runsDoHistorico } from '../execucao/historico.mjs';
|
|
8
|
+
import { RUN } from '../caminho/argumentos.mjs';
|
|
9
|
+
import { ARQUIVO, limparTema } from '../execucao/registro.mjs';
|
|
10
|
+
|
|
11
|
+
const MSG = {
|
|
12
|
+
semLinha: (n) => `${n} ${n === 1 ? 'execução' : 'execuções'} sem linha no histórico.`,
|
|
13
|
+
vazias: (runs) => `${runs.length} ${runs.length === 1 ? 'execução abandonada (pasta vazia)' : 'execuções abandonadas (pasta vazia)'}: ${runs.join(', ')}`,
|
|
14
|
+
semPasta: (runs) => `${runs.length} ${runs.length === 1 ? 'linha' : 'linhas'} do histórico sem a pasta da execução: ${runs.join(', ')} (só aponto: nada é apagado)`,
|
|
15
|
+
interrompida: (crew) => ` (interrompida: /opencrew retomar ${crew} continua de onde parou)`,
|
|
16
|
+
comoRegistrar: 'Para registrar: --aplicar "historico:<execução>=<tema>"',
|
|
17
|
+
semValor: (item) => `Sobra ou falta valor em --aplicar "${item.escrito}". Veja os itens com --ajuda.`,
|
|
18
|
+
semExecucao: (item) => `Pasta de execução não encontrada em --aplicar "${item.escrito}".`,
|
|
19
|
+
jaTemLinha: (run) => `A execução ${run} já tem linha no histórico.`,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const COM_DATA = /^\d{4}-\d{2}-\d{2}/;
|
|
23
|
+
const MAXIMO_DE_NOMES = 8;
|
|
24
|
+
|
|
25
|
+
const entradas = (pasta) => (existsSync(pasta) ? readdirSync(pasta, { withFileTypes: true }) : []);
|
|
26
|
+
const saidaDe = (crew) => path.join(crew.pasta, 'output');
|
|
27
|
+
|
|
28
|
+
/** O `status` do registro da pasta, ou `null` (sem registro, ou ilegível). */
|
|
29
|
+
function statusDe(pasta) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(path.join(pasta, ARQUIVO), 'utf8')).status ?? null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Uma pasta da execução, como o usuário a reconhece: pelos arquivos dela (`v1/ata.md`); sem arquivo direto, `v1/`. */
|
|
38
|
+
function daPasta(execucao, nome) {
|
|
39
|
+
const arquivos = entradas(path.join(execucao, nome)).filter((e) => e.isFile()).map((e) => `${nome}/${e.name}`);
|
|
40
|
+
return arquivos.length ? arquivos : [`${nome}/`];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** As pastas de `output/` que são de execução: o nome começa por uma data, ou a pasta tem registro. */
|
|
44
|
+
function execucoes(crew) {
|
|
45
|
+
const pastas = entradas(saidaDe(crew)).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
46
|
+
return pastas.map((run) => {
|
|
47
|
+
const pasta = path.join(saidaDe(crew), run);
|
|
48
|
+
// O registro (e um temporário dele) não conta como arquivo da execução: a pasta que só tem isso é abandonada.
|
|
49
|
+
const doRegistro = (nome) => nome === ARQUIVO || (nome.startsWith(`${ARQUIVO}.`) && nome.endsWith('.tmp'));
|
|
50
|
+
const topo = entradas(pasta).filter((e) => !doRegistro(e.name)).flatMap((e) => (e.isDirectory() ? daPasta(pasta, e.name) : [e.name])).sort();
|
|
51
|
+
return { run, topo, status: statusDe(pasta), ehExecucao: COM_DATA.test(run) || existsSync(path.join(pasta, ARQUIVO)) };
|
|
52
|
+
}).filter((e) => e.ehExecucao);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** O que não bate entre as pastas e o `runs.md`. */
|
|
56
|
+
function situacao(crew) {
|
|
57
|
+
const comLinhaNoHistorico = runsDoHistorico(crew.runs);
|
|
58
|
+
const pastas = execucoes(crew);
|
|
59
|
+
const semLinha = pastas.filter((e) => !comLinhaNoHistorico.includes(e.run));
|
|
60
|
+
return {
|
|
61
|
+
semLinha: semLinha.filter((e) => e.topo.length),
|
|
62
|
+
vazias: semLinha.filter((e) => !e.topo.length).map((e) => e.run),
|
|
63
|
+
semPasta: comLinhaNoHistorico.filter((run) => !existsSync(path.join(saidaDe(crew), run))),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** O que é só apontado: a pasta vazia e a linha sem pasta. */
|
|
68
|
+
function apontados({ vazias, semPasta }) {
|
|
69
|
+
return [...(vazias.length ? [MSG.vazias(vazias)] : []), ...(semPasta.length ? [MSG.semPasta(semPasta)] : [])];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function linhaDaPasta(crew, { run, topo, status }) {
|
|
73
|
+
const nomes = topo.length > MAXIMO_DE_NOMES ? [...topo.slice(0, MAXIMO_DE_NOMES), '…'] : topo;
|
|
74
|
+
return `${run}: ${nomes.join(', ')}${status === 'aberta' ? MSG.interrompida(crew.nome) : ''}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** O achado `historico`: só existe quando há pasta com arquivos e sem linha. @returns {string[]|null} */
|
|
78
|
+
export function achadoDoHistorico(crew) {
|
|
79
|
+
const s = situacao(crew);
|
|
80
|
+
if (!s.semLinha.length) return null;
|
|
81
|
+
return [MSG.semLinha(s.semLinha.length), ...s.semLinha.map((e) => linhaDaPasta(crew, e)), ...apontados(s), MSG.comoRegistrar];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Sem o achado, o que é só apontado sai como nota: não deixa a crew pendente. @returns {string[]} */
|
|
85
|
+
export function notasDoHistorico(crew) {
|
|
86
|
+
const s = situacao(crew);
|
|
87
|
+
return s.semLinha.length ? [] : apontados(s).map((linha) => `Nota: ${linha}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** O item `historico:<execução>=<tema>`: a linha "Registrada depois", na posição da data dela. */
|
|
91
|
+
export function historico(crew, item, plano) {
|
|
92
|
+
const run = item.alvo;
|
|
93
|
+
const pasta = path.join(saidaDe(crew), run);
|
|
94
|
+
if (!RUN.test(run) || !entradas(saidaDe(crew)).some((e) => e.isDirectory() && e.name === run)) return MSG.semExecucao(item);
|
|
95
|
+
const { runs } = crew.arquivos;
|
|
96
|
+
const texto = plano.texto(runs, crew.runs);
|
|
97
|
+
if (runsDoHistorico(texto).includes(run)) return MSG.jaTemLinha(run);
|
|
98
|
+
if (!limparTema(item.valor)) return MSG.semValor(item);
|
|
99
|
+
const data = dataDe(run, statSync(pasta).mtime); // id sem data: o dia em que a pasta mudou pela última vez
|
|
100
|
+
const linha = linhaDe({ data, run, tema: limparTema(item.valor), saida: '', score: '', resultado: REGISTRADA_DEPOIS });
|
|
101
|
+
return plano.trocar(runs, crew.runs, comLinha(texto, { crew: crew.nome, run, linha, porData: true }));
|
|
102
|
+
}
|
|
@@ -6,14 +6,15 @@
|
|
|
6
6
|
// node _opencrew/core/scripts/conserto.mjs --crew "crews/<crew>" --aplicar "<item>" grava
|
|
7
7
|
// Itens de --aplicar: manifesto · nome:<agente>=<Nome Sobrenome> · formato:<passo>=<formato> ·
|
|
8
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
|
|
11
|
-
// `_memory/memories.md` da crew, e a cópia `<arquivo>.bak` de cada um (a
|
|
12
|
-
// não é sobrescrita). Nunca apaga.
|
|
9
|
+
// irreversivel:<passo> · historico:<execução>=<tema> (ver --ajuda).
|
|
10
|
+
// Só grava `crew.yaml`, `crew-party.csv`, `agents/*.agent.md`, `pipeline/steps/*.md`,
|
|
11
|
+
// `_memory/memories.md` e `_memory/runs.md` da crew, e a cópia `<arquivo>.bak` de cada um (a
|
|
12
|
+
// cópia que já existe não é sobrescrita). Nunca apaga.
|
|
13
13
|
// Última linha da saída: CONSERTO:OK (nada a consertar) · CONSERTO:PENDENTE (há achados) ·
|
|
14
14
|
// CONSERTO:APLICADO (tudo o que foi pedido está gravado) · CONSERTO:ERRO (nada foi gravado).
|
|
15
15
|
// Código de saída: 0, menos com CONSERTO:ERRO (1).
|
|
16
|
-
//
|
|
16
|
+
// Specs: fase-u4a-conserto-de-crews.md e fase-u5c-execucao-registrada.md, regras 12 e 13
|
|
17
|
+
// (repositório do OpenCrew).
|
|
17
18
|
import { existsSync } from 'node:fs';
|
|
18
19
|
import path from 'node:path';
|
|
19
20
|
import { ehPrincipal, erroDeUso } from './comum.mjs';
|
|
@@ -22,6 +23,7 @@ import { planejar } from './conserto/aplicar.mjs';
|
|
|
22
23
|
import { AJUDA, lerArgs, lerItem, limpar } from './conserto/argumentos.mjs';
|
|
23
24
|
import { lerCrew } from './conserto/crew.mjs';
|
|
24
25
|
import { gravar } from './conserto/gravar.mjs';
|
|
26
|
+
import { notasDoHistorico } from './conserto/historico.mjs';
|
|
25
27
|
|
|
26
28
|
const MSG = {
|
|
27
29
|
estranho: (arg) => `Argumento desconhecido: ${limpar(arg)}. Veja as opções com --ajuda.`,
|
|
@@ -46,9 +48,9 @@ function erroDaChamada(raiz, args) {
|
|
|
46
48
|
/** As linhas do diagnóstico: um bloco por achado e a linha de situação. */
|
|
47
49
|
function relatorio(crew) {
|
|
48
50
|
const achados = diagnosticar(crew);
|
|
49
|
-
if (!achados.length) return [MSG.emDia(crew.nome), 'CONSERTO:OK'];
|
|
51
|
+
if (!achados.length) return [MSG.emDia(crew.nome), ...notasDoHistorico(crew), 'CONSERTO:OK'];
|
|
50
52
|
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'];
|
|
53
|
+
return [MSG.titulo(crew.nome, achados.length), ...blocos, '', ...notasDoHistorico(crew), 'CONSERTO:PENDENTE'];
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/** As linhas de um `--aplicar`: o que foi gravado, as cópias e a linha de situação. */
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// aparece. Arquivo da entrega é citado com caminho relativo à pasta do LEIA-ME; o de origem,
|
|
3
3
|
// relativo ao projeto. Sem hora: as mesmas entradas geram os mesmos bytes. PT-BR fixo.
|
|
4
4
|
// Specs: fase-u3a1-pasta-de-entrega.md, §4 e §6, e fase-u3a2-entrega-no-projeto.md, §4 e §6
|
|
5
|
-
// (ressalvas, a cópia e "Sobre esta pasta"),
|
|
5
|
+
// (ressalvas, a cópia e "Sobre esta pasta"), e fase-u5c-execucao-registrada.md, regra 10 (o tema
|
|
6
|
+
// no título), no repositório do OpenCrew.
|
|
6
7
|
import { CANAIS, EDITAVEIS, OUTROS } from './canais.mjs';
|
|
7
8
|
import { frasesDePendencia } from './pendencias.mjs';
|
|
8
9
|
import { NAO_EDITE_O_WORD, passosDe } from './passos.mjs';
|
|
@@ -97,6 +98,7 @@ function sobre(d) {
|
|
|
97
98
|
/**
|
|
98
99
|
* @param {object} d
|
|
99
100
|
* @param {string} d.crew nome da crew · @param {string} d.run id da execução
|
|
101
|
+
* @param {string} [d.tema] o tema do registro da execução; com ele, o título é `{tema} ({run})`
|
|
100
102
|
* @param {string[]} d.pastas os canais presentes, na ordem das seções
|
|
101
103
|
* @param {object[]} d.arquivos `{ pasta, nome, tipo, origem, titulo, numerado }` — no LEIA-ME da
|
|
102
104
|
* cópia, só os que foram copiados
|
|
@@ -113,7 +115,7 @@ function sobre(d) {
|
|
|
113
115
|
export function montarLeiame(d) {
|
|
114
116
|
const temMd = d.arquivos.some((a) => a.nome.toLowerCase().endsWith('.md'));
|
|
115
117
|
const partes = [
|
|
116
|
-
`# Entrega — ${d.crew} — ${d.run}${d.reentrega > 1 ? ` (reentrega ${d.reentrega})` : ''}`,
|
|
118
|
+
`# Entrega — ${d.crew} — ${d.tema ? `${d.tema} (${d.run})` : d.run}${d.reentrega > 1 ? ` (reentrega ${d.reentrega})` : ''}`,
|
|
117
119
|
...antesDeUsar(d),
|
|
118
120
|
...d.pastas.flatMap((pasta) => secaoDoCanal(d, pasta)),
|
|
119
121
|
...secaoDaPasta(d, OUTROS, 'Outros arquivos'),
|
|
@@ -35,6 +35,7 @@ import { DOCUMENTOS, converterDocumentos, naoConferidoDoWord } from './entrega/d
|
|
|
35
35
|
import { MSG as COPIA, guardar } from './entrega/guardar.mjs';
|
|
36
36
|
import { gravarEntrega } from './entrega/gravar.mjs';
|
|
37
37
|
import { montarLeiame } from './entrega/leiame.mjs';
|
|
38
|
+
import { lerRegistro } from './execucao/registro.mjs';
|
|
38
39
|
import { lembrarDestino } from './entrega/lembrar.mjs';
|
|
39
40
|
import { nomear } from './entrega/nomes.mjs';
|
|
40
41
|
import { alertasDeTamanho, naoConferido, pendenciasPorPasta } from './entrega/pendencias.mjs';
|
|
@@ -95,6 +96,7 @@ async function montar(raiz, args, itens, aceitas) {
|
|
|
95
96
|
const presentes = new Set([...arquivos.map((a) => a.pasta), ...todas.keys()]);
|
|
96
97
|
const dados = {
|
|
97
98
|
crew: path.basename(path.resolve(raiz, args.crew)), run: args.run, arquivos, avisos,
|
|
99
|
+
tema: (await lerRegistro(path.resolve(raiz, args.crew, 'output', args.run)))?.tema ?? '',
|
|
98
100
|
...separarPendencias(todas, aceitas, args.aceitar),
|
|
99
101
|
pastas: Object.keys(CANAIS).filter((c) => presentes.has(c)),
|
|
100
102
|
alertas: await alertasDeTamanho(raiz, itens, arquivos, verificacao),
|
|
@@ -32,22 +32,29 @@ async function trocarNome(de, para, { renomear = fs.rename, esperar = pausa }) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Grava o
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* @param {object} estado
|
|
35
|
+
* Grava o texto num temporário da mesma pasta (`<arquivo>.<pid>.tmp`) e troca o nome: quem lê vê
|
|
36
|
+
* o arquivo antigo ou o novo, nunca um pela metade. Quando desiste, o arquivo fica como estava, o
|
|
37
|
+
* temporário é apagado e o erro sobe.
|
|
39
38
|
* @param {{ renomear?: Function, esperar?: Function }} [deps] SÓ PARA TESTE: a troca de nome
|
|
40
39
|
* (`(de, para) => Promise`) e a espera entre as tentativas (`(ms) => Promise`)
|
|
41
|
-
* @returns {Promise<boolean>} `false` quando desistiu: o arquivo fica como estava e o temporário é apagado
|
|
42
40
|
*/
|
|
43
|
-
export async function
|
|
41
|
+
export async function gravarTexto(arquivo, texto, deps = {}) {
|
|
44
42
|
const temporario = `${arquivo}.${process.pid}.tmp`;
|
|
45
43
|
try {
|
|
46
|
-
await fs.writeFile(temporario,
|
|
44
|
+
await fs.writeFile(temporario, texto);
|
|
47
45
|
await trocarNome(temporario, arquivo, deps);
|
|
48
|
-
|
|
49
|
-
} catch {
|
|
46
|
+
} catch (erro) {
|
|
50
47
|
await fs.rm(temporario, { force: true }).catch(() => {});
|
|
51
|
-
|
|
48
|
+
throw erro;
|
|
52
49
|
}
|
|
53
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Grava o `state.json`, inteiro ou nada (ver `gravarTexto`).
|
|
54
|
+
* @param {string} arquivo caminho do `state.json`
|
|
55
|
+
* @param {object} estado
|
|
56
|
+
* @returns {Promise<boolean>} `false` quando desistiu
|
|
57
|
+
*/
|
|
58
|
+
export async function gravarEstado(arquivo, estado, deps = {}) {
|
|
59
|
+
return gravarTexto(arquivo, `${JSON.stringify(estado, null, 2)}\n`, deps).then(() => true, () => false);
|
|
60
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Linha de comando do `execucao.mjs`: a crew, a ação, as opções e a linha de uso.
|
|
2
|
+
// Spec: fase-u5c-execucao-registrada.md, §3 (repositório do OpenCrew).
|
|
3
|
+
import { MSG } from '../comum.mjs';
|
|
4
|
+
import { RUN, limpar } from '../caminho/argumentos.mjs';
|
|
5
|
+
|
|
6
|
+
export const USO = 'Uso: node _opencrew/core/scripts/execucao.mjs <crew> <marcar|fechar|retomar> [opções]';
|
|
7
|
+
export const ACOES = ['marcar', 'fechar', 'retomar'];
|
|
8
|
+
/** Os resultados que cada evento do `marcar` aceita. */
|
|
9
|
+
export const RESULTADOS = { checkpoint: ['aprovado', 'corrigido', 'pulado'], revisao: ['aprovado', 'rejeitado'] };
|
|
10
|
+
export const FECHOS = ['aprovado', 'rejeitado', 'abortado', 'publicado'];
|
|
11
|
+
|
|
12
|
+
const OPCAO = /^--(run|passo|evento|resultado|nota|saida|tema)(?:=(.*))?$/s;
|
|
13
|
+
const INTEIRO = /^[1-9]\d{0,8}$/;
|
|
14
|
+
const lista = (itens) => `${itens.slice(0, -1).join(', ')} ou ${itens.at(-1)}`;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `argv` → `{ crew, acao, run, passo, evento, resultado, nota, saida, tema }`. Os dois primeiros
|
|
18
|
+
* argumentos soltos são a crew e a ação. Opção vale como `--nome valor` e `--nome=valor`; opção
|
|
19
|
+
* ausente fica `undefined`.
|
|
20
|
+
*/
|
|
21
|
+
export function lerArgs(argv) {
|
|
22
|
+
const soltos = [];
|
|
23
|
+
const opcoes = {};
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const [, nome, colado] = argv[i].match(OPCAO) ?? [];
|
|
26
|
+
if (!nome) soltos.push(argv[i]);
|
|
27
|
+
else if (colado !== undefined) opcoes[nome] = colado;
|
|
28
|
+
else opcoes[nome] = i + 1 < argv.length && !OPCAO.test(argv[i + 1]) ? argv[++i] : '';
|
|
29
|
+
}
|
|
30
|
+
const [crew, acao] = soltos.filter((s) => !s.startsWith('--'));
|
|
31
|
+
return { crew, acao, ...opcoes };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function erroDoMarcar({ passo, evento, resultado }) {
|
|
35
|
+
if (!passo) return MSG.faltaOpcao('--passo');
|
|
36
|
+
if (!INTEIRO.test(passo)) return 'O --passo é um número inteiro a partir de 1.';
|
|
37
|
+
if (!evento) return MSG.faltaOpcao('--evento');
|
|
38
|
+
if (!RESULTADOS[evento]) return 'O --evento é checkpoint ou revisao.';
|
|
39
|
+
if (!resultado) return MSG.faltaOpcao('--resultado');
|
|
40
|
+
return RESULTADOS[evento].includes(resultado) ? null : `O --resultado de ${evento} é ${lista(RESULTADOS[evento])}.`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function erroDoFechar({ resultado }) {
|
|
44
|
+
if (!resultado) return MSG.faltaOpcao('--resultado');
|
|
45
|
+
return FECHOS.includes(resultado) ? null : `O --resultado de fechar é ${lista(FECHOS)}.`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* O que falta ou está errado na linha de comando, antes de olhar o disco. Só o `retomar` dispensa
|
|
50
|
+
* o `--run` (sem ele, vale a execução aberta mais recente).
|
|
51
|
+
* @returns {string|null} o motivo em PT-BR, ou `null`
|
|
52
|
+
*/
|
|
53
|
+
export function erroDeArgumentos(args) {
|
|
54
|
+
const { crew, acao, run } = args;
|
|
55
|
+
if (!crew) return 'Falta o nome da crew.';
|
|
56
|
+
if (!acao) return `Falta a ação. Ações: ${ACOES.join(', ')}.`;
|
|
57
|
+
if (!ACOES.includes(acao)) return `Ação desconhecida: ${limpar(acao)}. Ações: ${ACOES.join(', ')}.`;
|
|
58
|
+
if (run === '' || (acao !== 'retomar' && !run)) return MSG.faltaOpcao('--run');
|
|
59
|
+
if (run !== undefined && !RUN.test(run)) return 'O --run só aceita letras, dígitos, ponto, sublinhado e hífen.';
|
|
60
|
+
if (acao === 'marcar') return erroDoMarcar(args);
|
|
61
|
+
return acao === 'fechar' ? erroDoFechar(args) : null;
|
|
62
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// A tabela do `runs.md` de uma crew, em funções puras: a linha de uma execução e o texto do
|
|
2
|
+
// arquivo com essa linha posta ou trocada. Nenhuma outra linha muda, byte a byte (fim de linha
|
|
3
|
+
// incluído). Usado pelo `execucao.mjs fechar` e pelo conserto do histórico.
|
|
4
|
+
// Spec: fase-u5c-execucao-registrada.md, §4 e regras 7 e 13 (repositório do OpenCrew).
|
|
5
|
+
|
|
6
|
+
const COLUNAS = '| Data | Run ID | Tema | Output | Score | Resultado |';
|
|
7
|
+
const TRACOS = '|------|--------|------|--------|-------|-----------|';
|
|
8
|
+
/** O `runs.md` que nasce quando a crew ainda não tem um. */
|
|
9
|
+
export const cabecalho = (crew) => `# Run History: ${crew}\n\n${COLUNAS}\n${TRACOS}\n`;
|
|
10
|
+
|
|
11
|
+
/** O `--resultado` do `fechar` como aparece na coluna Resultado. */
|
|
12
|
+
export const ROTULO = { aprovado: 'Aprovado', rejeitado: 'Rejeitado', abortado: 'Abortado', publicado: 'Publicado' };
|
|
13
|
+
export const REGISTRADA_DEPOIS = 'Registrada depois';
|
|
14
|
+
|
|
15
|
+
const celula = (texto) => String(texto ?? '').replace(/\|/g, '/').replace(/\s+/g, ' ').trim() || '—';
|
|
16
|
+
/** @param {{ data, run, tema, saida, score, resultado }} execucao célula vazia sai como `—` */
|
|
17
|
+
export const linhaDe = ({ data, run, tema, saida, score, resultado }) => `| ${[data, run, tema, saida, score, resultado].map(celula).join(' | ')} |`;
|
|
18
|
+
|
|
19
|
+
const dois = (n) => String(n).padStart(2, '0');
|
|
20
|
+
/** A data de uma execução: os 10 primeiros caracteres do `run_id`; id sem data, o dia de `quando`. */
|
|
21
|
+
export function dataDe(run, quando) {
|
|
22
|
+
const [doRun] = /^\d{4}-\d{2}-\d{2}/.exec(run) ?? [];
|
|
23
|
+
return doRun ?? `${quando.getFullYear()}-${dois(quando.getMonth() + 1)}-${dois(quando.getDate())}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const celulas = (linha) => (linha.trim().startsWith('|') ? linha.trim().replace(/\|$/, '').split('|').slice(1).map((c) => c.trim()) : null);
|
|
27
|
+
const ehTracos = (linha) => /^\s*\|[\s:|-]*-[\s:|-]*$/.test(linha);
|
|
28
|
+
|
|
29
|
+
/** Onde está a tabela: o índice da linha de traços e o da última linha de execução (ou o dos traços). */
|
|
30
|
+
function tabela(pedacos) {
|
|
31
|
+
const comRunId = (i) => i > 0 && (celulas(pedacos[i - 1]) ?? []).some((c) => /^run\s*id$/i.test(c));
|
|
32
|
+
let tracos = pedacos.findIndex((p, i) => ehTracos(p) && comRunId(i));
|
|
33
|
+
if (tracos < 0) tracos = pedacos.findIndex((p, i) => ehTracos(p) && i > 0 && celulas(pedacos[i - 1]));
|
|
34
|
+
if (tracos < 0) return null;
|
|
35
|
+
// Linha em branco no meio não encerra a tabela, se depois dela ainda há linha de execução.
|
|
36
|
+
let fim = tracos;
|
|
37
|
+
for (let i = tracos + 1; i < pedacos.length && (celulas(pedacos[i]) || !pedacos[i].trim()); i++) if (celulas(pedacos[i])) fim = i;
|
|
38
|
+
return { tracos, fim };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Os `run_id` que já têm linha no histórico. */
|
|
42
|
+
export function runsDoHistorico(texto) {
|
|
43
|
+
const pedacos = String(texto ?? '').split(/(?<=\n)/);
|
|
44
|
+
const t = tabela(pedacos);
|
|
45
|
+
return t ? pedacos.slice(t.tracos + 1, t.fim + 1).map((p) => celulas(p)?.[1]).filter(Boolean) : [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Onde a linha entra: logo abaixo dos traços, ou (`porData`) antes da primeira execução mais antiga. */
|
|
49
|
+
function posicao(pedacos, { tracos, fim }, data) {
|
|
50
|
+
if (data === null) return tracos + 1;
|
|
51
|
+
for (let i = tracos + 1; i <= fim; i++) if (celulas(pedacos[i]) && (celulas(pedacos[i])[0] ?? '') < data) return i;
|
|
52
|
+
return fim + 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* O texto do `runs.md` com a linha da execução: trocada, se o `run` já tem linha; senão, posta.
|
|
57
|
+
* @param {string|null} texto o arquivo como está no disco (`null`: não existe)
|
|
58
|
+
* @param {{ crew: string, run: string, linha: string, porData?: boolean }} dados `porData`: a linha
|
|
59
|
+
* entra na posição da data dela (conserto); sem isso, logo abaixo do cabeçalho (a mais nova em cima)
|
|
60
|
+
*/
|
|
61
|
+
export function comLinha(texto, { crew, run, linha, porData = false }) {
|
|
62
|
+
if (texto === null || texto === '') return `${cabecalho(crew)}${linha}\n`;
|
|
63
|
+
const fimDeLinha = /\r\n/.test(texto) ? '\r\n' : '\n';
|
|
64
|
+
const pedacos = texto.split(/(?<=\n)/);
|
|
65
|
+
const t = tabela(pedacos);
|
|
66
|
+
if (!t) return `${texto}${texto.endsWith('\n') ? '' : fimDeLinha}${fimDeLinha}${[COLUNAS, TRACOS, linha].join(fimDeLinha)}${fimDeLinha}`;
|
|
67
|
+
const igual = pedacos.findIndex((p, i) => i > t.tracos && i <= t.fim && celulas(p)?.[1] === run);
|
|
68
|
+
if (igual >= 0) pedacos[igual] = `${linha}${/\r?\n$/.exec(pedacos[igual])?.[0] ?? ''}`;
|
|
69
|
+
else {
|
|
70
|
+
const i = posicao(pedacos, t, porData ? linha.split('|')[1].trim() : null);
|
|
71
|
+
// Depois de uma linha sem quebra no fim (a última do arquivo), a quebra vem antes da linha nova.
|
|
72
|
+
const semQuebra = i > 0 && !pedacos[i - 1].endsWith('\n');
|
|
73
|
+
pedacos.splice(i, 0, semQuebra ? `${fimDeLinha}${linha}` : `${linha}${fimDeLinha}`);
|
|
74
|
+
}
|
|
75
|
+
return pedacos.join('');
|
|
76
|
+
}
|