@orkastery/cli 0.6.6 → 0.6.7

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 (41) hide show
  1. package/adapters/claude-code/agents/ork-check.md +4 -0
  2. package/adapters/claude-code/mods/ork-freio/hooks/lote.ts +1 -1
  3. package/adapters/claude-code/mods/ork-torre/hooks/frases.ts +2 -1
  4. package/adapters/openclaw/openclaw.plugin.json +1 -1
  5. package/adapters/openclaw/package.json +1 -1
  6. package/assets/adapters-en/claude-code/agents/ork-check.md +4 -0
  7. package/assets/skills-en/phases/check-quality/SKILL.md +14 -0
  8. package/dist/adapters/claude-bg.js +1 -1
  9. package/dist/agendador-cpu.js +27 -4
  10. package/dist/ambiente-funcional.js +72 -0
  11. package/dist/cenarios-sensores.js +3 -7
  12. package/dist/check-agil.js +204 -60
  13. package/dist/check-paralelo.js +101 -36
  14. package/dist/claim-lint.js +19 -0
  15. package/dist/claims.js +77 -11
  16. package/dist/fila-pesada.js +8 -6
  17. package/dist/fix.js +152 -6
  18. package/dist/hitl-gates.js +1 -1
  19. package/dist/index.js +53 -4
  20. package/dist/locale-ajuda.js +10 -4
  21. package/dist/locale-claims.js +22 -6
  22. package/dist/locale-operacao-restante.js +12 -0
  23. package/dist/locale-prompts.js +4 -2
  24. package/dist/locale-pulse.js +2 -0
  25. package/dist/locale-retry.js +8 -0
  26. package/dist/locale-verify.js +8 -0
  27. package/dist/mcp-artifacts.js +24 -0
  28. package/dist/mcp-install.js +1 -1
  29. package/dist/mcp-server.js +11 -1
  30. package/dist/mcp-verify-async.js +2 -1
  31. package/dist/noctum-provas.js +56 -30
  32. package/dist/recibo-prova.js +87 -0
  33. package/dist/retry.js +6 -0
  34. package/dist/security-scan/executor.js +9 -3
  35. package/dist/security-scan/guard.js +5 -2
  36. package/dist/ship.js +1 -1
  37. package/dist/temporario-das-lanes.js +94 -0
  38. package/dist/verify.js +662 -410
  39. package/eval/fixtures/fx-sensores-runtime/caso.json +1 -1
  40. package/package.json +1 -1
  41. package/skills/phases/check-quality/SKILL.md +14 -0
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.PECA_SEM_RELATORIO = exports.RelatorioDoCheck = exports.TETO_DO_RELATORIO_BYTES = exports.MAXIMO_DE_PREPAROS_REFEITOS = exports.GuardaDoProduto = void 0;
36
+ exports.assinaturaDoAmbiente = exports.PECA_SEM_RELATORIO = exports.RelatorioDoCheck = exports.TETO_DO_RELATORIO_BYTES = exports.MAXIMO_DE_PREPAROS_REFEITOS = exports.GuardaDoProduto = void 0;
37
37
  exports.paralelismoDaVarreduraNoGrupo = paralelismoDaVarreduraNoGrupo;
38
38
  exports.passosDoComando = passosDoComando;
39
39
  exports.elosDoComando = elosDoComando;
@@ -45,11 +45,13 @@ exports.executorDaClaimNoAgile = executorDaClaimNoAgile;
45
45
  exports.refazerPreparo = refazerPreparo;
46
46
  exports.testDasLanes = testDasLanes;
47
47
  exports.porQueDasLanes = porQueDasLanes;
48
+ exports.comandoDaClaimDependeDasLanes = comandoDaClaimDependeDasLanes;
49
+ exports.planejarClaimsNoAgile = planejarClaimsNoAgile;
48
50
  exports.comandosNoAgile = comandosNoAgile;
49
51
  exports.pecasDoRelatorio = pecasDoRelatorio;
50
52
  exports.listasDasLanes = listasDasLanes;
51
53
  exports.resultadosDasLanes = resultadosDasLanes;
52
- exports.assinaturaDoAmbiente = assinaturaDoAmbiente;
54
+ exports.classificarClaimPelasLanes = classificarClaimPelasLanes;
53
55
  exports.executorDaClaimPelasLanes = executorDaClaimPelasLanes;
54
56
  /**
55
57
  * RM-072 (D3 a D8 da ork-check5x): o motor do Agile Check no verify.
@@ -79,6 +81,7 @@ exports.executorDaClaimPelasLanes = executorDaClaimPelasLanes;
79
81
  * ok e e o nome que a baseline compara). Nada aqui decide veredito: a classificacao e a do verify.
80
82
  */
81
83
  const node_crypto_1 = require("node:crypto");
84
+ const ambiente_funcional_1 = require("./ambiente-funcional");
82
85
  const fs = __importStar(require("node:fs"));
83
86
  const os = __importStar(require("node:os"));
84
87
  const path = __importStar(require("node:path"));
@@ -494,11 +497,78 @@ function porQueDasLanes(c) {
494
497
  };
495
498
  return caidas.map((l) => `lane ${l.lane}: ${porque(l)}`).join('; ');
496
499
  }
500
+ /** A mesma gramatica fechada que o reaproveitamento usa, sem precisar esperar a lane para classificar a dependencia. */
501
+ function comandoDaClaimDependeDasLanes(manifesto, comando, cwd) {
502
+ const passosDoPreparo = manifesto.verify.preparo ? passosDoComando(manifesto.verify.preparo) : [];
503
+ const daFrente = tirarPassosDoPreparo(comando, passosDoPreparo);
504
+ const { resto } = daFrente.passos.length > 0 ? daFrente : tirarPassosDoPreparoAposLeituras(comando, passosDoPreparo);
505
+ if (!resto)
506
+ return false;
507
+ const elos = elosDoComando(resto);
508
+ return !!elos?.length && elos.every((elo) => comandoGrepQ(elo) || !!auditoriaTap(elo, cwd) || !!comandoNodeTest(elo, cwd))
509
+ && elos.some((elo) => !!auditoriaTap(elo, cwd) || !!comandoNodeTest(elo, cwd));
510
+ }
511
+ /**
512
+ * Claims dependentes de lane conservam a ordem antiga. As demais entram no grupo; a ordem interna e claims sobre o
513
+ * mesmo arquivo formam cadeias, enquanto arquivos distintos podem usar vagas diferentes do pool de claims.
514
+ */
515
+ function planejarClaimsNoAgile(manifesto, claims, cwd, guarda, preparo) {
516
+ if (guarda.conferir())
517
+ return { execucoes: [], ids: [], cadeias: 0 };
518
+ const passosDoPreparo = manifesto.verify.preparo ? passosDoComando(manifesto.verify.preparo) : [];
519
+ const ultimosPorArquivo = new Map();
520
+ const execucoes = [];
521
+ const ids = [];
522
+ for (const claim of claims) {
523
+ const usaPreparo = (comando) => {
524
+ const daFrente = tirarPassosDoPreparo(comando, passosDoPreparo);
525
+ return (daFrente.passos.length > 0 ? daFrente : tirarPassosDoPreparoAposLeituras(comando, passosDoPreparo)).passos.length > 0;
526
+ };
527
+ // A guarda e o teto de refacoes sao seriais: claim que consome o preparo continua no caminho anterior.
528
+ if (!claim.verificar.length || claim.verificar.some((comando) => comandoDaClaimDependeDasLanes(manifesto, comando, cwd)
529
+ || usaPreparo(comando)))
530
+ continue;
531
+ ids.push(claim.id);
532
+ let anterior = ultimosPorArquivo.get(claim.arquivo);
533
+ claim.verificar.forEach((comando, i) => {
534
+ const nome = `${claim.id}.${i + 1}`;
535
+ const daFrente = tirarPassosDoPreparo(comando, passosDoPreparo);
536
+ const tirado = daFrente.passos.length > 0 ? daFrente : tirarPassosDoPreparoAposLeituras(comando, passosDoPreparo);
537
+ if (tirado.passos.length > 0 && tirado.resto === '') {
538
+ execucoes.push({ nome, comando, resultado: doPreparo(nome, comando, preparo, tirado.passos) });
539
+ return;
540
+ }
541
+ const executado = tirado.passos.length > 0 ? tirado.resto : comando;
542
+ const item = { nome: `claim:${nome}`, comando: executado, papelCpu: 'claim',
543
+ ...(anterior ? { apos: [anterior] } : {}) };
544
+ execucoes.push({ nome, comando, item,
545
+ ...(executado !== comando ? { comandoExecutado: executado,
546
+ preparo: { reaproveitado: true, passos: tirado.passos } } : {}) });
547
+ anterior = item.nome;
548
+ });
549
+ if (anterior)
550
+ ultimosPorArquivo.set(claim.arquivo, anterior);
551
+ }
552
+ return { execucoes, ids, cadeias: ultimosPorArquivo.size };
553
+ }
554
+ function resultadosDasClaimsAntecipadas(plano, porItem) {
555
+ if (!plano)
556
+ return {};
557
+ return Object.fromEntries(plano.execucoes.map((e) => {
558
+ if (e.resultado)
559
+ return [e.nome, { ...e.resultado, agendamento: 'antecipada' }];
560
+ const bruto = porItem.get(e.item.nome);
561
+ return [e.nome, { ...bruto, nome: e.nome, comando: e.comando,
562
+ agendamento: 'antecipada',
563
+ ...(e.comandoExecutado ? { comandoExecutado: e.comandoExecutado } : {}),
564
+ ...(e.preparo ? { preparo: e.preparo } : {}) }];
565
+ }));
566
+ }
497
567
  /**
498
568
  * Os comandos do manifesto no Agile (D4): `build` reaproveitado do preparo com a guarda valendo; o resto num grupo
499
569
  * paralelo, com as lanes no lugar do `verify.test`, so com a guarda valendo; sem ela, tudo em serie (o Classic).
500
570
  */
501
- function comandosNoAgile(manifesto, cwd, lista, executor, grupo, guarda, preparo, selecao = { comandos: {}, lanes: {} }, passagemParcial, selecaoPorMudanca = {}) {
571
+ function comandosNoAgile(manifesto, cwd, lista, executor, grupo, guarda, preparo, selecao = { comandos: {}, lanes: {} }, passagemParcial, selecaoPorMudanca = {}, claimsAntecipadas) {
502
572
  const pecas = [];
503
573
  const passosDoPreparo = manifesto.verify.preparo ? passosDoComando(manifesto.verify.preparo) : [];
504
574
  const lanesDeclaradas = Object.entries(manifesto.check?.agile?.test ?? {});
@@ -517,7 +587,7 @@ function comandosNoAgile(manifesto, cwd, lista, executor, grupo, guarda, preparo
517
587
  ? passagemParcial?.(c.nome, c.comando, cwd, c.filtro, selecao.comandos[c.nome]) : null;
518
588
  return parcial && !parcial.ok ? parcial : executor(c.nome, c.comando, cwd);
519
589
  };
520
- return { comandos: lista.map(executar), pecas, paralelo: false, lanes: [] };
590
+ return { comandos: lista.map(executar), claimsAntecipadas: {}, pecas, paralelo: false, lanes: [] };
521
591
  }
522
592
  // Com a guarda: o build primeiro (do preparo, quando e so passos dele), e o resto junto.
523
593
  const build = lista.find((c) => c.nome === 'build');
@@ -560,7 +630,7 @@ function comandosNoAgile(manifesto, cwd, lista, executor, grupo, guarda, preparo
560
630
  ...(c.filtro ? { filtro: c.filtro } : {}), ...(selecao.comandos[c.nome] ? { testesFiltrados: selecao.comandos[c.nome] } : {}),
561
631
  papelCpu: c.nome === 'security_scan' ? 'varredura' : 'outro' });
562
632
  }
563
- itens.push(...encadeados);
633
+ itens.push(...(claimsAntecipadas?.execucoes.flatMap((e) => e.item ? [e.item] : []) ?? []), ...encadeados);
564
634
  const parciaisVermelhas = new Map();
565
635
  for (const item of itens) {
566
636
  if (!item.filtro || !item.testesFiltrados?.length || !passagemParcial)
@@ -590,7 +660,7 @@ function comandosNoAgile(manifesto, cwd, lista, executor, grupo, guarda, preparo
590
660
  itens.forEach((item, i) => {
591
661
  if (item.nome.startsWith('test:'))
592
662
  lanes.push({ lane: item.nome.slice('test:'.length), resultado: resultados[i] });
593
- else
663
+ else if (!item.nome.startsWith('claim:'))
594
664
  porNome.set(item.nome, resultados[i]);
595
665
  });
596
666
  // As lanes voltam na ordem do manifesto, mesmo com a encadeada no fim do grupo.
@@ -602,7 +672,9 @@ function comandosNoAgile(manifesto, cwd, lista, executor, grupo, guarda, preparo
602
672
  }
603
673
  if (itens.length > 1)
604
674
  pecas.push({ peca: 'grupo', estado: 'agile' });
605
- return { comandos: lista.map((c) => porNome.get(c.nome)), pecas, paralelo: itens.length > 1, lanes: lanes.map((l) => l.lane) };
675
+ return { comandos: lista.map((c) => porNome.get(c.nome)),
676
+ claimsAntecipadas: resultadosDasClaimsAntecipadas(claimsAntecipadas, porItemInteiro),
677
+ pecas, paralelo: itens.length > 1, lanes: lanes.map((l) => l.lane) };
606
678
  }
607
679
  /** O teto da leitura do relatorio da rodada. */
608
680
  exports.TETO_DO_RELATORIO_BYTES = 4 * 1024 * 1024;
@@ -700,16 +772,8 @@ function resultadosDasLanes(linhas) {
700
772
  estado: x.estado }];
701
773
  }) }));
702
774
  }
703
- /** Hash do ambiente funcional; transporte do CHECK, npm e bookkeeping do shell nao mudam o teste. */
704
- function assinaturaDoAmbiente(env) {
705
- const ignorada = (nome) => nome.startsWith('npm_') || nome.startsWith('NPM_')
706
- || ['ORK_CHECK_RELATORIO', 'ORK_CHECK_CPUS_DA_LANE', 'ORK_CHECK_CPUS_DA_LANE_APOS_VARREDURA',
707
- 'ORK_CHECK_CPUS_DA_LANE_APOS_RESERVA', 'ORK_CHECK_RESERVA_DEVOLVIDA', 'ORK_CHECK_CONTAINERS_DA_LANE', 'ORK_CHECK_CPUS_DOCKER',
708
- 'NODE_TEST_CONTEXT', 'NODE_COMPILE_CACHE', 'PWD', 'OLDPWD', 'SHLVL', '_', 'INIT_CWD'].includes(nome);
709
- const pares = Object.entries(env).filter(([nome, valor]) => valor !== undefined && !ignorada(nome))
710
- .sort(([a], [b]) => a.localeCompare(b));
711
- return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(pares)).digest('hex');
712
- }
775
+ var ambiente_funcional_2 = require("./ambiente-funcional");
776
+ Object.defineProperty(exports, "assinaturaDoAmbiente", { enumerable: true, get: function () { return ambiente_funcional_2.assinaturaDoAmbiente; } });
713
777
  /** Le somente a gramatica aceita para reaproveitar: `node --test [--test-name-pattern P] arquivo...`. */
714
778
  function comandoNodeTest(comando, cwd) {
715
779
  const palavras = palavrasDoComando(comando);
@@ -774,68 +838,148 @@ function resultadoReaproveitado(nome, comando, pedido, usadas, esperado, nomes)
774
838
  nomes: nomes ?? (pedido.padrao === undefined ? pedido.arquivos.map((a) => `${a}:todos`) : pedido.arquivos.map((a) => `${a}:/${pedido.padrao}/`)),
775
839
  head: esperado.head, ambiente: esperado.ambiente } };
776
840
  }
841
+ function recusaPelasLanes(motivo, arquivos, esperado, assinatura, candidata, usarExecutorEquivalente = candidata) {
842
+ return { decisao: { reaproveitada: false, motivo, arquivos: [...new Set(arquivos)], head: esperado.head, ambiente: assinatura },
843
+ candidata, usarExecutorEquivalente, elos: [] };
844
+ }
845
+ /** Casos nominais ficam exatamente um nivel abaixo do evento que representa o arquivo no runner isolado. */
846
+ function casosNominaisDoArquivo(vistos, arquivo) {
847
+ const niveisDoArquivo = vistos.filter((t) => path.basename(t.nome) === arquivo).map((t) => t.nesting);
848
+ if (niveisDoArquivo.length === 0)
849
+ return [];
850
+ const nivelDosCasos = Math.min(...niveisDoArquivo) + 1;
851
+ return vistos.filter((t) => t.nesting === nivelDosCasos);
852
+ }
853
+ /** Reconhece especificamente o TAP com filtro: ele e fechado, mas a lane inteira nao prova a auditoria filtrada. */
854
+ function tapComPadraoDeNome(comando, cwd) {
855
+ const divisor = ' | ';
856
+ const i = comando.indexOf(divisor);
857
+ if (i < 0 || comando.indexOf(divisor, i + divisor.length) >= 0)
858
+ return null;
859
+ const node = comando.slice(0, i).trim();
860
+ if (!/^node --test-reporter=tap\b/.test(node))
861
+ return null;
862
+ const pedido = comandoNodeTest(node.replace(/^node --test-reporter=tap\b/, 'node --test'), cwd);
863
+ return pedido?.padrao !== undefined ? pedido : null;
864
+ }
865
+ function planejarClaimPelasLanes(comando, cwd, evidencias, esperado) {
866
+ const assinatura = (0, ambiente_funcional_1.assinaturaDoAmbiente)(esperado.ambiente);
867
+ const elos = elosDoComando(comando);
868
+ if (!elos)
869
+ return recusaPelasLanes('comando-aberto', [], esperado, assinatura, false, false);
870
+ const planejados = [];
871
+ const arquivos = [];
872
+ for (const elo of elos) {
873
+ if (comandoGrepQ(elo)) {
874
+ planejados.push({ tipo: 'grep', comando: elo });
875
+ continue;
876
+ }
877
+ const tap = auditoriaTap(elo, cwd);
878
+ const comPadrao = tap ? null : tapComPadraoDeNome(elo, cwd);
879
+ if (comPadrao)
880
+ return recusaPelasLanes('tap-com-test-name-pattern', [...arquivos, ...comPadrao.arquivos], esperado, assinatura, true);
881
+ const pedido = tap?.pedido ?? comandoNodeTest(elo, cwd);
882
+ if (!pedido)
883
+ return recusaPelasLanes('comando-nao-elegivel', arquivos, esperado, assinatura, false, false);
884
+ arquivos.push(...pedido.arquivos);
885
+ planejados.push({ tipo: 'lane', comando: elo, pedido, usadas: [],
886
+ ...(tap ? { nomes: pedido.arquivos.map((a) => `${a}:${tap.prefixo}>=${tap.minimo};sem-falha;sem-skip`) } : {}) });
887
+ }
888
+ if (!planejados.some((p) => p.tipo === 'lane')) {
889
+ return recusaPelasLanes('comando-nao-elegivel', arquivos, esperado, assinatura, false, false);
890
+ }
891
+ const vigencia = esperado.podeReaproveitar?.();
892
+ if (vigencia === false)
893
+ return recusaPelasLanes('lanes-invigentes', arquivos, esperado, assinatura, true);
894
+ if (typeof vigencia === 'string')
895
+ return recusaPelasLanes(vigencia, arquivos, esperado, assinatura, true);
896
+ if (evidencias.length === 0)
897
+ return recusaPelasLanes('evidencia-vazia', arquivos, esperado, assinatura, true);
898
+ for (const item of planejados) {
899
+ if (item.tipo !== 'lane')
900
+ continue;
901
+ const tap = auditoriaTap(item.comando, cwd);
902
+ const usadasDoElo = new Set();
903
+ for (const arquivo of item.pedido.arquivos) {
904
+ const comArquivo = evidencias.filter((e) => e.testes.some((t) => t.arquivo === arquivo));
905
+ if (comArquivo.length === 0)
906
+ return recusaPelasLanes('arquivo-nao-executado', arquivos, esperado, assinatura, true);
907
+ const noHead = comArquivo.filter((e) => e.head === esperado.head);
908
+ if (noHead.length === 0)
909
+ return recusaPelasLanes('head-divergente', arquivos, esperado, assinatura, true);
910
+ const noAmbiente = noHead.filter((e) => e.ambiente === assinatura);
911
+ if (noAmbiente.length === 0) {
912
+ return recusaPelasLanes('ambiente-funcional-divergente', arquivos, esperado, assinatura, true);
913
+ }
914
+ const vistos = noAmbiente.flatMap((e) => e.testes.filter((t) => t.arquivo === arquivo));
915
+ if (vistos.some((t) => t.estado === 'fail'))
916
+ return recusaPelasLanes('falha-na-lane', arquivos, esperado, assinatura, true);
917
+ if (vistos.some((t) => t.estado === 'skip' || t.estado === 'todo')) {
918
+ return recusaPelasLanes('skip-ou-todo-na-lane', arquivos, esperado, assinatura, true);
919
+ }
920
+ if (tap) {
921
+ const doArquivo = casosNominaisDoArquivo(vistos, arquivo).filter((t) => t.estado === 'pass');
922
+ const doPrefixo = doArquivo.filter((t) => new RegExp(`^${tap.prefixo}[0-9]+:`).test(t.nome));
923
+ if (doPrefixo.length === 0)
924
+ return recusaPelasLanes('prefixo-divergente', arquivos, esperado, assinatura, true);
925
+ if (doPrefixo.length < tap.minimo) {
926
+ return recusaPelasLanes('contagem-abaixo-do-minimo', arquivos, esperado, assinatura, true);
927
+ }
928
+ }
929
+ noAmbiente.forEach((e) => usadasDoElo.add(e));
930
+ }
931
+ item.usadas = [...usadasDoElo];
932
+ }
933
+ const usadas = planejados.flatMap((p) => p.tipo === 'lane' ? p.usadas : []);
934
+ const lanes = [...new Set(usadas.map((e) => e.lane))];
935
+ const nomes = planejados.flatMap((p) => p.tipo !== 'lane' ? [] : p.nomes ?? (p.pedido.padrao === undefined
936
+ ? p.pedido.arquivos.map((a) => `${a}:todos`) : p.pedido.arquivos.map((a) => `${a}:/${p.pedido.padrao}/`)));
937
+ return { decisao: { reaproveitada: true, lanes, arquivos: [...new Set(arquivos)], nomes: [...new Set(nomes)],
938
+ head: esperado.head, ambiente: assinatura }, candidata: true, usarExecutorEquivalente: false, elos: planejados };
939
+ }
940
+ /** A classificacao pura usada pelo executor e, na M12, pelo planejador dos lotes de claims. */
941
+ function classificarClaimPelasLanes(comando, cwd, evidencias, esperado) {
942
+ const p = planejarClaimPelasLanes(comando, cwd, evidencias, esperado);
943
+ return { decisao: p.decisao, candidata: p.candidata, usarExecutorEquivalente: p.usarExecutorEquivalente };
944
+ }
777
945
  /**
778
946
  * Reaproveita uma claim somente quando a lane terminou todos os arquivos pedidos no mesmo HEAD e ambiente. Um arquivo
779
947
  * inteiro verde e evidencia mais forte que `--test-name-pattern`: o padrao fica registrado em `nomes`.
780
948
  */
781
949
  function executorDaClaimPelasLanes(base, evidencias, esperado) {
782
- const assinatura = assinaturaDoAmbiente(esperado.ambiente);
783
950
  return (nome, comando, cwd) => {
784
951
  const executar = esperado.executorEquivalente ?? base;
785
- if (esperado.podeReaproveitar?.() === false)
786
- return executar(nome, comando, cwd);
787
- const elos = elosDoComando(comando);
788
- if (!elos)
789
- return base(nome, comando, cwd);
790
- const planejados = [];
791
- for (const elo of elos) {
792
- if (comandoGrepQ(elo)) {
793
- planejados.push({ tipo: 'grep', comando: elo });
794
- continue;
795
- }
796
- const tap = auditoriaTap(elo, cwd);
797
- const pedido = tap?.pedido ?? comandoNodeTest(elo, cwd);
798
- if (!pedido)
799
- return base(nome, comando, cwd);
800
- const usadas = evidencias.filter((e) => e.head === esperado.head && e.ambiente === assinatura
801
- && e.testes.some((t) => pedido.arquivos.includes(t.arquivo)));
802
- const cobertos = pedido.arquivos.every((arquivo) => {
803
- const vistos = usadas.flatMap((e) => e.testes.filter((t) => t.arquivo === arquivo));
804
- if (vistos.length === 0 || vistos.some((t) => t.estado === 'fail'))
805
- return false;
806
- if (vistos.some((t) => t.estado === 'skip' || t.estado === 'todo'))
807
- return false;
808
- if (!tap)
809
- return true;
810
- const doArquivo = vistos.filter((t) => t.nesting === 0);
811
- return doArquivo.filter((t) => t.estado === 'pass' && new RegExp(`^${tap.prefixo}[0-9]+:`).test(t.nome)).length >= tap.minimo;
812
- });
813
- if (!cobertos)
814
- return executar(nome, comando, cwd);
815
- const nomes = tap ? pedido.arquivos.map((a) => `${a}:${tap.prefixo}>=${tap.minimo};sem-falha;sem-skip`) : undefined;
816
- planejados.push({ tipo: 'lane', resultado: resultadoReaproveitado(nome, elo, pedido, usadas, { head: esperado.head, ambiente: assinatura }, nomes) });
952
+ const plano = planejarClaimPelasLanes(comando, cwd, evidencias, esperado);
953
+ if (!plano.decisao.reaproveitada) {
954
+ const r = (plano.usarExecutorEquivalente ? executar : base)(nome, comando, cwd);
955
+ // Comando fora da gramatica das lanes nao foi recusado por elas: ele segue sem inventar uma decisao nominal.
956
+ return plano.candidata ? { ...r, lane: plano.decisao } : r;
817
957
  }
818
958
  let duracaoMs = 0;
819
959
  const resumos = [];
820
960
  let ultimo = null;
821
961
  const lanes = new Set(), arquivos = new Set(), nomes = new Set();
822
- for (const item of planejados) {
823
- const r = item.tipo === 'grep' ? base(nome, item.comando, cwd) : item.resultado;
962
+ for (const item of plano.elos) {
963
+ const r = item.tipo === 'grep' ? base(nome, item.comando, cwd)
964
+ : resultadoReaproveitado(nome, item.comando, item.pedido, item.usadas, { head: esperado.head, ambiente: plano.decisao.ambiente }, item.nomes);
824
965
  duracaoMs += r.duracaoMs ?? 0;
825
966
  resumos.push(r.resumo);
826
967
  ultimo = r;
827
968
  if (!r.ok)
828
- return { ...r, comando, comandoExecutado: item.tipo === 'grep' ? item.comando : r.comando, duracaoMs };
829
- for (const lane of r.lane?.lanes ?? [])
830
- lanes.add(lane);
831
- for (const arquivo of r.lane?.arquivos ?? [])
832
- arquivos.add(arquivo);
833
- for (const n of r.lane?.nomes ?? [])
834
- nomes.add(n);
969
+ return { ...r, comando, comandoExecutado: item.comando, duracaoMs, lane: plano.decisao };
970
+ if (r.lane?.reaproveitada) {
971
+ for (const lane of r.lane.lanes)
972
+ lanes.add(lane);
973
+ for (const arquivo of r.lane.arquivos)
974
+ arquivos.add(arquivo);
975
+ for (const n of r.lane.nomes)
976
+ nomes.add(n);
977
+ }
835
978
  }
836
979
  if (!ultimo || lanes.size === 0)
837
980
  return base(nome, comando, cwd);
838
981
  return { ...ultimo, nome, comando, duracaoMs, resumo: resumos.filter(Boolean).join('\n'),
839
- lane: { reaproveitada: true, lanes: [...lanes], arquivos: [...arquivos], nomes: [...nomes], head: esperado.head, ambiente: assinatura } };
982
+ lane: { reaproveitada: true, lanes: [...lanes], arquivos: [...arquivos], nomes: [...nomes], head: esperado.head,
983
+ ambiente: plano.decisao.ambiente } };
840
984
  };
841
985
  }
@@ -34,10 +34,14 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.argvDoItem = argvDoItem;
37
+ exports.limiteDeVagas = limiteDeVagas;
37
38
  exports.workersDaLane = workersDaLane;
38
39
  exports.chaveDoPerfilIntegracao = chaveDoPerfilIntegracao;
39
40
  exports.picoDaArvoreParaPerfil = picoDaArvoreParaPerfil;
40
41
  exports.picoCpuPorFonte = picoCpuPorFonte;
42
+ exports.devolucoesDaReserva = devolucoesDaReserva;
43
+ exports.aplicarDevolucaoDaReserva = aplicarDevolucaoDaReserva;
44
+ exports.aplicarPlanoCpu = aplicarPlanoCpu;
41
45
  exports.esperaDoGrupo = esperaDoGrupo;
42
46
  exports.rodarEmParalelo = rodarEmParalelo;
43
47
  /**
@@ -58,19 +62,21 @@ const os = __importStar(require("node:os"));
58
62
  const path = __importStar(require("node:path"));
59
63
  const agendador_cpu_1 = require("./agendador-cpu");
60
64
  const integracoes_locais_1 = require("./integracoes-locais");
65
+ const ambiente_funcional_1 = require("./ambiente-funcional");
61
66
  const TIMEOUT_BIN = '/usr/bin/timeout';
62
67
  const NICE_BIN = '/usr/bin/nice';
63
68
  const TIME_BIN = '/usr/bin/time';
69
+ const BASH_BIN = '/usr/bin/bash';
64
70
  const CONTRATO_PERFIL_INTEGRACAO = 'ork.check.cpu-integracao/v2';
65
71
  const INTERVALO_CPU_MS = 100;
66
72
  /** O argv de um item: `nice` (se pedido e existir), `timeout(1)` (se existir) e o `argv` do item ou `bash -c`. */
67
73
  function argvDoItem(item, comGrupo = fs.existsSync(TIMEOUT_BIN), comNice = fs.existsSync(NICE_BIN)) {
68
- const programa = item.argv?.length ? item.argv : ['bash', '-c', item.comando];
74
+ const programa = item.argv?.length ? item.argv : [BASH_BIN, '--noprofile', '--norc', '-c', item.comando];
69
75
  const base = comGrupo ? [TIMEOUT_BIN, '--kill-after=15', `${Math.ceil(item.prazoMs / 1000)}`, ...programa] : programa;
70
76
  return item.nice && comNice ? [NICE_BIN, '-n', String(item.nice), ...base] : base;
71
77
  }
72
78
  /** As vagas do grupo: no maximo `max` comandos ao mesmo tempo, na ordem em que pediram. */
73
- function vagas(max) {
79
+ function limiteDeVagas(max) {
74
80
  let livres = max;
75
81
  const fila = [];
76
82
  return {
@@ -125,7 +131,7 @@ function workersDaLane(item, plano, cpus, totalDeLanes) {
125
131
  if (item.papelCpu === 'hermetica')
126
132
  return (0, agendador_cpu_1.workersDaReserva)(plano.cpusHermetica);
127
133
  if (item.papelCpu === 'integracao')
128
- return (0, agendador_cpu_1.workersDaReserva)(plano.cpusIntegracao);
134
+ return (0, agendador_cpu_1.workersDaReserva)(plano.cpusIntegracaoEmUso);
129
135
  // Outros fecham a cadeia depois que as reservas da varredura e da integracao voltaram.
130
136
  return capacidade;
131
137
  }
@@ -255,6 +261,25 @@ function nomesDosContainers(arquivo) {
255
261
  return [];
256
262
  }
257
263
  }
264
+ function evidenciaDaCotaDocker(arquivo) {
265
+ try {
266
+ const evidencias = fs.readFileSync(arquivo, 'utf8').split('\n').filter(Boolean).flatMap((linha) => {
267
+ try {
268
+ const r = JSON.parse(linha);
269
+ return r.tipo === 'cota-docker' && ['aplicada', 'fallback'].includes(String(r.estado))
270
+ && Number.isFinite(r.cota) && Number(r.cota) > 0 && typeof r.motivo === 'string'
271
+ ? [{ estado: r.estado, cota: Number(r.cota), motivo: r.motivo }] : [];
272
+ }
273
+ catch {
274
+ return [];
275
+ }
276
+ });
277
+ return evidencias.find((x) => x.estado === 'fallback') ?? evidencias.at(-1);
278
+ }
279
+ catch {
280
+ return undefined;
281
+ }
282
+ }
258
283
  function arquivoCpuDoContainer(nome) {
259
284
  const r = (0, node_child_process_1.spawnSync)('docker', ['inspect', '--format', '{{.Id}} {{.State.Pid}}', nome], { encoding: 'utf8', timeout: 1000 });
260
285
  const [id = '', pidTexto = ''] = r.stdout?.trim().split(/\s+/) ?? [];
@@ -394,11 +419,15 @@ async function rodarEspecificacao(esp) {
394
419
  Object.assign(medicao, { cpusIntegracao: perfil.picoCpusDaArvore, fonteCpusIntegracao: 'medida' });
395
420
  else if (medicao.cpusIntegracao === undefined)
396
421
  Object.assign(medicao, { cpusIntegracao: null, fonteCpusIntegracao: 'conservadora' });
397
- const plano = usaAgendador ? (0, agendador_cpu_1.planejarCpuDoGrupo)(cpus, medicao) : undefined;
422
+ const nomesDasClaims = new Set(esp.itens.filter((i) => i.papelCpu === 'claim').map((i) => i.nome));
423
+ const cadeiasDeClaims = esp.itens.filter((i) => i.papelCpu === 'claim'
424
+ && !(i.apos ?? []).some((nome) => nomesDasClaims.has(nome))).length;
425
+ const plano = usaAgendador ? (0, agendador_cpu_1.planejarCpuDoGrupo)(cpus, medicao, { cadeiasDeClaims }) : undefined;
398
426
  const sinalDaReserva = plano && !plano.saturado ? path.join(esp.dir, 'reserva-devolvida.json') : undefined;
399
- const itens = plano ? aplicarPlano(esp.itens, plano, sinalDaReserva) : esp.itens;
427
+ const itens = plano ? aplicarPlanoCpu(esp.itens, plano, sinalDaReserva) : esp.itens;
400
428
  const tetoInformado = esp.maxParalelo && esp.maxParalelo > 0 ? esp.maxParalelo : itens.length;
401
- const limite = vagas(Math.max(1, Math.min(tetoInformado, (0, agendador_cpu_1.workersDaReserva)(cpus))));
429
+ const limite = limiteDeVagas(Math.max(1, Math.min(tetoInformado, (0, agendador_cpu_1.workersDaReserva)(cpus))));
430
+ const limiteDasClaims = plano?.cpusClaims ? limiteDeVagas(plano.cpusClaims) : null;
402
431
  const exclusivos = recursosExclusivos();
403
432
  const totalDeLanes = itens.filter((item) => item.nome.startsWith('test:')).length;
404
433
  const porNome = new Map();
@@ -416,12 +445,16 @@ async function rodarEspecificacao(esp) {
416
445
  await vezDoItem(itens[j], j);
417
446
  }
418
447
  const soltarRecursos = await exclusivos.tomar(item.recursos ?? []);
448
+ if (item.papelCpu === 'claim')
449
+ await limiteDasClaims?.tomar();
419
450
  await limite.tomar();
420
451
  try {
421
452
  return await lancar(item, i);
422
453
  }
423
454
  finally {
424
455
  limite.soltar();
456
+ if (item.papelCpu === 'claim')
457
+ limiteDasClaims?.soltar();
425
458
  soltarRecursos();
426
459
  }
427
460
  });
@@ -429,35 +462,33 @@ async function rodarEspecificacao(esp) {
429
462
  return vez;
430
463
  };
431
464
  const todos = Promise.all(itens.map(vezDoItem));
432
- const reservados = itens.filter((item) => item.papelCpu === 'varredura' || item.papelCpu === 'integracao');
465
+ const reservados = itens.filter((item) => ['varredura', 'integracao', 'claim'].includes(item.papelCpu ?? ''));
433
466
  const devolverReserva = async () => {
434
- if (!sinalDaReserva || !reservados.some((item) => item.papelCpu === 'varredura')
435
- || !reservados.some((item) => item.papelCpu === 'integracao'))
467
+ if (!sinalDaReserva || !reservados.length)
436
468
  return;
437
469
  const varreduras = reservados.filter((item) => item.papelCpu === 'varredura');
438
470
  const integracoes = reservados.filter((item) => item.papelCpu === 'integracao');
439
- const degrau = async (origem, pecas, vagas, antes) => {
471
+ const claims = reservados.filter((item) => item.papelCpu === 'claim');
472
+ let vagas = plano.cpusHermetica;
473
+ const devolucoes = devolucoesDaReserva(plano);
474
+ const degrau = async (origem, pecas) => {
475
+ if (!pecas.length)
476
+ return;
440
477
  await Promise.all(pecas.map((item) => vezDoItem(item, indice.get(item.nome))));
441
- fs.appendFileSync(sinalDaReserva, JSON.stringify({ contrato: 'ork.check.reserva-devolvida/v2', origem,
442
- em: new Date().toISOString(), vagasGanhas: vagas - antes, vagas }) + '\n', { mode: 0o600 });
478
+ const registro = aplicarDevolucaoDaReserva(vagas, plano.capacidadeEfetiva, origem, devolucoes[origem], new Date().toISOString());
479
+ vagas = registro.vagas;
480
+ fs.appendFileSync(sinalDaReserva, JSON.stringify(registro) + '\n', { mode: 0o600 });
443
481
  };
444
- if (!plano.integracaoEmParalelo) {
445
- await degrau('varredura', varreduras, plano.cpusHermeticaAposVarredura, plano.cpusHermetica);
446
- await degrau('integracao', integracoes, plano.cpusHermeticaAposReserva, plano.cpusHermeticaAposVarredura);
447
- }
448
- else {
449
- // As promessas escrevem em série no event loop; o primeiro término recebe somente a reserva da própria peça.
450
- let vagas = plano.cpusHermetica;
451
- const aoTerminar = async (origem, pecas, libera) => {
452
- await Promise.all(pecas.map((item) => vezDoItem(item, indice.get(item.nome))));
453
- const antes = vagas;
454
- vagas = Math.min(plano.cpus, vagas + libera);
455
- fs.appendFileSync(sinalDaReserva, JSON.stringify({ contrato: 'ork.check.reserva-devolvida/v2', origem,
456
- em: new Date().toISOString(), vagasGanhas: vagas - antes, vagas }) + '\n', { mode: 0o600 });
457
- };
458
- await Promise.all([aoTerminar('varredura', varreduras, plano.cpusDaFatia),
459
- aoTerminar('integracao', integracoes, plano.cpusIntegracao)]);
460
- }
482
+ const devolverInfraestrutura = plano.integracaoEmParalelo
483
+ ? Promise.all([degrau('varredura', varreduras), degrau('integracao', integracoes)])
484
+ : (async () => {
485
+ await degrau('varredura', varreduras);
486
+ await degrau('integracao', integracoes);
487
+ })();
488
+ await Promise.all([
489
+ degrau('claims', claims),
490
+ devolverInfraestrutura,
491
+ ]);
461
492
  };
462
493
  const [, terminados] = await Promise.all([devolverReserva(), todos]);
463
494
  const medidaDaIntegracao = terminados.find((t) => t.medicaoCpu)?.medicaoCpu;
@@ -465,6 +496,10 @@ async function rodarEspecificacao(esp) {
465
496
  plano.medicaoCpuIntegracao = medidaDaIntegracao;
466
497
  gravarPerfilIntegracao(chaveDoPerfil, cpus, medidaDaIntegracao);
467
498
  }
499
+ if (plano) {
500
+ const cotas = terminados.flatMap((t) => t.cotaDocker ? [t.cotaDocker] : []);
501
+ plano.cotaDosContainersDaIntegracao = cotas.find((x) => x.estado === 'fallback') ?? cotas.at(-1);
502
+ }
468
503
  fs.writeFileSync(path.join(esp.dir, 'resultado.json'), JSON.stringify({ terminados, ...(plano ? { agendamentoCpu: plano } : {}) }), { mode: 0o600 });
469
504
  function lancar(item, i) {
470
505
  return new Promise((resolve) => {
@@ -497,14 +532,18 @@ async function rodarEspecificacao(esp) {
497
532
  catch { /* medida incompleta abaixo */ }
498
533
  }
499
534
  const medicaoCpu = medirIntegracao && sonda ? sonda.fechar(arvoreMs, t.duracaoMs) : undefined;
500
- fim = { ...t, ...(medicaoCpu ? { medicaoCpu } : {}) };
535
+ const cotaDocker = medirIntegracao ? evidenciaDaCotaDocker(registro) : undefined;
536
+ fim = { ...t, ...(medicaoCpu ? { medicaoCpu } : {}), ...(cotaDocker ? { cotaDocker } : {}) };
501
537
  fs.closeSync(saida);
502
538
  fs.closeSync(erro);
503
539
  resolve(fim);
504
540
  };
505
541
  // Sem o `timeout(1)`, o proprio auxiliar mata pelo prazo, como o `spawnSync` do `exec` faria.
506
542
  const workers = workersDaLane(item, plano, cpus, totalDeLanes);
507
- const ambiente = { ...process.env, ...item.ambiente, ...(workers === null ? {} : { [agendador_cpu_1.ENV_WORKERS]: String(workers) }),
543
+ const baseDoFilho = item.ambienteFuncional
544
+ ? (0, ambiente_funcional_1.ambienteFuncionalDosTestes)(process.env, item.ambiente ?? {})
545
+ : { ...process.env, ...item.ambiente };
546
+ const ambiente = { ...baseDoFilho, ...(workers === null ? {} : { [agendador_cpu_1.ENV_WORKERS]: String(workers) }),
508
547
  ...(medirIntegracao ? { [agendador_cpu_1.ENV_CONTAINERS_DA_LANE]: registro } : {}) };
509
548
  const filho = (0, node_child_process_1.spawn)(bin, args, { cwd: item.cwd, env: ambiente, stdio: ['ignore', saida, erro],
510
549
  ...(comGrupo ? {} : { timeout: item.prazoMs, killSignal: 'SIGTERM' }) });
@@ -521,13 +560,26 @@ async function rodarEspecificacao(esp) {
521
560
  });
522
561
  }
523
562
  }
524
- /** Aplica a mesma conta aos processos, as lanes e aos conteineres que eles criarem. */
525
- function aplicarPlano(originais, plano, sinalDaReserva) {
563
+ /** Reservas que cada peça devolve; a integração encadeada usa a fatia da varredura desde que ela termina. */
564
+ function devolucoesDaReserva(plano) {
565
+ return {
566
+ varredura: plano.integracaoEmParalelo ? plano.cpusDaFatia : 0,
567
+ integracao: plano.cpusIntegracaoEmUso,
568
+ claims: plano.cpusClaims,
569
+ };
570
+ }
571
+ /** Transição pura do sinal; relógio e estado entram por argumento para a prova não depender da parede da máquina. */
572
+ function aplicarDevolucaoDaReserva(vagasAtuais, capacidade, origem, libera, em) {
573
+ const vagas = Math.min(capacidade, vagasAtuais + Math.max(0, libera));
574
+ return { contrato: 'ork.check.reserva-devolvida/v2', origem, em, vagasGanhas: vagas - vagasAtuais, vagas };
575
+ }
576
+ function aplicarPlanoCpu(originais, plano, sinalDaReserva) {
526
577
  const itens = originais.map((item) => ({ ...item, ambiente: { ...item.ambiente } }));
527
578
  const porPapel = (papel) => itens.filter((i) => i.papelCpu === papel);
528
579
  const hermetica = porPapel('hermetica')[0];
529
580
  const varredura = porPapel('varredura')[0];
530
581
  const integracoes = porPapel('integracao');
582
+ const claims = porPapel('claim');
531
583
  const outros = porPapel('outro');
532
584
  const depender = (item, nomes) => {
533
585
  if (!item)
@@ -540,7 +592,7 @@ function aplicarPlano(originais, plano, sinalDaReserva) {
540
592
  };
541
593
  const cpusContainer = String(plano.cpusPorContainer);
542
594
  ambiente(hermetica, { [agendador_cpu_1.ENV_CPUS_DA_LANE]: String(plano.cpusHermetica), [agendador_cpu_1.ENV_CPUS_DOCKER]: cpusContainer,
543
- ...(sinalDaReserva && varredura && integracoes.length > 0 ? {
595
+ ...(sinalDaReserva && (varredura || integracoes.length > 0 || claims.length > 0) ? {
544
596
  [agendador_cpu_1.ENV_CPUS_DA_LANE_APOS_VARREDURA]: String(plano.cpusHermeticaAposVarredura),
545
597
  [agendador_cpu_1.ENV_CPUS_DA_LANE_APOS_RESERVA]: String(plano.cpusHermeticaAposReserva), [agendador_cpu_1.ENV_SINAL_DA_RESERVA]: sinalDaReserva,
546
598
  } : {}) });
@@ -549,11 +601,21 @@ function aplicarPlano(originais, plano, sinalDaReserva) {
549
601
  [agendador_cpu_1.ENV_CPUS_DOCKER]: cpusContainer,
550
602
  });
551
603
  let anteriorIntegracao;
604
+ const workersDaIntegracao = (0, agendador_cpu_1.workersDaReserva)(plano.cpusIntegracaoEmUso);
605
+ const cotaDoContainerDaIntegracao = String(Math.floor((plano.cpusIntegracaoEmUso / workersDaIntegracao) * 100) / 100);
552
606
  for (const item of integracoes) {
553
- ambiente(item, { [agendador_cpu_1.ENV_CPUS_DA_LANE]: String(plano.cpusIntegracao), [agendador_cpu_1.ENV_CPUS_DOCKER]: cpusContainer });
607
+ ambiente(item, { [agendador_cpu_1.ENV_CPUS_DA_LANE]: String(plano.cpusIntegracaoEmUso),
608
+ [agendador_cpu_1.ENV_CPUS_DOCKER]: cotaDoContainerDaIntegracao });
554
609
  depender(item, [anteriorIntegracao ?? (plano.integracaoEmParalelo ? undefined : varredura?.nome)]);
555
610
  anteriorIntegracao = item.nome;
556
611
  }
612
+ if (plano.cpusClaims === 0) {
613
+ let anteriorClaim = anteriorIntegracao;
614
+ for (const item of claims) {
615
+ depender(item, [anteriorClaim ?? varredura?.nome, hermetica?.nome]);
616
+ anteriorClaim = item.nome;
617
+ }
618
+ }
557
619
  if (plano.saturado)
558
620
  depender(varredura, [hermetica?.nome]);
559
621
  // Comandos sem papel conhecido nao disputam CPU com as tres pecas: fecham a cadeia, um por vez.
@@ -585,7 +647,10 @@ function rodarEmParalelo(itens, ambiente = process.env, opcoes = {}) {
585
647
  ...(opcoes.cpus ? { cpus: opcoes.cpus } : {}), ...(opcoes.medicaoCpu ? { medicaoCpu: opcoes.medicaoCpu } : {}) };
586
648
  fs.writeFileSync(especificacao, JSON.stringify(esp), { mode: 0o600 });
587
649
  const prazo = esperaDoGrupo(itens, opcoes.maxParalelo) + 60_000;
588
- const r = (0, node_child_process_1.spawnSync)(process.execPath, [__filename, especificacao], { env: ambiente, encoding: 'utf8', timeout: prazo,
650
+ const ambienteDoAuxiliar = { ...ambiente };
651
+ // Contexto privado do harness: herdá-lo faz este Node comum falar o protocolo binário de um filho de `node --test`.
652
+ delete ambienteDoAuxiliar.NODE_TEST_CONTEXT;
653
+ const r = (0, node_child_process_1.spawnSync)(process.execPath, [__filename, especificacao], { env: ambienteDoAuxiliar, encoding: 'utf8', timeout: prazo,
589
654
  killSignal: 'SIGKILL' });
590
655
  let lido = { terminados: [] };
591
656
  try {