@orkastery/cli 0.6.1 → 0.6.2
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/adapters/claude-code/agents/ork-check.md +1 -1
- package/adapters/claude-code/agents/ork-go.md +1 -1
- package/adapters/claude-code/agents/ork-goal.md +1 -1
- package/adapters/claude-code/agents/ork-master.md +1 -1
- package/adapters/claude-code/agents/ork-plan.md +1 -1
- package/adapters/claude-code/agents/ork-ship.md +1 -1
- package/adapters/claude-code/commands/ork.md +1 -1
- package/adapters/claude-code/mods/ork-torre/hooks/frases.ts +2 -0
- package/adapters/codex/skills/ork/SKILL.md +1 -1
- package/adapters/openclaw/openclaw.plugin.json +1 -1
- package/adapters/openclaw/package.json +1 -1
- package/assets/adapters-en/claude-code/agents/ork-check.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-go.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-goal.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-master.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-plan.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-ship.md +1 -1
- package/assets/adapters-en/claude-code/commands/ork.md +1 -1
- package/assets/adapters-en/codex/skills/ork/SKILL.md +1 -1
- package/assets/orkmind_fixture.py +4 -1
- package/assets/skills-en/core/orkastery-bootstrap/SKILL.md +1 -1
- package/assets/skills-en/phases/check-quality/SKILL.md +1 -1
- package/assets/skills-en/phases/go-implementation/SKILL.md +1 -1
- package/dist/adapters/claude-bg.js +3 -1
- package/dist/adapters/grok-mcp-servidor.js +22 -12
- package/dist/adapters/grok.js +1 -1
- package/dist/changelog-check.js +217 -0
- package/dist/check-agil.js +241 -5
- package/dist/ci.js +3 -2
- package/dist/claim-lint.js +29 -0
- package/dist/claims.js +4 -0
- package/dist/docs.js +7 -0
- package/dist/fila-pesada.js +121 -11
- package/dist/hitl-classificacao.js +1 -0
- package/dist/index.js +43 -25
- package/dist/intelligence-graph-cli.js +27 -9
- package/dist/locale-claims.js +16 -2
- package/dist/locale-grafo.js +56 -1
- package/dist/locale-maestro.js +38 -0
- package/dist/locale-network.js +8 -6
- package/dist/locale-onboarding.js +14 -0
- package/dist/locale-preflight.js +80 -0
- package/dist/locale-projetos.js +6 -0
- package/dist/locale-prompts.js +8 -8
- package/dist/locale-pulse.js +4 -0
- package/dist/locale-retry.js +16 -0
- package/dist/locale-security-scan.js +35 -1
- package/dist/locale-verify.js +15 -0
- package/dist/locale.js +24 -7
- package/dist/maestro-cli.js +12 -17
- package/dist/mcp-install.js +2 -1
- package/dist/mcp-server.js +20 -17
- package/dist/mcp-verify-async.js +353 -0
- package/dist/mcp-verify-worker.js +17 -0
- package/dist/mcp-verify.js +10 -1
- package/dist/pedido-conducao.js +51 -0
- package/dist/preflight.js +25 -26
- package/dist/prompts.js +4 -2
- package/dist/pulse.js +4 -2
- package/dist/rede.js +24 -12
- package/dist/retry.js +12 -0
- package/dist/security-scan/cli.js +2 -2
- package/dist/security-scan/report.js +1 -1
- package/dist/session-watcher.js +33 -7
- package/dist/verify.js +166 -34
- package/package.json +1 -1
- package/skills/core/orkastery-bootstrap/SKILL.md +1 -1
- package/skills/phases/check-quality/SKILL.md +1 -1
- package/skills/phases/go-implementation/SKILL.md +1 -1
package/dist/check-agil.js
CHANGED
|
@@ -38,6 +38,7 @@ exports.paralelismoDaVarreduraNoGrupo = paralelismoDaVarreduraNoGrupo;
|
|
|
38
38
|
exports.passosDoComando = passosDoComando;
|
|
39
39
|
exports.elosDoComando = elosDoComando;
|
|
40
40
|
exports.tirarPassosDoPreparo = tirarPassosDoPreparo;
|
|
41
|
+
exports.tirarPassosDoPreparoAposLeituras = tirarPassosDoPreparoAposLeituras;
|
|
41
42
|
exports.hashDoProduto = hashDoProduto;
|
|
42
43
|
exports.prepararNoAgile = prepararNoAgile;
|
|
43
44
|
exports.executorDaClaimNoAgile = executorDaClaimNoAgile;
|
|
@@ -47,6 +48,9 @@ exports.porQueDasLanes = porQueDasLanes;
|
|
|
47
48
|
exports.comandosNoAgile = comandosNoAgile;
|
|
48
49
|
exports.pecasDoRelatorio = pecasDoRelatorio;
|
|
49
50
|
exports.listasDasLanes = listasDasLanes;
|
|
51
|
+
exports.resultadosDasLanes = resultadosDasLanes;
|
|
52
|
+
exports.assinaturaDoAmbiente = assinaturaDoAmbiente;
|
|
53
|
+
exports.executorDaClaimPelasLanes = executorDaClaimPelasLanes;
|
|
50
54
|
/**
|
|
51
55
|
* RM-072 (D3 a D8 da ork-check5x): o motor do Agile Check no verify.
|
|
52
56
|
*
|
|
@@ -54,12 +58,16 @@ exports.listasDasLanes = listasDasLanes;
|
|
|
54
58
|
*
|
|
55
59
|
* - o preparo do Agile (`check.agile.preparo`, como o `tsgo` deste projeto) e, se ele falhar ou nao existir, o
|
|
56
60
|
* `verify.preparo`, com a peca `preparo` em fallback;
|
|
57
|
-
* -
|
|
61
|
+
* - quando ha lanes, elas rodam antes das claims; `node --test` dos mesmos arquivos, HEAD e ambiente usa o resultado
|
|
62
|
+
* nominal da lane, enquanto comando aberto ou evidencia divergente roda inteiro; se uma claim posterior mudar o
|
|
63
|
+
* produto, preparo e grupo rodam outra vez sobre o produto final;
|
|
64
|
+
* - as demais claims, uma por vez, contra o produto do preparo: a claim que comeca pelos passos do `verify.preparo` roda sem
|
|
58
65
|
* eles quando a guarda vale (o preparo passou, a arvore e a mesma do fim do preparo e o hash dos caminhos de
|
|
59
66
|
* `check.agile.produto` tambem); qualquer divergencia roda o comando inteiro, e o motivo vai ao ledger. Passo no meio
|
|
60
|
-
* da cadeia
|
|
67
|
+
* da cadeia so sai quando tudo antes dele e uma leitura `grep -q` fechada; qualquer outro elo pode mudar a entrada do
|
|
68
|
+
* build (como o mutante que edita o fonte e compila), entao a cadeia fica inteira. Quando
|
|
61
69
|
* so o produto mudou (a claim anterior compilou de novo no meio da cadeia), o preparo roda de novo antes da proxima
|
|
62
|
-
* claim que o reaproveitaria, ate `MAXIMO_DE_PREPAROS_REFEITOS` vezes, e uma vez
|
|
70
|
+
* claim que o reaproveitaria, ate `MAXIMO_DE_PREPAROS_REFEITOS` vezes, e uma vez depois das claims se elas mudarem o produto, com a peca
|
|
63
71
|
* `preparo-refeito`;
|
|
64
72
|
* - o `build` que e so passos do preparo, com a guarda valendo, sai do preparo desta rodada sem rodar de novo;
|
|
65
73
|
* - `typecheck`, as lanes de `test` (`check.agile.test`, ou o `verify.test`) e o `security_scan` num grupo paralelo, so
|
|
@@ -157,6 +165,78 @@ function tirarPassosDoPreparo(comando, passosDoPreparo) {
|
|
|
157
165
|
}
|
|
158
166
|
return { passos, resto };
|
|
159
167
|
}
|
|
168
|
+
/** Palavras de um comando simples, sem operadores ou expansoes do shell. */
|
|
169
|
+
function palavrasDoComando(comando) {
|
|
170
|
+
const palavras = [];
|
|
171
|
+
let atual = '', aspas = null, escape = false;
|
|
172
|
+
const fechar = () => { if (atual !== '')
|
|
173
|
+
palavras.push(atual); atual = ''; };
|
|
174
|
+
for (const c of comando) {
|
|
175
|
+
if (escape) {
|
|
176
|
+
atual += c;
|
|
177
|
+
escape = false;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (c === '\\' && aspas !== "'") {
|
|
181
|
+
escape = true;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (aspas) {
|
|
185
|
+
if (c === aspas)
|
|
186
|
+
aspas = null;
|
|
187
|
+
else if (aspas === '"' && (c === '$' || c === '`'))
|
|
188
|
+
return null;
|
|
189
|
+
else
|
|
190
|
+
atual += c;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (c === "'" || c === '"') {
|
|
194
|
+
aspas = c;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (/\s/.test(c)) {
|
|
198
|
+
fechar();
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (';&|<>$`(){}'.includes(c))
|
|
202
|
+
return null;
|
|
203
|
+
atual += c;
|
|
204
|
+
}
|
|
205
|
+
if (escape || aspas)
|
|
206
|
+
return null;
|
|
207
|
+
fechar();
|
|
208
|
+
return palavras;
|
|
209
|
+
}
|
|
210
|
+
/** Uma leitura sem efeito colateral aceita antes do preparo unico. */
|
|
211
|
+
function comandoGrepQ(comando) {
|
|
212
|
+
const palavras = palavrasDoComando(comando);
|
|
213
|
+
if (!palavras || palavras[0] !== 'grep' || palavras[1] !== '-q')
|
|
214
|
+
return false;
|
|
215
|
+
const argumentos = palavras[2] === '--' ? palavras.slice(3) : palavras.slice(2);
|
|
216
|
+
return argumentos.length === 2 && argumentos[0] !== '' && argumentos[1] !== '' && !argumentos[1].startsWith('-');
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Tira passos exatos do preparo que aparecem depois apenas de `grep -q`. A leitura nao altera a entrada do build;
|
|
220
|
+
* qualquer outro elo antes do preparo conserva a regra antiga e faz a cadeia rodar inteira.
|
|
221
|
+
*/
|
|
222
|
+
function tirarPassosDoPreparoAposLeituras(comando, passosDoPreparo) {
|
|
223
|
+
const elos = elosDoComando(comando);
|
|
224
|
+
if (!elos)
|
|
225
|
+
return { passos: [], resto: comando.trim() };
|
|
226
|
+
const passos = [];
|
|
227
|
+
const resto = [];
|
|
228
|
+
let somenteLeiturasAntes = true;
|
|
229
|
+
for (const elo of elos) {
|
|
230
|
+
if (somenteLeiturasAntes && passosDoPreparo.includes(elo)) {
|
|
231
|
+
passos.push(elo);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
resto.push(elo);
|
|
235
|
+
if (!comandoGrepQ(elo))
|
|
236
|
+
somenteLeiturasAntes = false;
|
|
237
|
+
}
|
|
238
|
+
return passos.length > 0 ? { passos, resto: resto.join(SEPARADOR) } : { passos: [], resto: comando.trim() };
|
|
239
|
+
}
|
|
160
240
|
/**
|
|
161
241
|
* O sha256 do conteudo dos caminhos do produto (arquivos em ordem, com o caminho relativo). Caminho ausente entra como
|
|
162
242
|
* ausente, para o produto que some nao passar por igual; link simbolico entra pelo destino que ele aponta, sem segui-lo.
|
|
@@ -256,7 +336,8 @@ function doPreparo(nome, comando, preparo, passos) {
|
|
|
256
336
|
function executorDaClaimNoAgile(manifesto, executor, vigente, refazer) {
|
|
257
337
|
const passosDoPreparo = manifesto.verify.preparo ? passosDoComando(manifesto.verify.preparo) : [];
|
|
258
338
|
return (nome, comando, cwd) => {
|
|
259
|
-
const
|
|
339
|
+
const daFrente = tirarPassosDoPreparo(comando, passosDoPreparo);
|
|
340
|
+
const { passos, resto } = daFrente.passos.length > 0 ? daFrente : tirarPassosDoPreparoAposLeituras(comando, passosDoPreparo);
|
|
260
341
|
if (passos.length === 0)
|
|
261
342
|
return executor(nome, comando, cwd);
|
|
262
343
|
let motivo = vigente.guarda.conferir();
|
|
@@ -353,7 +434,8 @@ function testDasLanes(lanes) {
|
|
|
353
434
|
...(lanes.some((l) => l.resultado.stealPct !== undefined) ? { stealPct: lanes.find((l) => l.resultado.stealPct !== undefined).resultado.stealPct } : {}),
|
|
354
435
|
...(relogio.length ? { relogioSobSteal: relogio } : {}),
|
|
355
436
|
lanes: lanes.map((l) => ({ lane: l.lane, comando: l.resultado.comando, ok: l.resultado.ok, code: l.resultado.code,
|
|
356
|
-
...(l.resultado.causa ? { causa: l.resultado.causa } : {}), duracaoMs: l.resultado.duracaoMs ?? 0
|
|
437
|
+
...(l.resultado.causa ? { causa: l.resultado.causa } : {}), duracaoMs: l.resultado.duracaoMs ?? 0,
|
|
438
|
+
...(l.resultado.testeQueCaiu?.length ? { testeQueCaiu: l.resultado.testeQueCaiu } : {}) })),
|
|
357
439
|
};
|
|
358
440
|
}
|
|
359
441
|
/**
|
|
@@ -526,3 +608,157 @@ function listasDasLanes(linhas) {
|
|
|
526
608
|
return { lane: l.lane, arquivos, sha256: (0, node_crypto_1.createHash)('sha256').update(arquivos.join('\n')).digest('hex') };
|
|
527
609
|
});
|
|
528
610
|
}
|
|
611
|
+
/** Evidencia nominal produzida pelo reporter, sem confiar em campos abertos do JSONL. */
|
|
612
|
+
function resultadosDasLanes(linhas) {
|
|
613
|
+
return linhas.filter((l) => l.tipo === 'resultados' && typeof l.lane === 'string' && typeof l.head === 'string'
|
|
614
|
+
&& typeof l.ambiente === 'string' && Array.isArray(l.testes))
|
|
615
|
+
.map((l) => ({ lane: l.lane, head: l.head, ambiente: l.ambiente,
|
|
616
|
+
testes: l.testes.flatMap((t) => {
|
|
617
|
+
if (!t || typeof t !== 'object' || Array.isArray(t))
|
|
618
|
+
return [];
|
|
619
|
+
const x = t;
|
|
620
|
+
if (typeof x.arquivo !== 'string' || typeof x.nome !== 'string' || !Number.isInteger(x.nesting)
|
|
621
|
+
|| !['pass', 'fail', 'skip', 'todo'].includes(String(x.estado)))
|
|
622
|
+
return [];
|
|
623
|
+
return [{ arquivo: x.arquivo, nome: x.nome, nesting: x.nesting,
|
|
624
|
+
estado: x.estado }];
|
|
625
|
+
}) }));
|
|
626
|
+
}
|
|
627
|
+
/** Hash do ambiente funcional; transporte do CHECK, npm e bookkeeping do shell nao mudam o teste. */
|
|
628
|
+
function assinaturaDoAmbiente(env) {
|
|
629
|
+
const ignorada = (nome) => nome.startsWith('npm_') || nome.startsWith('NPM_')
|
|
630
|
+
|| ['ORK_CHECK_RELATORIO', 'ORK_CHECK_CPUS_DA_LANE', 'ORK_CHECK_CPUS_DOCKER',
|
|
631
|
+
'NODE_TEST_CONTEXT', 'NODE_COMPILE_CACHE', 'PWD', 'OLDPWD', 'SHLVL', '_', 'INIT_CWD'].includes(nome);
|
|
632
|
+
const pares = Object.entries(env).filter(([nome, valor]) => valor !== undefined && !ignorada(nome))
|
|
633
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
634
|
+
return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(pares)).digest('hex');
|
|
635
|
+
}
|
|
636
|
+
/** Le somente a gramatica aceita para reaproveitar: `node --test [--test-name-pattern P] arquivo...`. */
|
|
637
|
+
function comandoNodeTest(comando, cwd) {
|
|
638
|
+
const palavras = palavrasDoComando(comando);
|
|
639
|
+
if (!palavras)
|
|
640
|
+
return null;
|
|
641
|
+
if (palavras[0] !== 'node' || palavras[1] !== '--test')
|
|
642
|
+
return null;
|
|
643
|
+
let padrao;
|
|
644
|
+
const arquivos = [];
|
|
645
|
+
for (let i = 2; i < palavras.length; i++) {
|
|
646
|
+
const p = palavras[i];
|
|
647
|
+
if (p === '--test-name-pattern') {
|
|
648
|
+
if (padrao !== undefined || !palavras[++i])
|
|
649
|
+
return null;
|
|
650
|
+
padrao = palavras[i];
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (p.startsWith('--test-name-pattern=')) {
|
|
654
|
+
if (padrao !== undefined)
|
|
655
|
+
return null;
|
|
656
|
+
padrao = p.slice('--test-name-pattern='.length);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (p.startsWith('-') || /[*?\[]/.test(p))
|
|
660
|
+
return null;
|
|
661
|
+
const absoluto = path.resolve(cwd, p);
|
|
662
|
+
const relativo = path.relative(path.join(cwd, 'core', 'dist-test', 'test'), absoluto);
|
|
663
|
+
if (!relativo || relativo.startsWith('..') || path.isAbsolute(relativo) || relativo !== path.basename(relativo)
|
|
664
|
+
|| !relativo.endsWith('.test.js'))
|
|
665
|
+
return null;
|
|
666
|
+
arquivos.push(path.basename(absoluto));
|
|
667
|
+
}
|
|
668
|
+
if (padrao !== undefined) {
|
|
669
|
+
try {
|
|
670
|
+
void new RegExp(padrao);
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return arquivos.length > 0 ? { arquivos: [...new Set(arquivos)], ...(padrao !== undefined ? { padrao } : {}) } : null;
|
|
677
|
+
}
|
|
678
|
+
/** A forma fechada usada pelas claims para provar contagem, zero falhas e zero skips no TAP. */
|
|
679
|
+
function auditoriaTap(comando, cwd) {
|
|
680
|
+
const divisor = ' | ';
|
|
681
|
+
const i = comando.indexOf(divisor);
|
|
682
|
+
if (i < 0 || comando.indexOf(divisor, i + divisor.length) >= 0)
|
|
683
|
+
return null;
|
|
684
|
+
const node = comando.slice(0, i).trim();
|
|
685
|
+
const awk = comando.slice(i + divisor.length).trim();
|
|
686
|
+
const pedido = comandoNodeTest(node.replace(/^node --test-reporter=tap\b/, 'node --test'), cwd);
|
|
687
|
+
if (!pedido || pedido.arquivos.length !== 1 || pedido.padrao !== undefined)
|
|
688
|
+
return null;
|
|
689
|
+
const m = /^awk '\/\^ \*not ok\/{f\+\+} \/# SKIP\/{s\+\+} \/\^ \*ok \[0-9\]\+ - ([A-Z][A-Z0-9_-]*)\[0-9\]\+:\/{p\+\+} END\{exit !\(p>=([1-9][0-9]*) && f==0 && s==0\)\}'$/.exec(awk);
|
|
690
|
+
return m ? { pedido, prefixo: m[1], minimo: Number(m[2]) } : null;
|
|
691
|
+
}
|
|
692
|
+
function resultadoReaproveitado(nome, comando, pedido, usadas, esperado, nomes) {
|
|
693
|
+
const lanes = [...new Set(usadas.filter((e) => e.testes.some((t) => pedido.arquivos.includes(t.arquivo))).map((e) => e.lane))];
|
|
694
|
+
return { nome, comando, ok: true, code: 0, causa: 'exit', duracaoMs: 0, executado: true,
|
|
695
|
+
resumo: `reaproveitado das lanes ${lanes.join(', ')} desta rodada`,
|
|
696
|
+
lane: { reaproveitada: true, lanes, arquivos: pedido.arquivos,
|
|
697
|
+
nomes: nomes ?? (pedido.padrao === undefined ? pedido.arquivos.map((a) => `${a}:todos`) : pedido.arquivos.map((a) => `${a}:/${pedido.padrao}/`)),
|
|
698
|
+
head: esperado.head, ambiente: esperado.ambiente } };
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Reaproveita uma claim somente quando a lane terminou todos os arquivos pedidos no mesmo HEAD e ambiente. Um arquivo
|
|
702
|
+
* inteiro verde e evidencia mais forte que `--test-name-pattern`: o padrao fica registrado em `nomes`.
|
|
703
|
+
*/
|
|
704
|
+
function executorDaClaimPelasLanes(base, evidencias, esperado) {
|
|
705
|
+
const assinatura = assinaturaDoAmbiente(esperado.ambiente);
|
|
706
|
+
return (nome, comando, cwd) => {
|
|
707
|
+
const executar = esperado.executorEquivalente ?? base;
|
|
708
|
+
if (esperado.podeReaproveitar?.() === false)
|
|
709
|
+
return executar(nome, comando, cwd);
|
|
710
|
+
const elos = elosDoComando(comando);
|
|
711
|
+
if (!elos)
|
|
712
|
+
return base(nome, comando, cwd);
|
|
713
|
+
const planejados = [];
|
|
714
|
+
for (const elo of elos) {
|
|
715
|
+
if (comandoGrepQ(elo)) {
|
|
716
|
+
planejados.push({ tipo: 'grep', comando: elo });
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const tap = auditoriaTap(elo, cwd);
|
|
720
|
+
const pedido = tap?.pedido ?? comandoNodeTest(elo, cwd);
|
|
721
|
+
if (!pedido)
|
|
722
|
+
return base(nome, comando, cwd);
|
|
723
|
+
const usadas = evidencias.filter((e) => e.head === esperado.head && e.ambiente === assinatura
|
|
724
|
+
&& e.testes.some((t) => pedido.arquivos.includes(t.arquivo)));
|
|
725
|
+
const cobertos = pedido.arquivos.every((arquivo) => {
|
|
726
|
+
const vistos = usadas.flatMap((e) => e.testes.filter((t) => t.arquivo === arquivo));
|
|
727
|
+
if (vistos.length === 0 || vistos.some((t) => t.estado === 'fail'))
|
|
728
|
+
return false;
|
|
729
|
+
if (vistos.some((t) => t.estado === 'skip' || t.estado === 'todo'))
|
|
730
|
+
return false;
|
|
731
|
+
if (!tap)
|
|
732
|
+
return true;
|
|
733
|
+
const doArquivo = vistos.filter((t) => t.nesting === 0);
|
|
734
|
+
return doArquivo.filter((t) => t.estado === 'pass' && new RegExp(`^${tap.prefixo}[0-9]+:`).test(t.nome)).length >= tap.minimo;
|
|
735
|
+
});
|
|
736
|
+
if (!cobertos)
|
|
737
|
+
return executar(nome, comando, cwd);
|
|
738
|
+
const nomes = tap ? pedido.arquivos.map((a) => `${a}:${tap.prefixo}>=${tap.minimo};sem-falha;sem-skip`) : undefined;
|
|
739
|
+
planejados.push({ tipo: 'lane', resultado: resultadoReaproveitado(nome, elo, pedido, usadas, { head: esperado.head, ambiente: assinatura }, nomes) });
|
|
740
|
+
}
|
|
741
|
+
let duracaoMs = 0;
|
|
742
|
+
const resumos = [];
|
|
743
|
+
let ultimo = null;
|
|
744
|
+
const lanes = new Set(), arquivos = new Set(), nomes = new Set();
|
|
745
|
+
for (const item of planejados) {
|
|
746
|
+
const r = item.tipo === 'grep' ? base(nome, item.comando, cwd) : item.resultado;
|
|
747
|
+
duracaoMs += r.duracaoMs ?? 0;
|
|
748
|
+
resumos.push(r.resumo);
|
|
749
|
+
ultimo = r;
|
|
750
|
+
if (!r.ok)
|
|
751
|
+
return { ...r, comando, comandoExecutado: item.tipo === 'grep' ? item.comando : r.comando, duracaoMs };
|
|
752
|
+
for (const lane of r.lane?.lanes ?? [])
|
|
753
|
+
lanes.add(lane);
|
|
754
|
+
for (const arquivo of r.lane?.arquivos ?? [])
|
|
755
|
+
arquivos.add(arquivo);
|
|
756
|
+
for (const n of r.lane?.nomes ?? [])
|
|
757
|
+
nomes.add(n);
|
|
758
|
+
}
|
|
759
|
+
if (!ultimo || lanes.size === 0)
|
|
760
|
+
return base(nome, comando, cwd);
|
|
761
|
+
return { ...ultimo, nome, comando, duracaoMs, resumo: resumos.filter(Boolean).join('\n'),
|
|
762
|
+
lane: { reaproveitada: true, lanes: [...lanes], arquivos: [...arquivos], nomes: [...nomes], head: esperado.head, ambiente: assinatura } };
|
|
763
|
+
};
|
|
764
|
+
}
|
package/dist/ci.js
CHANGED
|
@@ -389,10 +389,11 @@ function lintDoBundle(claims) {
|
|
|
389
389
|
for (const claim of claims) {
|
|
390
390
|
for (const achado of (0, claim_lint_1.analisarComandos)(claim.verificar)) {
|
|
391
391
|
const linha = (0, claim_lint_1.linhaDoLintDeClaim)(claim.id, achado);
|
|
392
|
-
|
|
392
|
+
const bloqueiaNova = achado.regra === 'suite-inteira' || (0, claim_lint_1.regraFatalDeClaim)(achado.regra);
|
|
393
|
+
if (bloqueiaNova && claim.lint !== undefined)
|
|
393
394
|
recusas.push(linha);
|
|
394
395
|
else
|
|
395
|
-
avisos.push(
|
|
396
|
+
avisos.push(bloqueiaNova ? anteriorAoLint(linha) : linha);
|
|
396
397
|
}
|
|
397
398
|
}
|
|
398
399
|
return { recusas, avisos };
|
package/dist/claim-lint.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* alheia e mexer em estado de quem nao pediu (D16). Nada de data de corte no codigo.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.REGRAS_FATAIS_DE_CLAIM = void 0;
|
|
20
|
+
exports.regraFatalDeClaim = regraFatalDeClaim;
|
|
19
21
|
exports.analisarComando = analisarComando;
|
|
20
22
|
exports.analisarComandos = analisarComandos;
|
|
21
23
|
exports.linhaDoLintDeClaim = linhaDoLintDeClaim;
|
|
@@ -29,7 +31,18 @@ const CORRECAO = {
|
|
|
29
31
|
'suite-inteira': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['suite-inteira'],
|
|
30
32
|
'sha-intermediario': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['sha-intermediario'],
|
|
31
33
|
'contagem-de-commits': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['contagem-de-commits'],
|
|
34
|
+
'binario-rg': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['binario-rg'],
|
|
35
|
+
'ork-fora-do-path': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['ork-fora-do-path'],
|
|
36
|
+
'estado-local': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['estado-local'],
|
|
37
|
+
'aspas-escapadas': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['aspas-escapadas'],
|
|
32
38
|
};
|
|
39
|
+
/** RM-037: estes achados tornam uma claim nova inexequivel no checkout do runner. */
|
|
40
|
+
exports.REGRAS_FATAIS_DE_CLAIM = new Set([
|
|
41
|
+
'binario-rg', 'ork-fora-do-path', 'estado-local', 'aspas-escapadas',
|
|
42
|
+
]);
|
|
43
|
+
function regraFatalDeClaim(regra) {
|
|
44
|
+
return exports.REGRAS_FATAIS_DE_CLAIM.has(regra);
|
|
45
|
+
}
|
|
33
46
|
/**
|
|
34
47
|
* (a) `npm [--prefix <dir>] [run] test` sem subcomando (`test:ci` nao casa) e sem arquivos
|
|
35
48
|
* nomeados depois de `--` (o comando que nomeia `.test.ts`/`.js` depois de `--` e focado).
|
|
@@ -41,8 +54,24 @@ const SO_ANCESTRALIDADE = /merge-base\s+--is-ancestor\s+\S+\s+\S+/g;
|
|
|
41
54
|
/** (b2) Contagem de commits comparada a um numero. */
|
|
42
55
|
const CONTAGEM = /rev-list\s+(?:[^|;&]*\s)?--count\b|\bgit\s+log\b[^|;&]*\|\s*wc\s+-l\b/;
|
|
43
56
|
const COMPARACAO = /-(?:eq|ne|gt|lt|ge|le)\s+['"]?\d|[^=!<>]==?\s*['"]?\d|grep\s+-(?:q?x|xq)\s+['"]?\d/;
|
|
57
|
+
/** Executavel em posicao de comando, no inicio ou depois de um operador do shell. */
|
|
58
|
+
function executavelDireto(comando, nome) {
|
|
59
|
+
return new RegExp(`(?:^|&&|\\|\\||[;|(])\\s*(?:command\\s+|env\\s+)?(${nome})(?=\\s|$)`).exec(comando);
|
|
60
|
+
}
|
|
44
61
|
function analisarComando(comando) {
|
|
45
62
|
const achados = [];
|
|
63
|
+
const rg = executavelDireto(comando, 'rg');
|
|
64
|
+
if (rg)
|
|
65
|
+
achados.push({ regra: 'binario-rg', trecho: rg[1], correcao: CORRECAO['binario-rg'] });
|
|
66
|
+
const ork = executavelDireto(comando, 'ork');
|
|
67
|
+
if (ork)
|
|
68
|
+
achados.push({ regra: 'ork-fora-do-path', trecho: ork[1], correcao: CORRECAO['ork-fora-do-path'] });
|
|
69
|
+
const estado = /(?:^|[\s"'=])((?:\.\/)?\.orkastery(?:[\\/][^\s"']*)?)/.exec(comando);
|
|
70
|
+
if (estado)
|
|
71
|
+
achados.push({ regra: 'estado-local', trecho: estado[1], correcao: CORRECAO['estado-local'] });
|
|
72
|
+
const aspas = /\\x(?:22|27)/i.exec(comando);
|
|
73
|
+
if (aspas)
|
|
74
|
+
achados.push({ regra: 'aspas-escapadas', trecho: aspas[0], correcao: CORRECAO['aspas-escapadas'] });
|
|
46
75
|
const suite = SUITE_INTEIRA.exec(comando);
|
|
47
76
|
if (suite)
|
|
48
77
|
achados.push({ regra: 'suite-inteira', trecho: suite[0].trim(), correcao: CORRECAO['suite-inteira'] });
|
package/dist/claims.js
CHANGED
|
@@ -154,6 +154,10 @@ function adicionarClaim(raiz, threadId, opcoes) {
|
|
|
154
154
|
verificar: opcoes.verificar,
|
|
155
155
|
origem: opcoes.origem,
|
|
156
156
|
});
|
|
157
|
+
const recusas = (claim.lint ?? []).filter((a) => (0, claim_lint_1.regraFatalDeClaim)(a.regra));
|
|
158
|
+
if (recusas.length > 0) {
|
|
159
|
+
throw new locale_1.ErroDoCatalogo((m) => m.claims.lintRecusado(recusas.map((a) => `claims.lint.${a.regra}: ${(0, claim_lint_1.linhaDoLintDeClaim)(claim.id, a)}`).join('\n ')));
|
|
160
|
+
}
|
|
157
161
|
const negativa = claim.negativa;
|
|
158
162
|
const verificar = claim.verificar;
|
|
159
163
|
gravarClaim(raiz, claim);
|
package/dist/docs.js
CHANGED
|
@@ -82,6 +82,7 @@ const ledger_1 = require("./ledger");
|
|
|
82
82
|
const branch_de_estado_1 = require("./branch-de-estado");
|
|
83
83
|
const locale_1 = require("./locale");
|
|
84
84
|
const merge_da_entrega_1 = require("./merge-da-entrega");
|
|
85
|
+
const changelog_check_1 = require("./changelog-check");
|
|
85
86
|
exports.DIR_PRODUTO = 'docs/produto';
|
|
86
87
|
exports.DIR_ROADMAP = 'docs/roadmap';
|
|
87
88
|
exports.DIR_PADROES = 'docs/padroes';
|
|
@@ -265,6 +266,12 @@ function verificarDocs(raiz, opcoes = {}) {
|
|
|
265
266
|
if (!opcoes.semGit && !base) {
|
|
266
267
|
achados.push(aviso('.', undefined, 'docs.git.base', (m) => m.baseAusente(opcoes.baseBranch ?? 'main'), (m) => m.baseAusenteCorrecao));
|
|
267
268
|
}
|
|
269
|
+
if (opcoes.pr && base) {
|
|
270
|
+
const changelog = (0, changelog_check_1.checarChangelog)(raiz, { base, head: 'HEAD', baseCarimbada: opcoes.baseCarimbada });
|
|
271
|
+
if (!changelog.ok) {
|
|
272
|
+
achados.push(erro(changelog_check_1.CHANGELOG, undefined, changelog.motivo ?? 'changelog.invalido', () => changelog.detalhe, () => changelog.correcao ?? 'corrija o CHANGELOG e repita a verificação'));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
268
275
|
const comandos = opcoes.ajudaDoCli ? comandosDaAjuda(opcoes.ajudaDoCli) : null;
|
|
269
276
|
for (const d of docs) {
|
|
270
277
|
if (!TIPOS.includes(d.tipo))
|
package/dist/fila-pesada.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.ErroDaFilaPesada = exports.ENV_DONO_DA_FILA = exports.CONTRATO_FILA_PESADA = void 0;
|
|
37
|
+
exports.avaliarVidaDoOcupante = avaliarVidaDoOcupante;
|
|
37
38
|
exports.kernelDisponivel = kernelDisponivel;
|
|
38
39
|
exports.tentarTravaExclusiva = tentarTravaExclusiva;
|
|
39
40
|
exports.dirDaFilaPesada = dirDaFilaPesada;
|
|
@@ -66,6 +67,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
66
67
|
const path = __importStar(require("node:path"));
|
|
67
68
|
const node_crypto_1 = require("node:crypto");
|
|
68
69
|
const node_child_process_1 = require("node:child_process");
|
|
70
|
+
const node_worker_threads_1 = require("node:worker_threads");
|
|
69
71
|
const estado_thread_1 = require("./estado-thread");
|
|
70
72
|
const horario_1 = require("./horario");
|
|
71
73
|
exports.CONTRATO_FILA_PESADA = 'ork.fila-pesada/v1';
|
|
@@ -75,11 +77,27 @@ const FLOCK = '/usr/bin/flock';
|
|
|
75
77
|
const DIR = 'fila-pesada';
|
|
76
78
|
/** Entre uma tentativa e outra. A vez costuma durar minutos; meio segundo não pesa em ninguém. */
|
|
77
79
|
const PAUSA_MS = 500;
|
|
80
|
+
/** A batida e complementar ao flock: atravessa a observacao de um sandbox com outro namespace de PID. */
|
|
81
|
+
const BATIDA_CADA_MS = 1_000;
|
|
82
|
+
const BATIDA_VALE_MS = 5_000;
|
|
83
|
+
function avaliarVidaDoOcupante(e) {
|
|
84
|
+
if (e.trava === 'ocupada')
|
|
85
|
+
return { vida: 'vivo', prova: 'trava-kernel' };
|
|
86
|
+
if (e.trava === 'livre')
|
|
87
|
+
return { vida: 'morto', prova: 'trava-livre' };
|
|
88
|
+
if (e.batidaValida)
|
|
89
|
+
return { vida: 'vivo', prova: 'batida' };
|
|
90
|
+
if (e.mesmoHost && e.mesmoEspacoPids && e.processo === 'vivo')
|
|
91
|
+
return { vida: 'vivo', prova: 'processo' };
|
|
92
|
+
if (e.mesmoHost && e.mesmoEspacoPids && e.processo === 'ausente')
|
|
93
|
+
return { vida: 'morto', prova: 'processo-ausente' };
|
|
94
|
+
return { vida: 'desconhecido', prova: 'indeterminada' };
|
|
95
|
+
}
|
|
78
96
|
/** A recusa é resposta, não falha: o CLI a imprime como a da condução, e sai com 3. */
|
|
79
97
|
class ErroDaFilaPesada extends Error {
|
|
80
98
|
recusa;
|
|
81
99
|
constructor(recusa) {
|
|
82
|
-
super(
|
|
100
|
+
super(`${recusa.motivo}: ${recusa.texto}`);
|
|
83
101
|
this.name = 'ErroDaFilaPesada';
|
|
84
102
|
this.recusa = recusa;
|
|
85
103
|
}
|
|
@@ -88,11 +106,17 @@ exports.ErroDaFilaPesada = ErroDaFilaPesada;
|
|
|
88
106
|
function kernelDisponivel() {
|
|
89
107
|
return process.platform === 'linux' && fs.existsSync(FLOCK);
|
|
90
108
|
}
|
|
91
|
-
/**
|
|
92
|
-
function
|
|
109
|
+
/** Diferencia a contencao provada do erro de observacao; erro nunca vira trava livre. */
|
|
110
|
+
function observarTrava(fd) {
|
|
93
111
|
const r = (0, node_child_process_1.spawnSync)(FLOCK, ['--exclusive', '--nonblock', '3'], { stdio: ['ignore', 'ignore', 'ignore', fd], timeout: 5000 });
|
|
94
|
-
|
|
112
|
+
if (!r.error && r.status === 0)
|
|
113
|
+
return 'adquirida';
|
|
114
|
+
if (!r.error && r.status === 1)
|
|
115
|
+
return 'ocupada';
|
|
116
|
+
return 'desconhecida';
|
|
95
117
|
}
|
|
118
|
+
/** Tomar a trava exige prova positiva. Contencao e observacao inconclusiva falham fechadas. */
|
|
119
|
+
function trancar(fd) { return observarTrava(fd) === 'adquirida'; }
|
|
96
120
|
function abrirTrava(arquivo, criar) {
|
|
97
121
|
const fd = fs.openSync(arquivo, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW | (criar ? fs.constants.O_CREAT : 0), 0o600);
|
|
98
122
|
const st = fs.fstatSync(fd);
|
|
@@ -210,11 +234,57 @@ function minhaVez(dir, senha) {
|
|
|
210
234
|
}
|
|
211
235
|
return true;
|
|
212
236
|
}
|
|
237
|
+
function hashLocal(valor) { return (0, node_crypto_1.createHash)('sha256').update(valor).digest('hex').slice(0, 24); }
|
|
238
|
+
/** Identidades opacas: provam igualdade sem publicar hostname, boot id ou inode do namespace. */
|
|
239
|
+
function identidadeDaMaquina() {
|
|
240
|
+
let host = null, espacoPids = null;
|
|
241
|
+
try {
|
|
242
|
+
host = hashLocal(fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim());
|
|
243
|
+
}
|
|
244
|
+
catch { /* nao observavel */ }
|
|
245
|
+
try {
|
|
246
|
+
espacoPids = hashLocal(fs.readlinkSync('/proc/self/ns/pid'));
|
|
247
|
+
}
|
|
248
|
+
catch { /* nao observavel */ }
|
|
249
|
+
return { host, espacoPids };
|
|
250
|
+
}
|
|
213
251
|
function gravarDono(fd, dono, posse) {
|
|
214
252
|
const bytes = Buffer.from(JSON.stringify({ contrato: exports.CONTRATO_FILA_PESADA, ...dono, posse }) + '\n', 'utf8');
|
|
215
253
|
fs.ftruncateSync(fd, 0);
|
|
216
254
|
fs.writeSync(fd, bytes, 0, bytes.length, 0);
|
|
217
255
|
}
|
|
256
|
+
function arquivoDaBatida(dir, slot, posse) {
|
|
257
|
+
return path.join(dir, `vaga-${slot}.${posse}.heartbeat.json`);
|
|
258
|
+
}
|
|
259
|
+
/** Worker nativo: a batida continua enquanto o verify sincrono bloqueia a thread JS, sem processo shell perdido. */
|
|
260
|
+
function iniciarBatida(dir, slot, posse) {
|
|
261
|
+
const arquivo = arquivoDaBatida(dir, slot, posse);
|
|
262
|
+
try {
|
|
263
|
+
const worker = new node_worker_threads_1.Worker(`
|
|
264
|
+
const fs = require('node:fs');
|
|
265
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
266
|
+
const { randomUUID } = require('node:crypto');
|
|
267
|
+
let ativo = true;
|
|
268
|
+
parentPort.on('message', () => { ativo = false; });
|
|
269
|
+
function bater() {
|
|
270
|
+
if (!ativo) return;
|
|
271
|
+
const tmp = workerData.arquivo + '.' + randomUUID() + '.tmp';
|
|
272
|
+
try {
|
|
273
|
+
fs.writeFileSync(tmp, JSON.stringify({ contrato: 'ork.fila-pesada-heartbeat/v1', posse: workerData.posse,
|
|
274
|
+
expiraEm: new Date(Date.now() + workerData.validadeMs).toISOString() }) + '\\n', { flag: 'wx', mode: 0o600 });
|
|
275
|
+
fs.renameSync(tmp, workerData.arquivo);
|
|
276
|
+
} catch (_) { try { fs.unlinkSync(tmp); } catch (_) {} }
|
|
277
|
+
if (ativo) setTimeout(bater, workerData.intervaloMs);
|
|
278
|
+
}
|
|
279
|
+
bater();
|
|
280
|
+
`, { eval: true, workerData: { arquivo, posse, intervaloMs: BATIDA_CADA_MS, validadeMs: BATIDA_VALE_MS } });
|
|
281
|
+
worker.unref();
|
|
282
|
+
return { worker, arquivo };
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
218
288
|
/** O artefato primeiro, depois a primeira vaga livre; sem vaga, solta o artefato. Nunca espera segurando nada. */
|
|
219
289
|
function tentarVaga(dir, pedido) {
|
|
220
290
|
const fdArtefato = tentarTravaExclusiva(path.join(dir, `artefato-${chaveDoArtefato(pedido.artefato)}.lock`));
|
|
@@ -224,17 +294,29 @@ function tentarVaga(dir, pedido) {
|
|
|
224
294
|
const fdVaga = tentarTravaExclusiva(path.join(dir, `vaga-${slot}.lock`));
|
|
225
295
|
if (fdVaga === null)
|
|
226
296
|
continue;
|
|
227
|
-
const dono = { slot, operacao: pedido.operacao, thread: pedido.thread, artefato: path.resolve(pedido.artefato),
|
|
228
|
-
pid: process.pid, desde: new Date().toISOString() };
|
|
229
297
|
const id = (0, node_crypto_1.randomUUID)();
|
|
298
|
+
const identidade = identidadeDaMaquina();
|
|
299
|
+
const dono = { slot, operacao: pedido.operacao, thread: pedido.thread, artefato: path.resolve(pedido.artefato),
|
|
300
|
+
pid: process.pid, desde: new Date().toISOString(), inicio: inicioDoProcesso(process.pid), ...identidade };
|
|
230
301
|
gravarDono(fdVaga, dono, id);
|
|
231
302
|
gravarDono(fdArtefato, dono, id);
|
|
232
|
-
|
|
303
|
+
const batida = iniciarBatida(dir, slot, id);
|
|
304
|
+
return { fdArtefato, fdVaga, slot, id, batida: batida?.worker ?? null, arquivoDaBatida: batida?.arquivo ?? null };
|
|
233
305
|
}
|
|
234
306
|
fs.closeSync(fdArtefato);
|
|
235
307
|
return null;
|
|
236
308
|
}
|
|
237
309
|
function soltar(posse) {
|
|
310
|
+
try {
|
|
311
|
+
posse.batida?.postMessage('parar');
|
|
312
|
+
void posse.batida?.terminate();
|
|
313
|
+
}
|
|
314
|
+
catch { /* o processo tambem encerra o worker */ }
|
|
315
|
+
if (posse.arquivoDaBatida)
|
|
316
|
+
try {
|
|
317
|
+
fs.unlinkSync(posse.arquivoDaBatida);
|
|
318
|
+
}
|
|
319
|
+
catch { /* batida expira sozinha */ }
|
|
238
320
|
for (const fd of [posse.fdVaga, posse.fdArtefato]) {
|
|
239
321
|
try {
|
|
240
322
|
fs.ftruncateSync(fd, 0);
|
|
@@ -264,7 +346,8 @@ function ocupantesDaFilaPesada(raiz) {
|
|
|
264
346
|
continue;
|
|
265
347
|
}
|
|
266
348
|
try {
|
|
267
|
-
|
|
349
|
+
const trava = observarTrava(fd);
|
|
350
|
+
if (trava === 'adquirida')
|
|
268
351
|
continue;
|
|
269
352
|
const slot = Number(/^vaga-(\d+)\.lock$/.exec(nome)[1]);
|
|
270
353
|
let dono = {};
|
|
@@ -272,9 +355,31 @@ function ocupantesDaFilaPesada(raiz) {
|
|
|
272
355
|
dono = JSON.parse(fs.readFileSync(path.join(dir, nome), 'utf8'));
|
|
273
356
|
}
|
|
274
357
|
catch { /* dono ainda gravando */ }
|
|
358
|
+
let batidaExpiraEm = null;
|
|
359
|
+
if (typeof dono.posse === 'string') {
|
|
360
|
+
try {
|
|
361
|
+
const batida = JSON.parse(fs.readFileSync(arquivoDaBatida(dir, slot, dono.posse), 'utf8'));
|
|
362
|
+
if (batida.posse === dono.posse && typeof batida.expiraEm === 'string')
|
|
363
|
+
batidaExpiraEm = batida.expiraEm;
|
|
364
|
+
}
|
|
365
|
+
catch { /* batida ausente, vencida ou ainda sendo publicada */ }
|
|
366
|
+
}
|
|
367
|
+
const local = identidadeDaMaquina();
|
|
368
|
+
const mesmoHost = typeof dono.host === 'string' && local.host === dono.host;
|
|
369
|
+
const mesmoEspacoPids = typeof dono.espacoPids === 'string' && local.espacoPids === dono.espacoPids;
|
|
370
|
+
let processo = 'desconhecido';
|
|
371
|
+
if (mesmoHost && mesmoEspacoPids && typeof dono.pid === 'number' && typeof dono.inicio === 'string') {
|
|
372
|
+
const atual = inicioDoProcesso(dono.pid);
|
|
373
|
+
processo = atual === dono.inicio ? 'vivo' : atual === null ? 'ausente' : 'ausente';
|
|
374
|
+
}
|
|
375
|
+
const estado = avaliarVidaDoOcupante({ trava: trava === 'ocupada' ? 'ocupada' : 'desconhecida',
|
|
376
|
+
batidaValida: batidaExpiraEm !== null && Date.parse(batidaExpiraEm) > Date.now(), mesmoHost, mesmoEspacoPids, processo });
|
|
275
377
|
ocupantes.push({ slot, operacao: typeof dono.operacao === 'string' ? dono.operacao : 'desconhecida',
|
|
276
378
|
thread: typeof dono.thread === 'string' ? dono.thread : null, artefato: typeof dono.artefato === 'string' ? dono.artefato : '',
|
|
277
|
-
pid: typeof dono.pid === 'number' ? dono.pid : 0, desde: typeof dono.desde === 'string' ? dono.desde : ''
|
|
379
|
+
pid: typeof dono.pid === 'number' ? dono.pid : 0, desde: typeof dono.desde === 'string' ? dono.desde : '',
|
|
380
|
+
inicio: typeof dono.inicio === 'string' ? dono.inicio : null, host: typeof dono.host === 'string' ? dono.host : null,
|
|
381
|
+
espacoPids: typeof dono.espacoPids === 'string' ? dono.espacoPids : null,
|
|
382
|
+
posse: typeof dono.posse === 'string' ? dono.posse : undefined, batidaExpiraEm, ...estado });
|
|
278
383
|
}
|
|
279
384
|
finally {
|
|
280
385
|
fs.closeSync(fd);
|
|
@@ -283,19 +388,24 @@ function ocupantesDaFilaPesada(raiz) {
|
|
|
283
388
|
return ocupantes;
|
|
284
389
|
}
|
|
285
390
|
function recusaDaFila(pedido, esperouMs, ocupantes) {
|
|
391
|
+
const indeterminado = ocupantes.some(o => o.vida === 'desconhecido');
|
|
392
|
+
const motivo = indeterminado ? 'concurrency.lease-indeterminado' : 'fila-pesada.ocupada';
|
|
286
393
|
const espera = esperouMs < 60000 ? `${Math.round(esperouMs / 1000)} s` : `${Math.round(esperouMs / 60000)} min`;
|
|
287
394
|
const linhas = [
|
|
288
395
|
`Operação pesada não executada: ${pedido.operacao}${pedido.thread ? ` da thread ${pedido.thread}` : ''} esperou ${espera} ` +
|
|
289
396
|
`e a fila de recursos pesados não liberou a vez (até ${pedido.slots} ao mesmo tempo, concurrency.max_parallel_heavy). ` +
|
|
290
397
|
'Verify e build juntos nesta máquina dão regressão falsa.',
|
|
291
398
|
...(ocupantes.length ? ocupantes.map(o => ` Quem ocupa: ${o.operacao}${o.thread ? ` da thread ${o.thread}` : ''}, ` +
|
|
292
|
-
`desde ${(0, horario_1.formatarDesde)(o.desde)}, processo ${o.pid}
|
|
399
|
+
`desde ${(0, horario_1.formatarDesde)(o.desde)}, processo ${o.pid}${o.vida === 'desconhecido' ? '; vida indeterminada' : ''}.`) :
|
|
400
|
+
[' Quem ocupa: a vez é de outro pedido que chegou antes e ainda espera.']),
|
|
401
|
+
...(indeterminado ? ['A vida do lease não pôde ser comprovada deste ambiente; “não sei” nunca é órfão. ' +
|
|
402
|
+
'Espere com --esperar ou devolva à condução com concurrency.lease-indeterminado.'] : []),
|
|
293
403
|
'O que você pode fazer:',
|
|
294
404
|
' 1. Esperar a vez: o dono da vaga segue rodando e, quando ele termina ou morre, a vaga volta sozinha.',
|
|
295
405
|
...(pedido.comandoDeEsperar ? [` ${pedido.comandoDeEsperar}`] : []),
|
|
296
406
|
' 2. Desligar a fila só nesta máquina, se ela aguenta: concurrency.max_parallel_heavy no orkastery.yaml (0 desliga).',
|
|
297
407
|
];
|
|
298
|
-
return { contrato: 'ork.fila-pesada-recusa/v1', motivo
|
|
408
|
+
return { contrato: 'ork.fila-pesada-recusa/v1', motivo, operacao: pedido.operacao, thread: pedido.thread,
|
|
299
409
|
slots: pedido.slots, esperouMs, ocupantes, texto: linhas.join('\n') };
|
|
300
410
|
}
|
|
301
411
|
const pausa = new Int32Array(new SharedArrayBuffer(4));
|
|
@@ -71,6 +71,7 @@ exports.MOTIVOS_DO_ORQUESTRADOR = Object.freeze([
|
|
|
71
71
|
'verify.failed', 'verify.regression', 'verify.timeout', 'verify.sem-veredito',
|
|
72
72
|
'claims.failed', 'claims.unverifiable', 'ci.failed', 'hitl.formato',
|
|
73
73
|
'lease.busy', 'vaga.stale', 'conducao.em-andamento', 'concurrency.limite',
|
|
74
|
+
'concurrency.lease-indeterminado', 'conducao.acao-necessaria',
|
|
74
75
|
]);
|
|
75
76
|
function quemDecide(motivo) {
|
|
76
77
|
return exports.MOTIVOS_DO_ORQUESTRADOR.includes(String(motivo ?? '')) ? 'orquestrador' : 'dono';
|