@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.
Files changed (45) hide show
  1. package/adapters/claude-code/README.md +1 -1
  2. package/adapters/hermes/README.md +1 -1
  3. package/adapters/openclaw/openclaw.plugin.json +1 -1
  4. package/adapters/openclaw/package.json +1 -1
  5. package/assets/adapters-en/claude-code/README.md +1 -1
  6. package/assets/adapters-en/hermes/README.md +1 -1
  7. package/assets/docs/padroes/roadmap-de-produto.md +1 -1
  8. package/assets/docs-en/padroes/roadmap-de-produto.md +1 -1
  9. package/assets/references-en/testing-patterns.md +1 -1
  10. package/dist/agendador-cpu.js +49 -5
  11. package/dist/catalogo.js +3 -3
  12. package/dist/{canarios-hitl.js → cenarios-hitl.js} +7 -7
  13. package/dist/{canarios-i43.js → cenarios-i43.js} +16 -16
  14. package/dist/{canarios-pulse.js → cenarios-pulse.js} +3 -3
  15. package/dist/{canarios-rm057.js → cenarios-rm057.js} +3 -3
  16. package/dist/{canarios-sensores.js → cenarios-sensores.js} +9 -8
  17. package/dist/{canarios.js → cenarios.js} +22 -22
  18. package/dist/check-agil.js +2 -1
  19. package/dist/check-paralelo.js +337 -14
  20. package/dist/claims.js +1 -1
  21. package/dist/docs.js +1 -1
  22. package/dist/evalrunner.js +37 -37
  23. package/dist/hitl-canais.js +1 -1
  24. package/dist/hitl-presentation.js +1 -1
  25. package/dist/hitl-tempo-parado.js +1 -1
  26. package/dist/index.js +4 -4
  27. package/dist/init.js +1 -1
  28. package/dist/locale-ajuda.js +6 -6
  29. package/dist/locale-operacao-restante.js +18 -18
  30. package/dist/sandbox.js +11 -11
  31. package/dist/security-scan/agil.js +28 -12
  32. package/dist/security-scan/executor.js +1 -0
  33. package/dist/security-scan/incremental.js +289 -0
  34. package/dist/security-scan/trabalhador.js +53 -5
  35. package/dist/verify.js +12 -1
  36. package/eval/README.md +8 -8
  37. package/eval/fixtures/fx-codex-dry/caso.json +1 -1
  38. package/eval/fixtures/fx-happy/caso.json +1 -1
  39. package/eval/fixtures/fx-indice-reversao/caso.json +1 -1
  40. package/eval/fixtures/fx-listagem-abertas/caso.json +1 -1
  41. package/eval/fixtures/fx-omnicanal/caso.json +1 -1
  42. package/eval/fixtures/fx-pedido-colado/caso.json +1 -1
  43. package/eval/fixtures/fx-schema-drift/caso.json +1 -1
  44. package/package.json +1 -1
  45. 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 canario `fx-pedido-colado` e a regra nos adaptadores existem.
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`: canarios de comportamento e o corpus das skills finas (bloco B4).
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 canario tambem nao. O comando sai diferente
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
- canarios: lista(args.opcoes.canario) ?? lista(args.opcoes.fixture),
3641
- soCanarios: args.opcoes['so-canarios'] === true,
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
- * canarios montam o projeto por ela).
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);
@@ -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] [--canario C] Canarios de comportamento + corpus das skills finas
368
- [--so-canarios] [--so-skills] [--json] (sai != 0 em qualquer falha)
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. Canarios: fx-happy, fx-hallucination,
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] [--canario C] Behavior canaries + corpus of the thin skills
782
- [--so-canarios] [--so-skills] [--json] (exits != 0 on any failure)
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. Canaries: fx-happy, fx-hallucination,
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 canários vazio: nenhuma prova foi executada',
343
- canarioAusente: (id) => `canário solicitado ausente: ${id}`,
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 canário solicitado',
346
- semExecutor: (nome, id) => `${nome}: nenhum executor registrado para o canario "${id}"`,
347
- semGit: 'git nao esta nesta maquina: o canario sai unavailable, nunca passing',
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
- canarioSemFixture: (id) => `o canario ${id} existe no codigo e nao tem eval/fixtures/${id}/caso.json`,
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
- canarios: 'Canarios (comportamento real do nucleo, contra git de verdade)',
354
- colunasCanarios: ['CANARIO', 'ESTADO', 'SOBRE'],
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
- resumoCanarios: (passaram, total, indisponiveis) => `canarios: ${passaram}/${total} verdes${indisponiveis > 0 ? ` (${indisponiveis} unavailable)` : ''}`,
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 canary filter: no evidence was executed',
819
- canarioAusente: (id) => `requested canary is missing: ${id}`,
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 canary',
822
- semExecutor: (name, id) => `${name}: no executor registered for canary "${id}"`,
823
- semGit: 'git is not on this machine: the canary is unavailable, never passing',
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
- canarioSemFixture: (id) => `canary ${id} exists in code but has no eval/fixtures/${id}/caso.json`,
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
- canarios: 'Canaries (real core behavior, against real git)',
830
- colunasCanarios: ['CANARY', 'STATE', 'SUBJECT'],
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
- resumoCanarios: (passed, total, unavailable) => `canaries: ${passed}/${total} green${unavailable > 0 ? ` (${unavailable} unavailable)` : ''}`,
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 canarios de `ork eval` (bloco B4).
3
+ * Sandbox deterministica para os cenarios de `ork eval` (bloco B4).
4
4
  *
5
- * Um canario precisa exercitar o comportamento REAL do nucleo: git de verdade, worktree de
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 canario falha.
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 canarios saem `unavailable`, nunca `passing`. */
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: canario que monta o
79
- * proprio manifesto testa o canario, nao o produto.
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: canario nunca escreve no registro compartilhado de contas do usuario.
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: canario nunca publica o estado da fabrica.
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', 'Canario do ork eval'], dir);
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 canario\n', 'inicial');
97
- (0, init_1.init)(dir, { nome: 'canario', abbrev });
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 = exports.SEMGREP_SEM_CACHE = void 0;
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, para o banco nao descer a cada rodada (cache de banco, nunca de
52
- * resultado: o HEAD e sempre varrido de novo, e a base usa o cache por SHA que o `scanBase` ja tem, com a chave da
53
- * especificacao do Agile). Cada peca do Agile que nao pode rodar cai para o equivalente do Classic e vai ao relatorio
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 Semgrep baixa as regras do registro (`p/typescript`, ...) a cada rodada, e nao tem cache delas. Regra lida de arquivo
75
- * local ganha outro `check_id` (o prefixo do caminho), o que muda a impressao digital de todo achado: o cache de regras
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
- if (tools.some(t => t.name === 'semgrep'))
288
- pecas.push(exports.SEMGREP_SEM_CACHE);
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 trabalho = { tool: eslintDoAgile(especificacaoDoAgile(t, pecas), restante, pecas), units: (0, run_1.unitsOf)(t, pairs), config,
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
+ }