@orkastery/cli 0.6.2 → 0.6.4
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/README.md +1 -1
- package/adapters/hermes/README.md +1 -1
- package/adapters/openclaw/openclaw.plugin.json +1 -1
- package/adapters/openclaw/package.json +1 -1
- package/assets/adapters-en/claude-code/README.md +1 -1
- package/assets/adapters-en/hermes/README.md +1 -1
- package/assets/docs/padroes/roadmap-de-produto.md +1 -1
- package/assets/docs-en/padroes/roadmap-de-produto.md +1 -1
- package/assets/references-en/testing-patterns.md +1 -1
- package/dist/agendador-cpu.js +49 -5
- package/dist/catalogo.js +3 -3
- package/dist/{canarios-hitl.js → cenarios-hitl.js} +7 -7
- package/dist/{canarios-i43.js → cenarios-i43.js} +16 -16
- package/dist/{canarios-pulse.js → cenarios-pulse.js} +3 -3
- package/dist/{canarios-rm057.js → cenarios-rm057.js} +3 -3
- package/dist/{canarios-sensores.js → cenarios-sensores.js} +9 -8
- package/dist/{canarios.js → cenarios.js} +22 -22
- package/dist/check-agil.js +2 -1
- package/dist/check-paralelo.js +337 -14
- package/dist/claims.js +1 -1
- package/dist/docs.js +1 -1
- package/dist/evalrunner.js +37 -37
- package/dist/hitl-canais.js +1 -1
- package/dist/hitl-presentation.js +1 -1
- package/dist/hitl-tempo-parado.js +1 -1
- package/dist/index.js +4 -4
- package/dist/init.js +1 -1
- package/dist/locale-ajuda.js +6 -6
- package/dist/locale-operacao-restante.js +18 -18
- package/dist/sandbox.js +11 -11
- package/dist/security-scan/agil.js +28 -12
- package/dist/security-scan/executor.js +1 -0
- package/dist/security-scan/incremental.js +289 -0
- package/dist/security-scan/trabalhador.js +53 -5
- package/dist/verify.js +12 -1
- package/eval/README.md +8 -8
- package/eval/fixtures/fx-codex-dry/caso.json +1 -1
- package/eval/fixtures/fx-happy/caso.json +1 -1
- package/eval/fixtures/fx-indice-reversao/caso.json +1 -1
- package/eval/fixtures/fx-listagem-abertas/caso.json +1 -1
- package/eval/fixtures/fx-omnicanal/caso.json +1 -1
- package/eval/fixtures/fx-pedido-colado/caso.json +1 -1
- package/eval/fixtures/fx-schema-drift/caso.json +1 -1
- package/package.json +1 -1
- package/references/testing-patterns.md +1 -1
|
@@ -29,7 +29,7 @@ exports.linhaDoHitlAcimaDaMeta = linhaDoHitlAcimaDaMeta;
|
|
|
29
29
|
* aberto nada disso ainda: a espera corre ate o fim do periodo.
|
|
30
30
|
*
|
|
31
31
|
* Limite dito: o "confirmo" que um condutor pede na conversa, fora do ork, nao chega ao ledger e
|
|
32
|
-
* nao entra aqui. Por isso o
|
|
32
|
+
* nao entra aqui. Por isso o cenario `fx-pedido-colado` e a regra nos adaptadores existem.
|
|
33
33
|
*
|
|
34
34
|
* Funcoes puras sobre eventos: nao leem disco nem relogio. Ausencia de pedido e zero pedidos, e a
|
|
35
35
|
* mediana sem resposta e `null`, nunca zero inventado. A unica leitura de disco e
|
package/dist/index.js
CHANGED
|
@@ -3622,9 +3622,9 @@ function comandoAdapter(args) {
|
|
|
3622
3622
|
return 2;
|
|
3623
3623
|
}
|
|
3624
3624
|
/**
|
|
3625
|
-
* `ork eval`:
|
|
3625
|
+
* `ork eval`: cenarios de comportamento e o corpus das skills finas (bloco B4).
|
|
3626
3626
|
*
|
|
3627
|
-
* Skill sem eval nao entra, e comportamento sem
|
|
3627
|
+
* Skill sem eval nao entra, e comportamento sem cenario tambem nao. O comando sai diferente
|
|
3628
3628
|
* de zero em qualquer falha, para o CI do kit poder barrar merge sem eval.
|
|
3629
3629
|
*/
|
|
3630
3630
|
function comandoEval(args) {
|
|
@@ -3637,8 +3637,8 @@ function comandoEval(args) {
|
|
|
3637
3637
|
const r = (0, evalrunner_1.rodarEval)({
|
|
3638
3638
|
catalogo,
|
|
3639
3639
|
skills: lista(args.opcoes.skill),
|
|
3640
|
-
|
|
3641
|
-
|
|
3640
|
+
cenarios: lista(args.opcoes.cenario) ?? lista(args.opcoes.fixture),
|
|
3641
|
+
soCenarios: args.opcoes['so-cenarios'] === true,
|
|
3642
3642
|
soSkills: args.opcoes['so-skills'] === true,
|
|
3643
3643
|
});
|
|
3644
3644
|
if (args.opcoes.json === true) {
|
package/dist/init.js
CHANGED
|
@@ -267,7 +267,7 @@ policies:
|
|
|
267
267
|
* exemplo) gravava manifesto, `AGENTS.md` e `.orkastery/` ali e saia 0; um repositorio criado depois
|
|
268
268
|
* dentro dessa pasta herdava o manifesto de fora, com outro nome e outra abbrev, e o doctor dizia PRONTO.
|
|
269
269
|
* A CLI recusa antes de gravar qualquer coisa. A funcao `init` segue aceitando qualquer pasta (demo e
|
|
270
|
-
*
|
|
270
|
+
* cenarios montam o projeto por ela).
|
|
271
271
|
*/
|
|
272
272
|
function exigirRepositorioParaInit(dir) {
|
|
273
273
|
const r = (0, util_1.exec)('git', ['rev-parse', '--is-inside-work-tree'], dir);
|
package/dist/locale-ajuda.js
CHANGED
|
@@ -364,8 +364,8 @@ Uso: ork [--projeto <nome|caminho>] <comando> [argumentos]
|
|
|
364
364
|
[--aceitar-catalogo <arquivo>] sobrescreve a copia com o catalogo, arquivo a arquivo
|
|
365
365
|
[--manter-copia <arquivo>] deixa a copia como esta, arquivo a arquivo
|
|
366
366
|
|
|
367
|
-
eval [--skill S] [--
|
|
368
|
-
[--so-
|
|
367
|
+
eval [--skill S] [--cenario C] Cenarios de comportamento + corpus das skills finas
|
|
368
|
+
[--so-cenarios] [--so-skills] [--json] (sai != 0 em qualquer falha)
|
|
369
369
|
|
|
370
370
|
audit packs [--estagio E] [--pack P] Os 7 packs, o estagio que os ativa e a postura
|
|
371
371
|
audit run <pack> [--profile P] Monta o prompt do pack e despacha pelo runtime adapter
|
|
@@ -413,7 +413,7 @@ Fases: GOAL PLAN GO CHECK SHIP MASTER. Modos: ${v.tags}.
|
|
|
413
413
|
Familias de lease: main-tree, worktree-write:<thread>, path:<glob>, board:<card>, service:<porta>.
|
|
414
414
|
Ciclos: greenfield, merge-branch, goal-plan, gap, feature-xl-faseada.
|
|
415
415
|
Fontes de medida do gate de tokens: runtime_reported, estimated, informada, unavailable.
|
|
416
|
-
Hosts de Camada 1: claude-code, codex, hermes, openclaw.
|
|
416
|
+
Hosts de Camada 1: claude-code, codex, hermes, openclaw. Cenarios: fx-happy, fx-hallucination,
|
|
417
417
|
fx-stale-base, fx-wiki-destroy, fx-schema-drift, fx-concurrency.
|
|
418
418
|
Packs de auditoria: clean-code, reuse, architecture, data-model, ux, security-privacy, process.
|
|
419
419
|
Superficie de ataque de rede (SP8..SP12, varredura deterministica): rota sem auth, endpoint de
|
|
@@ -778,8 +778,8 @@ Usage: ork [--projeto <name|path>] <command> [arguments]
|
|
|
778
778
|
[--aceitar-catalogo <file>] overwrites the copy with the catalog, file by file
|
|
779
779
|
[--manter-copia <file>] keeps the copy as it is, file by file
|
|
780
780
|
|
|
781
|
-
eval [--skill S] [--
|
|
782
|
-
[--so-
|
|
781
|
+
eval [--skill S] [--cenario C] Behavior scenarios + corpus of the thin skills
|
|
782
|
+
[--so-cenarios] [--so-skills] [--json] (exits != 0 on any failure)
|
|
783
783
|
|
|
784
784
|
audit packs [--estagio E] [--pack P] The 7 packs, the stage that activates them and the posture
|
|
785
785
|
audit run <pack> [--profile P] Builds the pack prompt and dispatches through the runtime adapter
|
|
@@ -827,7 +827,7 @@ Phases: GOAL PLAN GO CHECK SHIP MASTER. Modes: ${v.tags}.
|
|
|
827
827
|
Lease families: main-tree, worktree-write:<thread>, path:<glob>, board:<card>, service:<port>.
|
|
828
828
|
Cycles: greenfield, merge-branch, goal-plan, gap, feature-xl-faseada.
|
|
829
829
|
Token gate measurement sources: runtime_reported, estimated, informada, unavailable.
|
|
830
|
-
Layer 1 hosts: claude-code, codex, hermes, openclaw.
|
|
830
|
+
Layer 1 hosts: claude-code, codex, hermes, openclaw. Scenarios: fx-happy, fx-hallucination,
|
|
831
831
|
fx-stale-base, fx-wiki-destroy, fx-schema-drift, fx-concurrency.
|
|
832
832
|
Audit packs: clean-code, reuse, architecture, data-model, ux, security-privacy, process.
|
|
833
833
|
Network attack surface (SP8..SP12, deterministic scan): route without auth, exposed admin
|
|
@@ -339,23 +339,23 @@ exports.OPERACAO_RESTANTE_PT_BR = {
|
|
|
339
339
|
deveriaAusente: (valor, vezes) => `"${valor}" deveria estar AUSENTE da skill e aparece ${vezes}x`,
|
|
340
340
|
naoExiste: (valor, skill, porque) => `"${valor}" nao existe em skills/.../${skill}/SKILL.md (${porque})`,
|
|
341
341
|
ancora: 'todas as ancoras deste caso estao dentro da tabela de racionalizacoes: a skill lista a desculpa mas nao carrega a regra que responde a ela',
|
|
342
|
-
filtroVazio: 'filtro de
|
|
343
|
-
|
|
342
|
+
filtroVazio: 'filtro de cenários vazio: nenhuma prova foi executada',
|
|
343
|
+
cenarioAusente: (id) => `cenário solicitado ausente: ${id}`,
|
|
344
344
|
casoAusente: (nome) => `${nome}: falta caso.json`,
|
|
345
|
-
fixtureDiverge: 'id da fixture diverge do
|
|
346
|
-
semExecutor: (nome, id) => `${nome}: nenhum executor registrado para o
|
|
347
|
-
semGit: 'git nao esta nesta maquina: o
|
|
345
|
+
fixtureDiverge: 'id da fixture diverge do cenário solicitado',
|
|
346
|
+
semExecutor: (nome, id) => `${nome}: nenhum executor registrado para o cenario "${id}"`,
|
|
347
|
+
semGit: 'git nao esta nesta maquina: o cenario sai unavailable, nunca passing',
|
|
348
348
|
divergencia: (esperado, observado) => `esperado ${esperado}, observado ${observado}`,
|
|
349
|
-
|
|
349
|
+
cenarioSemFixture: (id) => `o cenario ${id} existe no codigo e nao tem eval/fixtures/${id}/caso.json`,
|
|
350
350
|
skillSemEval: (skill) => `skill sem eval nao entra no catalogo: falta eval/casos/${skill}.json`,
|
|
351
351
|
evalSemSkill: 'ha casos para uma skill que nao existe no catalogo',
|
|
352
352
|
titulo: (catalogo) => `ork eval, catalogo em ${catalogo}`,
|
|
353
|
-
|
|
354
|
-
|
|
353
|
+
cenarios: 'Cenarios (comportamento real do nucleo, contra git de verdade)',
|
|
354
|
+
colunasCenarios: ['CENARIO', 'ESTADO', 'SOBRE'],
|
|
355
355
|
estados: { passou: 'passou', unavailable: 'unavailable', falhou: 'falhou' },
|
|
356
356
|
skills: 'Corpus das skills (a metade estatica: as regras que precisam existir no arquivo)',
|
|
357
357
|
colunasSkills: ['SKILL', 'CASOS', 'ASSERCOES', 'FALHAS'], falhas: 'Falhas',
|
|
358
|
-
|
|
358
|
+
resumoCenarios: (passaram, total, indisponiveis) => `cenarios: ${passaram}/${total} verdes${indisponiveis > 0 ? ` (${indisponiveis} unavailable)` : ''}`,
|
|
359
359
|
resumoSkills: (skills, casos, assercoes, falhas) => `skills: ${skills} | casos: ${casos} | assercoes: ${assercoes} | falhas: ${falhas}`,
|
|
360
360
|
metade: 'metade comportamental (o agente resiste a desculpa?): unavailable, julgada por humano ou por modelo, nunca por este runner',
|
|
361
361
|
veredito: (ok) => `VEREDITO: ${ok ? 'verde' : 'vermelho'}`,
|
|
@@ -815,23 +815,23 @@ exports.OPERACAO_RESTANTE_EN = {
|
|
|
815
815
|
deveriaAusente: (value, times) => `"${value}" should be ABSENT from the skill and appears ${times} time(s)`,
|
|
816
816
|
naoExiste: (value, skill, why) => `"${value}" does not exist in skills/.../${skill}/SKILL.md (${why})`,
|
|
817
817
|
ancora: 'every anchor in this case is inside the rationalizations table: the skill lists the excuse but does not carry the rule that answers it',
|
|
818
|
-
filtroVazio: 'empty
|
|
819
|
-
|
|
818
|
+
filtroVazio: 'empty scenario filter: no evidence was executed',
|
|
819
|
+
cenarioAusente: (id) => `requested scenario is missing: ${id}`,
|
|
820
820
|
casoAusente: (name) => `${name}: caso.json is missing`,
|
|
821
|
-
fixtureDiverge: 'fixture id differs from the requested
|
|
822
|
-
semExecutor: (name, id) => `${name}: no executor registered for
|
|
823
|
-
semGit: 'git is not on this machine: the
|
|
821
|
+
fixtureDiverge: 'fixture id differs from the requested scenario',
|
|
822
|
+
semExecutor: (name, id) => `${name}: no executor registered for scenario "${id}"`,
|
|
823
|
+
semGit: 'git is not on this machine: the scenario is unavailable, never passing',
|
|
824
824
|
divergencia: (expected, observed) => `expected ${expected}, observed ${observed}`,
|
|
825
|
-
|
|
825
|
+
cenarioSemFixture: (id) => `scenario ${id} exists in code but has no eval/fixtures/${id}/caso.json`,
|
|
826
826
|
skillSemEval: (skill) => `a skill without eval does not enter the catalog: eval/casos/${skill}.json is missing`,
|
|
827
827
|
evalSemSkill: 'there are cases for a skill that does not exist in the catalog',
|
|
828
828
|
titulo: (catalog) => `ork eval, catalog at ${catalog}`,
|
|
829
|
-
|
|
830
|
-
|
|
829
|
+
cenarios: 'Scenarios (real core behavior, against real git)',
|
|
830
|
+
colunasCenarios: ['SCENARIO', 'STATE', 'SUBJECT'],
|
|
831
831
|
estados: { passou: 'passed', unavailable: 'unavailable', falhou: 'failed' },
|
|
832
832
|
skills: 'Skills corpus (the static half: rules that must exist in the file)',
|
|
833
833
|
colunasSkills: ['SKILL', 'CASES', 'ASSERTIONS', 'FAILURES'], falhas: 'Failures',
|
|
834
|
-
|
|
834
|
+
resumoCenarios: (passed, total, unavailable) => `scenarios: ${passed}/${total} green${unavailable > 0 ? ` (${unavailable} unavailable)` : ''}`,
|
|
835
835
|
resumoSkills: (skills, cases, assertions, failures) => `skills: ${skills} | cases: ${cases} | assertions: ${assertions} | failures: ${failures}`,
|
|
836
836
|
metade: 'behavioral half (does the agent resist the excuse?): unavailable, judged by a human or a model, never by this runner',
|
|
837
837
|
veredito: (ok) => `VERDICT: ${ok ? 'green' : 'red'}`,
|
package/dist/sandbox.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Sandbox deterministica para os
|
|
3
|
+
* Sandbox deterministica para os cenarios de `ork eval` (bloco B4).
|
|
4
4
|
*
|
|
5
|
-
* Um
|
|
5
|
+
* Um cenario precisa exercitar o comportamento REAL do nucleo: git de verdade, worktree de
|
|
6
6
|
* verdade, lease de verdade. Fazer isso no repositorio do usuario seria inaceitavel, e
|
|
7
7
|
* simular git com dublê seria testar o dublê. A saida e um repositorio git temporario,
|
|
8
8
|
* criado, usado e apagado dentro da mesma execucao.
|
|
9
9
|
*
|
|
10
10
|
* Regras que esta sandbox respeita, e por isso ela pode rodar em `ork eval` na maquina de
|
|
11
11
|
* qualquer builder: nada de rede, nada de credencial, nada fora de um diretorio temporario,
|
|
12
|
-
* e limpeza garantida mesmo quando o
|
|
12
|
+
* e limpeza garantida mesmo quando o cenario falha.
|
|
13
13
|
*/
|
|
14
14
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
15
|
if (k2 === undefined) k2 = k;
|
|
@@ -55,7 +55,7 @@ const path = __importStar(require("node:path"));
|
|
|
55
55
|
const init_1 = require("./init");
|
|
56
56
|
const manifest_1 = require("./manifest");
|
|
57
57
|
const util_1 = require("./util");
|
|
58
|
-
/** `git` existe nesta maquina? Sem ele os
|
|
58
|
+
/** `git` existe nesta maquina? Sem ele os cenarios saem `unavailable`, nunca `passing`. */
|
|
59
59
|
function gitDisponivel() {
|
|
60
60
|
return (0, util_1.noPath)('git') !== null;
|
|
61
61
|
}
|
|
@@ -75,15 +75,15 @@ function commitar(dir, arquivo, conteudo, mensagem) {
|
|
|
75
75
|
/**
|
|
76
76
|
* Repositorio git temporario com manifesto gerado pelo proprio `ork init`.
|
|
77
77
|
*
|
|
78
|
-
* O manifesto vem do `init` de verdade e nao de um objeto escrito a mao:
|
|
79
|
-
* proprio manifesto testa o
|
|
78
|
+
* O manifesto vem do `init` de verdade e nao de um objeto escrito a mao: cenario que monta o
|
|
79
|
+
* proprio manifesto testa o cenario, nao o produto.
|
|
80
80
|
*/
|
|
81
81
|
function sandboxGit(nome, abbrev = 'evl') {
|
|
82
|
-
// I-49:
|
|
82
|
+
// I-49: cenario nunca escreve no registro compartilhado de contas do usuario.
|
|
83
83
|
if (!process.env.ORK_CONTAS_DIR) {
|
|
84
84
|
process.env.ORK_CONTAS_DIR = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'ork-contas-eval-')), 'private');
|
|
85
85
|
}
|
|
86
|
-
// I-51:
|
|
86
|
+
// I-51: cenario nunca publica o estado da fabrica.
|
|
87
87
|
if (process.env.ORK_FABRICA_PUBLICAR === undefined)
|
|
88
88
|
process.env.ORK_FABRICA_PUBLICAR = '0';
|
|
89
89
|
if (!process.env.ORK_USUARIO_DIR)
|
|
@@ -91,10 +91,10 @@ function sandboxGit(nome, abbrev = 'evl') {
|
|
|
91
91
|
const dir = dirTemporario(nome);
|
|
92
92
|
(0, util_1.exec)('git', ['init', '-b', 'main'], dir);
|
|
93
93
|
(0, util_1.exec)('git', ['config', 'user.email', 'eval@orkastery.local'], dir);
|
|
94
|
-
(0, util_1.exec)('git', ['config', 'user.name', '
|
|
94
|
+
(0, util_1.exec)('git', ['config', 'user.name', 'Cenario do ork eval'], dir);
|
|
95
95
|
(0, util_1.exec)('git', ['config', 'commit.gpgsign', 'false'], dir);
|
|
96
|
-
commitar(dir, 'README.md', '# projeto do
|
|
97
|
-
(0, init_1.init)(dir, { nome: '
|
|
96
|
+
commitar(dir, 'README.md', '# projeto do cenario\n', 'inicial');
|
|
97
|
+
(0, init_1.init)(dir, { nome: 'cenario', abbrev });
|
|
98
98
|
return {
|
|
99
99
|
dir,
|
|
100
100
|
carregado: (0, manifest_1.exigirManifesto)(dir),
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.PRAZO_MINIMO_DO_CLASSIC_MS = exports.PRAZO_DO_BUILD_DA_IMAGEM_MS = exports.PRAZO_POR_REGRA_DO_SEMGREP_S =
|
|
36
|
+
exports.PRAZO_MINIMO_DO_CLASSIC_MS = exports.PRAZO_DO_BUILD_DA_IMAGEM_MS = exports.PRAZO_POR_REGRA_DO_SEMGREP_S = void 0;
|
|
37
37
|
exports.especificacaoDoAgile = especificacaoDoAgile;
|
|
38
38
|
exports.prazoDoBuildDaImagem = prazoDoBuildDaImagem;
|
|
39
39
|
exports.estourouNoAgile = estourouNoAgile;
|
|
@@ -48,9 +48,9 @@ exports.runScanAgil = runScanAgil;
|
|
|
48
48
|
* ferramentas do catalogo e a mesma classificacao contra a base. O que muda e o empacotamento: a arvore da base sai uma
|
|
49
49
|
* vez, antes, e cada ferramenta roda num trabalhador proprio (`trabalhador.ts`), ao mesmo tempo, chamando o mesmo
|
|
50
50
|
* `scanTool` do Classic; o relatorio sai na ordem do catalogo. As ferramentas do Trivy montam um diretorio persistente
|
|
51
|
-
* por ferramenta no `/tmp/trivy-cache` do catalogo
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* por ferramenta no `/tmp/trivy-cache` do catalogo. O resultado normalizado do Semgrep e das ferramentas de dependência
|
|
52
|
+
* fica no cache da máquina, por versão, regras e conteúdo; o Classic continua varrendo toda a árvore. Cada peça do Agile
|
|
53
|
+
* que não pode rodar cai para o equivalente do Classic e vai ao relatório
|
|
54
54
|
* (`check.pecas`) e ao `ORK_CHECK_RELATORIO` do verify.
|
|
55
55
|
*/
|
|
56
56
|
const fs = __importStar(require("node:fs"));
|
|
@@ -63,6 +63,7 @@ const redacao_url_1 = require("../redacao-url");
|
|
|
63
63
|
const compare_1 = require("./compare");
|
|
64
64
|
const config_1 = require("./config");
|
|
65
65
|
const guard_1 = require("./guard");
|
|
66
|
+
const incremental_1 = require("./incremental");
|
|
66
67
|
const run_1 = require("./run");
|
|
67
68
|
const executor_1 = require("./executor");
|
|
68
69
|
const imagem_de_lint_1 = require("./imagem-de-lint");
|
|
@@ -71,12 +72,9 @@ const TRABALHADOR = path.join(__dirname, 'trabalhador.js');
|
|
|
71
72
|
/** Sem prazo da rodada, o trabalhador ganha o maior prazo do catalogo para a HEAD e para a base, com folga. */
|
|
72
73
|
const FOLGA_MS = 60_000;
|
|
73
74
|
/**
|
|
74
|
-
* O
|
|
75
|
-
*
|
|
76
|
-
* fica de fora, declarado como peca em fallback.
|
|
75
|
+
* O cache guarda os achados normalizados junto da identidade das regras do registro. Assim uma rodada morna devolve o
|
|
76
|
+
* `check_id` original, em vez de reler uma regra por arquivo local e reescrever a impressão digital.
|
|
77
77
|
*/
|
|
78
|
-
exports.SEMGREP_SEM_CACHE = { peca: 'cache-semgrep', estado: 'fallback',
|
|
79
|
-
motivo: 'regra lida de arquivo local troca o check_id e a impressao digital', fallback: 'regras do registro a cada rodada, como no Classic' };
|
|
80
78
|
/**
|
|
81
79
|
* O prazo por regra e arquivo do Semgrep no Agile, em segundos. O padrao dele e 5 s; com as ferramentas juntas (e, no
|
|
82
80
|
* verify, junto das lanes de teste) a disputa de CPU fazia uma regra estourar (`partial-timeout` medido na primeira
|
|
@@ -284,8 +282,8 @@ function runScanAgil(opts, rodar = check_paralelo_1.rodarEmParalelo) {
|
|
|
284
282
|
return report;
|
|
285
283
|
}
|
|
286
284
|
report.base = base;
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
pecas.push({ peca: 'varredura-incremental', estado: 'agile',
|
|
286
|
+
motivo: 'arquivo pela versao e regras da ferramenta; dependencias pelo lockfile; demais ferramentas rodam inteiras' });
|
|
289
287
|
const count = (0, run_1.git)(root, ['rev-list', '--count', `${base}..${head}`]);
|
|
290
288
|
const deadline = opts.deadlineMs === undefined ? null : Date.now() + opts.deadlineMs;
|
|
291
289
|
// A arvore da base sai uma vez, aqui, e os trabalhadores a dividem (cada um varre a sua ferramenta nela).
|
|
@@ -298,6 +296,19 @@ function runScanAgil(opts, rodar = check_paralelo_1.rodarEmParalelo) {
|
|
|
298
296
|
}
|
|
299
297
|
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ork-security-scan-agil-')));
|
|
300
298
|
guard.add('outDirs', dir);
|
|
299
|
+
// A peça do cache já fazia parte do contrato ordenado do relatório antes das peças de executor. Resolver aqui mantém
|
|
300
|
+
// essa ordem pública; o mesmo resultado segue no trabalho que será entregue ao processo da ferramenta.
|
|
301
|
+
const regrasSemgrepPorIndice = new Map();
|
|
302
|
+
tools.forEach((t, i) => {
|
|
303
|
+
if (t.name !== 'semgrep')
|
|
304
|
+
return;
|
|
305
|
+
const regrasSemgrep = (0, incremental_1.resolverRegrasSemgrep)(t, (0, run_1.unitsOf)(t, pairs));
|
|
306
|
+
regrasSemgrepPorIndice.set(i, regrasSemgrep);
|
|
307
|
+
pecas.push(regrasSemgrep.reutilizavel
|
|
308
|
+
? { peca: 'cache-semgrep', estado: 'agile', motivo: 'hash do conteúdo das regras resolvidas; check_id preservado' }
|
|
309
|
+
: { peca: 'cache-semgrep', estado: 'fallback', motivo: regrasSemgrep.motivo,
|
|
310
|
+
fallback: 'varredura integral, como no Classic' });
|
|
311
|
+
});
|
|
301
312
|
// RM-072 (D12): quem tem o binario nativo na versao do catalogo roda sem o Docker; a peca diz quem faltou.
|
|
302
313
|
const probe = opts.probe ?? (0, executor_1.defaultProbe)();
|
|
303
314
|
const comNativo = tools.filter(t => t.native && !t.install);
|
|
@@ -310,7 +321,12 @@ function runScanAgil(opts, rodar = check_paralelo_1.rodarEmParalelo) {
|
|
|
310
321
|
}
|
|
311
322
|
const montados = tools.map((t, i) => {
|
|
312
323
|
const restante = deadline === null ? t.timeoutMs : Math.min(t.timeoutMs, deadline - Date.now());
|
|
313
|
-
const
|
|
324
|
+
const units = (0, run_1.unitsOf)(t, pairs);
|
|
325
|
+
const tool = eslintDoAgile(especificacaoDoAgile(t, pecas), restante, pecas);
|
|
326
|
+
const regrasSemgrep = regrasSemgrepPorIndice.get(i);
|
|
327
|
+
const trabalho = { tool, units, config,
|
|
328
|
+
incremental: true,
|
|
329
|
+
...(regrasSemgrep ? { regrasSemgrep } : {}),
|
|
314
330
|
...(nativos.has(t.name) ? { nativoPrimeiro: true } : {}),
|
|
315
331
|
ctx: { headTree, mainRoot, base, head, rangeCommits: count === null ? null : Number(count), deadline, ...(baseTree ? { baseTree } : {}) } };
|
|
316
332
|
const arquivo = path.join(dir, `${i}.json`);
|
|
@@ -160,6 +160,7 @@ function runDocker(inv) {
|
|
|
160
160
|
...Object.entries(tool.env ?? {}).flatMap(([k, v]) => ['-e', `${k}=${v}`]),
|
|
161
161
|
'--entrypoint', tool.containerBin ?? inv.argv[0], tool.image,
|
|
162
162
|
...inv.argv.slice(1).map(a => expand(a, values))];
|
|
163
|
+
(0, agendador_cpu_1.registrarContainerDaLane)(name);
|
|
163
164
|
inv.guard?.add('containers', name);
|
|
164
165
|
const r = (0, node_child_process_1.spawnSync)('docker', args, { encoding: 'utf8', timeout: inv.timeoutMs, killSignal: 'SIGKILL', maxBuffer: MAX_BUFFER, env: process.env });
|
|
165
166
|
const timedOut = r.error?.code === 'ETIMEDOUT';
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.lerCacheIncremental = void 0;
|
|
37
|
+
exports.resolverRegrasSemgrep = resolverRegrasSemgrep;
|
|
38
|
+
exports.consultarCacheIncremental = consultarCacheIncremental;
|
|
39
|
+
exports.gravarCacheIncremental = gravarCacheIncremental;
|
|
40
|
+
/** Cache de resultado normalizado do Agile, por conteúdo e fora do repositório. */
|
|
41
|
+
const crypto = __importStar(require("node:crypto"));
|
|
42
|
+
const node_child_process_1 = require("node:child_process");
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const run_1 = require("./run");
|
|
46
|
+
const CONTRATO = 'ork.security-scan.incremental/v1';
|
|
47
|
+
const LOCKFILES = new Set(['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'poetry.lock',
|
|
48
|
+
'Pipfile.lock', 'go.mod', 'go.sum']);
|
|
49
|
+
const sha256 = (valor) => crypto.createHash('sha256').update(valor).digest('hex');
|
|
50
|
+
const SCRIPT_DO_REGISTRO = String.raw `
|
|
51
|
+
# ORK_SEMGREP_RESOLVE_V1
|
|
52
|
+
import sys, urllib.request
|
|
53
|
+
pedido = urllib.request.Request(sys.argv[1], headers={"Accept": "application/json", "User-Agent": "orkastery-security-scan"})
|
|
54
|
+
with urllib.request.urlopen(pedido, timeout=20) as resposta:
|
|
55
|
+
if resposta.status != 200:
|
|
56
|
+
raise RuntimeError("HTTP " + str(resposta.status))
|
|
57
|
+
sys.stdout.buffer.write(resposta.read())
|
|
58
|
+
`;
|
|
59
|
+
/** O mesmo endpoint público para o qual o resolvedor do Semgrep mapeia um id do registro. */
|
|
60
|
+
function urlDoRegistro(referencia) {
|
|
61
|
+
if (/^[prs]\/[A-Za-z0-9._/-]+$/.test(referencia))
|
|
62
|
+
return `https://semgrep.dev/c/${referencia}`;
|
|
63
|
+
if (/^https:\/\//.test(referencia))
|
|
64
|
+
return referencia;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function baixarDoRegistro(ferramenta, url) {
|
|
68
|
+
const uid = process.getuid?.() ?? 65534;
|
|
69
|
+
const gid = process.getgid?.() ?? 65534;
|
|
70
|
+
const nome = `ork-security-scan-semgrep-registry-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
71
|
+
const r = (0, node_child_process_1.spawnSync)('docker', ['run', '--rm', '--name', nome, '--user', `${uid === 0 ? 65534 : uid}:${uid === 0 ? 65534 : gid}`,
|
|
72
|
+
'--network', 'default', '--entrypoint', 'python3', ferramenta.image, '-c', SCRIPT_DO_REGISTRO, url], { encoding: 'utf8', timeout: 30_000, maxBuffer: 64 * 1024 * 1024, killSignal: 'SIGKILL' });
|
|
73
|
+
if (r.error || r.status !== 0)
|
|
74
|
+
(0, node_child_process_1.spawnSync)('docker', ['rm', '-f', nome], { stdio: 'ignore', timeout: 10_000 });
|
|
75
|
+
return !r.error && r.status === 0 && r.stdout.trim() ? r.stdout : null;
|
|
76
|
+
}
|
|
77
|
+
/** O conteúdo que o próprio Semgrep extrai da resposta do registro (`rules` direto ou envelope `rule_config`). */
|
|
78
|
+
function conteudoResolvido(resposta) {
|
|
79
|
+
try {
|
|
80
|
+
const json = JSON.parse(resposta);
|
|
81
|
+
const regras = Object.prototype.hasOwnProperty.call(json, 'rules') ? json : json.rule_config;
|
|
82
|
+
return typeof regras === 'string' ? regras : JSON.stringify(regras);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return resposta;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const MARCAS_ENTRE_ARQUIVOS = ['join', 'interfile', 'crossfile', 'multifile'];
|
|
89
|
+
const MARCA_NA_LINHA = /(?:^|[\s{,])(?:mode\s*:\s*)?(?:join|inter[-_ ]?file|cross[-_ ]?file|multi[-_ ]?file)(?:\s*:|\s*$)/i;
|
|
90
|
+
const CHAVE_DA_LINHA = /^(\s*)(-\s+)?([A-Za-z_][\w-]*)\s*:(?:\s+(.*))?$/;
|
|
91
|
+
const normalizar = (chave) => chave.toLowerCase().replace(/[-_]/g, '');
|
|
92
|
+
/**
|
|
93
|
+
* Se a marca cruza arquivos de fato. `join` cruza em qualquer motor. `interfile` em `options` só cruza no motor Pro do
|
|
94
|
+
* Semgrep; em `metadata` é rótulo do registro, que o motor ignora. Em qualquer outro lugar, cruza (conservador).
|
|
95
|
+
*/
|
|
96
|
+
function marcaCruzaArquivos(marca, pai, pro) {
|
|
97
|
+
if (marca === 'join')
|
|
98
|
+
return true;
|
|
99
|
+
if (pai === 'metadata')
|
|
100
|
+
return false;
|
|
101
|
+
if (pai === 'options')
|
|
102
|
+
return pro;
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* O registro responde em YAML. Toda linha com marca cruza arquivos, menos a chave da própria marca, em bloco e com valor
|
|
107
|
+
* booleano, cujo pai pela indentação a torna inócua no motor da rodada. Linha de texto que parece chave nunca relaxa nada.
|
|
108
|
+
*/
|
|
109
|
+
function marcaNoYaml(conteudo, pro) {
|
|
110
|
+
const pais = [];
|
|
111
|
+
for (const linha of conteudo.split(/\r?\n/)) {
|
|
112
|
+
const m = CHAVE_DA_LINHA.exec(linha);
|
|
113
|
+
const coluna = m ? m[1].length + (m[2]?.length ?? 0) : 0;
|
|
114
|
+
if (m)
|
|
115
|
+
while (pais.length && pais[pais.length - 1].coluna >= coluna)
|
|
116
|
+
pais.pop();
|
|
117
|
+
if (MARCA_NA_LINHA.test(linha)) {
|
|
118
|
+
const chave = m ? normalizar(m[3]) : null;
|
|
119
|
+
const booleano = !!m && /^(?:true|false|yes|no|on|off)?\s*(?:#.*)?$/i.test(m[4] ?? '');
|
|
120
|
+
if (!chave || !MARCAS_ENTRE_ARQUIVOS.includes(chave) || !booleano
|
|
121
|
+
|| marcaCruzaArquivos(chave, pais[pais.length - 1]?.chave ?? null, pro))
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (m)
|
|
125
|
+
pais.push({ coluna, chave: normalizar(m[3]) });
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
function regraEntreArquivos(conteudo, pro) {
|
|
130
|
+
let valor;
|
|
131
|
+
try {
|
|
132
|
+
valor = JSON.parse(conteudo);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return marcaNoYaml(conteudo, pro);
|
|
136
|
+
}
|
|
137
|
+
if (typeof valor === 'string')
|
|
138
|
+
return marcaNoYaml(valor, pro);
|
|
139
|
+
const visitar = (atual, pai) => {
|
|
140
|
+
if (Array.isArray(atual))
|
|
141
|
+
return atual.some((item) => visitar(item, pai));
|
|
142
|
+
if (!atual || typeof atual !== 'object')
|
|
143
|
+
return false;
|
|
144
|
+
return Object.entries(atual).some(([chave, filho]) => {
|
|
145
|
+
const normal = normalizar(chave);
|
|
146
|
+
if (normal === 'mode' && typeof filho === 'string' && /^(join|interfile|crossfile|multifile)$/i.test(filho))
|
|
147
|
+
return true;
|
|
148
|
+
if (MARCAS_ENTRE_ARQUIVOS.includes(normal) && filho !== false && filho !== null
|
|
149
|
+
&& marcaCruzaArquivos(normal, pai, pro))
|
|
150
|
+
return true;
|
|
151
|
+
return visitar(filho, normal);
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
return visitar(valor, null);
|
|
155
|
+
}
|
|
156
|
+
/** O Semgrep da rodada usa o motor Pro, o único que cruza arquivos nas regras `interfile`: `--pro*`, `semgrep ci` ou token. */
|
|
157
|
+
function motorPro(ferramenta, unidades) {
|
|
158
|
+
const stacks = new Set(unidades.map((u) => u.stack).filter((s) => !!s));
|
|
159
|
+
const todos = [ferramenta.args, ...[...stacks].map((s) => ferramenta.argsByStack?.[s] ?? [])];
|
|
160
|
+
return todos.some((args) => args.some((a) => a.startsWith('--pro')) || args.includes('ci'))
|
|
161
|
+
|| Object.keys(ferramenta.env ?? {}).some((k) => /^SEMGREP_APP_TOKEN$/i.test(k));
|
|
162
|
+
}
|
|
163
|
+
function referenciasDasRegras(ferramenta, unidades) {
|
|
164
|
+
const stacks = new Set(unidades.map((u) => u.stack).filter((s) => !!s));
|
|
165
|
+
const referencias = new Set();
|
|
166
|
+
for (const stack of stacks) {
|
|
167
|
+
const args = ferramenta.argsByStack?.[stack] ?? [];
|
|
168
|
+
for (let i = 0; i < args.length - 1; i++)
|
|
169
|
+
if (args[i] === '--config' || args[i] === '-f')
|
|
170
|
+
referencias.add(args[++i]);
|
|
171
|
+
}
|
|
172
|
+
return [...referencias].sort();
|
|
173
|
+
}
|
|
174
|
+
function resolverRegrasSemgrep(ferramenta, unidades, baixar = baixarDoRegistro) {
|
|
175
|
+
const referencias = referenciasDasRegras(ferramenta, unidades);
|
|
176
|
+
if (referencias.length === 0)
|
|
177
|
+
return { reutilizavel: false, hash: null, motivo: 'regras-sem-referencia', referencias };
|
|
178
|
+
const resolvidas = [];
|
|
179
|
+
const pro = motorPro(ferramenta, unidades);
|
|
180
|
+
for (const referencia of referencias) {
|
|
181
|
+
const url = urlDoRegistro(referencia);
|
|
182
|
+
if (!url)
|
|
183
|
+
return { reutilizavel: false, hash: null, motivo: 'referencia-de-regra-nao-resolvivel', referencias };
|
|
184
|
+
const resposta = baixar(ferramenta, url);
|
|
185
|
+
if (!resposta)
|
|
186
|
+
return { reutilizavel: false, hash: null, motivo: 'registro-indisponivel', referencias };
|
|
187
|
+
const conteudo = conteudoResolvido(resposta);
|
|
188
|
+
if (regraEntreArquivos(conteudo, pro))
|
|
189
|
+
return { reutilizavel: false, hash: null, motivo: 'regra-entre-arquivos', referencias };
|
|
190
|
+
resolvidas.push([referencia, conteudo]);
|
|
191
|
+
}
|
|
192
|
+
return { reutilizavel: true, hash: sha256(JSON.stringify(resolvidas)), referencias };
|
|
193
|
+
}
|
|
194
|
+
/** Só habilita o que tem garantia explícita: Semgrep por arquivo e dependências por lockfile. */
|
|
195
|
+
function escopoDaFerramenta(t, trabalho) {
|
|
196
|
+
if (t.name === 'semgrep') {
|
|
197
|
+
// A API direta antecede o trabalhador e continua útil para conferir o retrato por arquivo isoladamente. No caminho
|
|
198
|
+
// real do Agile, `incremental: true` torna obrigatória a identidade das regras resolvidas: ausência, registro
|
|
199
|
+
// indisponível ou regra entre arquivos nunca cai nesta compatibilidade.
|
|
200
|
+
if (trabalho.incremental !== true)
|
|
201
|
+
return 'arquivo';
|
|
202
|
+
return trabalho.regrasSemgrep?.reutilizavel && trabalho.regrasSemgrep.hash ? 'arquivo' : null;
|
|
203
|
+
}
|
|
204
|
+
if (t.category === 'dependency')
|
|
205
|
+
return 'lockfile';
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
function arquivos(root, escopo) {
|
|
209
|
+
const saida = {};
|
|
210
|
+
const visitar = (dir) => {
|
|
211
|
+
let nomes;
|
|
212
|
+
try {
|
|
213
|
+
nomes = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
for (const nome of nomes) {
|
|
219
|
+
if (nome.name === '.git')
|
|
220
|
+
continue;
|
|
221
|
+
const absoluto = path.join(dir, nome.name);
|
|
222
|
+
if (nome.isDirectory()) {
|
|
223
|
+
visitar(absoluto);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (!nome.isFile())
|
|
227
|
+
continue;
|
|
228
|
+
const relativo = path.relative(root, absoluto).split(path.sep).join('/');
|
|
229
|
+
const lockfile = LOCKFILES.has(nome.name) || /^requirements(?:\.[^.]+)?\.txt$/.test(nome.name);
|
|
230
|
+
if (escopo === 'lockfile' && !lockfile)
|
|
231
|
+
continue;
|
|
232
|
+
try {
|
|
233
|
+
saida[relativo] = sha256(fs.readFileSync(absoluto));
|
|
234
|
+
}
|
|
235
|
+
catch { /* arquivo mudou durante a leitura: nao entra */ }
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
visitar(root);
|
|
239
|
+
return saida;
|
|
240
|
+
}
|
|
241
|
+
function identidade(trabalho, ferramenta) {
|
|
242
|
+
return sha256(JSON.stringify({ ferramenta, unidades: trabalho.units, configuracao: trabalho.config, base: trabalho.ctx.base,
|
|
243
|
+
regrasSemgrep: trabalho.regrasSemgrep?.hash ?? null }));
|
|
244
|
+
}
|
|
245
|
+
function localDaEntrada(chave) {
|
|
246
|
+
return path.join((0, run_1.cacheRoot)(), 'incremental', `${chave}.json`);
|
|
247
|
+
}
|
|
248
|
+
function consultarCacheIncremental(trabalho, ferramenta) {
|
|
249
|
+
const escopo = escopoDaFerramenta(ferramenta, trabalho);
|
|
250
|
+
if (!escopo)
|
|
251
|
+
return { escopo: null, atual: null, anterior: null, alterados: [], reutilizado: null };
|
|
252
|
+
const retrato = { escopo, arquivos: arquivos(trabalho.ctx.headTree, escopo) };
|
|
253
|
+
if (escopo === 'lockfile' && Object.keys(retrato.arquivos).length === 0) {
|
|
254
|
+
return { escopo, atual: retrato, anterior: null, alterados: [], reutilizado: null };
|
|
255
|
+
}
|
|
256
|
+
const chave = identidade(trabalho, ferramenta);
|
|
257
|
+
try {
|
|
258
|
+
const entrada = JSON.parse(fs.readFileSync(localDaEntrada(chave), 'utf8'));
|
|
259
|
+
if (entrada.contrato !== CONTRATO || entrada.chave !== chave || entrada.resultado?.run?.state !== 'ran') {
|
|
260
|
+
return { escopo, atual: retrato, anterior: null, alterados: [], reutilizado: null };
|
|
261
|
+
}
|
|
262
|
+
const nomes = new Set([...Object.keys(entrada.retrato.arquivos), ...Object.keys(retrato.arquivos)]);
|
|
263
|
+
const alterados = [...nomes].filter((nome) => entrada.retrato.arquivos[nome] !== retrato.arquivos[nome]).sort();
|
|
264
|
+
return { escopo, atual: retrato, anterior: entrada.resultado, alterados,
|
|
265
|
+
reutilizado: alterados.length === 0 ? entrada.resultado : null };
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return { escopo, atual: retrato, anterior: null, alterados: [], reutilizado: null };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const lerCacheIncremental = (trabalho, ferramenta) => consultarCacheIncremental(trabalho, ferramenta).reutilizado;
|
|
272
|
+
exports.lerCacheIncremental = lerCacheIncremental;
|
|
273
|
+
function gravarCacheIncremental(trabalho, ferramenta, resultado) {
|
|
274
|
+
const escopo = escopoDaFerramenta(ferramenta, trabalho);
|
|
275
|
+
if (!escopo || resultado.run.state !== 'ran')
|
|
276
|
+
return;
|
|
277
|
+
const retrato = { escopo, arquivos: arquivos(trabalho.ctx.headTree, escopo) };
|
|
278
|
+
if (escopo === 'lockfile' && Object.keys(retrato.arquivos).length === 0)
|
|
279
|
+
return;
|
|
280
|
+
const chave = identidade(trabalho, ferramenta), arquivo = localDaEntrada(chave);
|
|
281
|
+
try {
|
|
282
|
+
fs.mkdirSync(path.dirname(arquivo), { recursive: true, mode: 0o700 });
|
|
283
|
+
const temporario = `${arquivo}.${process.pid}.tmp`;
|
|
284
|
+
const entrada = { contrato: CONTRATO, chave, retrato, resultado };
|
|
285
|
+
fs.writeFileSync(temporario, JSON.stringify(entrada), { mode: 0o600 });
|
|
286
|
+
fs.renameSync(temporario, arquivo);
|
|
287
|
+
}
|
|
288
|
+
catch { /* cache ausente custa CPU, mas nunca muda o veredito */ }
|
|
289
|
+
}
|