@orkastery/cli 0.6.3 → 0.6.5

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/dist/manifest.js CHANGED
@@ -257,6 +257,197 @@ function lerCheck(v, erros, avisos) {
257
257
  }
258
258
  /** Nome de lane: o que vira `test:<lane>` no ledger. */
259
259
  const NOME_DE_LANE = /^[a-z0-9][a-z0-9-]{0,31}$/;
260
+ /** RM-081 F1: o relatorio e sempre um caminho POSIX relativo, nunca uma busca ou um caminho da maquina. */
261
+ function caminhoJUnit(chave, v, erros) {
262
+ if (typeof v !== 'string' || !v.trim()) {
263
+ erros.push(`${chave} precisa ser um caminho relativo, em texto`);
264
+ return undefined;
265
+ }
266
+ const bruto = v.trim();
267
+ const normal = path.posix.normalize(bruto);
268
+ if (bruto.includes('\\') || bruto.includes('\0') || path.posix.isAbsolute(bruto) || path.win32.isAbsolute(bruto)
269
+ || normal === '.' || normal === '..' || normal.startsWith('../')) {
270
+ erros.push(`${chave}: ${exibir(v)} precisa ser um caminho relativo dentro da worktree`);
271
+ return undefined;
272
+ }
273
+ return normal;
274
+ }
275
+ /** RM-081 F3: o runner recebe a lista somente no marcador fechado, uma vez. */
276
+ function filtroDoComando(chave, v, erros) {
277
+ if (v === undefined || v === null)
278
+ return undefined;
279
+ if (typeof v !== 'string' || !v.trim()) {
280
+ erros.push(`${chave} precisa ser um comando em texto com o marcador {testes}`);
281
+ return undefined;
282
+ }
283
+ const filtro = v.trim();
284
+ const marcadores = filtro.match(/\{testes\}/g)?.length ?? 0;
285
+ if (marcadores !== 1) {
286
+ erros.push(`${chave} precisa ter o marcador {testes} exatamente uma vez`);
287
+ return undefined;
288
+ }
289
+ return filtro;
290
+ }
291
+ /** RM-081 F5: caminho ou glob POSIX relativo, sem sair da worktree nem alcançar estado do nucleo. */
292
+ function caminhoDaSelecao(chave, v, erros) {
293
+ if (typeof v !== 'string' || !v.trim()) {
294
+ erros.push(`${chave} precisa ser um caminho ou glob relativo, em texto`);
295
+ return undefined;
296
+ }
297
+ const bruto = v.trim().replace(/\/+$/, '');
298
+ const normal = path.posix.normalize(bruto);
299
+ if (!bruto || bruto.includes('\\') || bruto.includes('\0') || path.posix.isAbsolute(bruto)
300
+ || path.win32.isAbsolute(bruto) || normal === '.' || normal === '..' || normal.startsWith('../')
301
+ || /^(\.orkastery|\.git)(\/|$)/.test(normal)) {
302
+ erros.push(`${chave}: ${exibir(v)} precisa ficar dentro da worktree, fora de .orkastery e .git`);
303
+ return undefined;
304
+ }
305
+ return normal;
306
+ }
307
+ /** RM-081 F5: o manifesto declara o mecanismo e a superficie que o runner sabe relacionar. */
308
+ function selecaoDoComando(chave, v, erros) {
309
+ if (v === undefined || v === null)
310
+ return undefined;
311
+ if (typeof v !== 'object' || Array.isArray(v)) {
312
+ erros.push(`${chave} precisa ser um mapa com comando, mapeia e configuracao opcional`);
313
+ return undefined;
314
+ }
315
+ for (const nome of Object.keys(v)) {
316
+ if (!['comando', 'mapeia', 'configuracao'].includes(nome)) {
317
+ erros.push(`${chave}.${nome} desconhecida; use somente comando, mapeia e configuracao`);
318
+ }
319
+ }
320
+ const comando = typeof v.comando === 'string' ? v.comando.trim() : '';
321
+ const bases = comando.match(/\{base\}/g)?.length ?? 0;
322
+ const arquivos = comando.match(/\{arquivos\}/g)?.length ?? 0;
323
+ if (!comando || bases + arquivos !== 1) {
324
+ erros.push(`${chave}.comando precisa ter exatamente um marcador {base} ou {arquivos}`);
325
+ }
326
+ const lista = (nome) => {
327
+ const valor = v[nome];
328
+ if (valor === undefined || valor === null)
329
+ return [];
330
+ const itens = Array.isArray(valor) ? valor : [valor];
331
+ return itens.flatMap((item, indice) => {
332
+ const lido = caminhoDaSelecao(`${chave}.${nome}[${indice}]`, item, erros);
333
+ return lido ? [lido] : [];
334
+ });
335
+ };
336
+ const mapeia = lista('mapeia');
337
+ const configuracao = lista('configuracao');
338
+ return comando && bases + arquivos === 1
339
+ ? { comando, mapeia, ...(configuracao.length ? { configuracao } : {}) }
340
+ : undefined;
341
+ }
342
+ /** RM-081 F2: uma unica estrategia, fechada e validada antes de qualquer teste rodar. */
343
+ function relogioDoComando(chave, v, erros) {
344
+ if (v === undefined || v === null)
345
+ return undefined;
346
+ if (typeof v !== 'object' || Array.isArray(v)) {
347
+ erros.push(`${chave} precisa ser um mapa com exatamente uma de nome, marcador ou tag`);
348
+ return undefined;
349
+ }
350
+ const conhecidas = ['nome', 'marcador', 'tag'];
351
+ for (const nome of Object.keys(v)) {
352
+ if (!conhecidas.includes(nome))
353
+ erros.push(`${chave}.${nome} desconhecida; use somente nome, marcador ou tag`);
354
+ }
355
+ const presentes = conhecidas.filter((nome) => v[nome] !== undefined && v[nome] !== null);
356
+ if (presentes.length !== 1) {
357
+ erros.push(`${chave} precisa declarar exatamente uma de nome, marcador ou tag`);
358
+ return undefined;
359
+ }
360
+ const estrategia = presentes[0];
361
+ if (estrategia === 'nome') {
362
+ const valores = Array.isArray(v.nome) ? v.nome : [v.nome];
363
+ const padroes = valores.flatMap((item) => typeof item === 'string' && item.trim() ? [item.trim()] : []);
364
+ if (padroes.length !== valores.length || padroes.length === 0 || padroes.length > 20) {
365
+ erros.push(`${chave}.nome precisa ter de 1 a 20 expressoes regulares em texto`);
366
+ return undefined;
367
+ }
368
+ for (const padrao of padroes) {
369
+ try {
370
+ void new RegExp(padrao);
371
+ }
372
+ catch {
373
+ erros.push(`${chave}.nome contem expressao regular invalida: ${exibir(padrao)}`);
374
+ return undefined;
375
+ }
376
+ }
377
+ return { nome: padroes };
378
+ }
379
+ const valor = v[estrategia];
380
+ if (typeof valor !== 'string' || !valor.trim()) {
381
+ erros.push(`${chave}.${estrategia} precisa ser texto nao vazio`);
382
+ return undefined;
383
+ }
384
+ return estrategia === 'marcador' ? { marcador: valor.trim() } : { tag: valor.trim() };
385
+ }
386
+ /** RM-081 F4: recursos da lane sao nomes locais a rodada, unicos e de uma linha. */
387
+ function recursosDoComando(chave, v, erros) {
388
+ if (v === undefined)
389
+ return undefined;
390
+ if (!Array.isArray(v) || v.length === 0) {
391
+ erros.push(`${chave} precisa ser uma lista nao vazia de textos`);
392
+ return undefined;
393
+ }
394
+ const lidos = [];
395
+ let valido = true;
396
+ for (const recurso of v) {
397
+ if (typeof recurso !== 'string' || !recurso.trim() || recurso.trim().length > 80 || /[\r\n]/.test(recurso)) {
398
+ erros.push(`${chave}: ${exibir(recurso)} precisa ser texto de uma linha, de 1 a 80 caracteres`);
399
+ valido = false;
400
+ }
401
+ else if (lidos.includes(recurso.trim())) {
402
+ erros.push(`${chave}: ${exibir(recurso.trim())} esta repetido`);
403
+ valido = false;
404
+ }
405
+ else
406
+ lidos.push(recurso.trim());
407
+ }
408
+ return valido ? lidos : undefined;
409
+ }
410
+ /** Texto continua sendo o contrato legado; o objeto exige comando e aceita os opt-ins da superficie. */
411
+ function lerComando(chave, v, erros, aceitaSelecao = false, aceitaRecursos = false) {
412
+ if (v === undefined || v === null)
413
+ return {};
414
+ if (typeof v === 'string')
415
+ return { comando: v };
416
+ if (typeof v !== 'object' || Array.isArray(v)) {
417
+ erros.push(`${chave} precisa ser o comando em texto ou um mapa com comando (recebido ${exibir(v)})`);
418
+ return {};
419
+ }
420
+ const conhecidas = ['comando', 'junit', 'filtro', 'relogio', ...(aceitaSelecao ? ['selecao'] : []),
421
+ ...(aceitaRecursos ? ['recursos'] : [])];
422
+ for (const nome of Object.keys(v)) {
423
+ if (!conhecidas.includes(nome)) {
424
+ erros.push(`${chave}.${nome} desconhecida; use somente ${conhecidas.join(', ')}`);
425
+ }
426
+ }
427
+ const comando = typeof v.comando === 'string' && v.comando.trim() ? v.comando.trim() : undefined;
428
+ if (!comando)
429
+ erros.push(`${chave}.comando precisa ser texto nao vazio`);
430
+ const junit = v.junit === undefined ? undefined : caminhoJUnit(`${chave}.junit`, v.junit, erros);
431
+ const dispensaJUnit = aceitaRecursos && v.recursos !== undefined && v.recursos !== null;
432
+ if (v.junit === undefined) {
433
+ const dependencias = ['filtro', 'relogio']
434
+ .filter((dependencia) => v[dependencia] !== undefined && v[dependencia] !== null);
435
+ for (const dependencia of dependencias) {
436
+ if (v[dependencia] !== undefined && v[dependencia] !== null) {
437
+ erros.push(`${chave}.junit precisa ser declarado porque ${chave}.${dependencia} depende do relatorio JUnit`);
438
+ }
439
+ }
440
+ if (!dependencias.length && !dispensaJUnit) {
441
+ erros.push(`${chave}.junit precisa ser declarado no objeto de comando`);
442
+ }
443
+ }
444
+ const filtro = filtroDoComando(`${chave}.filtro`, v.filtro, erros);
445
+ const relogio = relogioDoComando(`${chave}.relogio`, v.relogio, erros);
446
+ const selecao = aceitaSelecao ? selecaoDoComando(`${chave}.selecao`, v.selecao, erros) : undefined;
447
+ const recursos = aceitaRecursos ? recursosDoComando(`${chave}.recursos`, v.recursos, erros) : undefined;
448
+ return { ...(comando ? { comando } : {}), ...(junit ? { junit } : {}), ...(filtro ? { filtro } : {}),
449
+ ...(relogio ? { relogio } : {}), ...(selecao ? { selecao } : {}), ...(recursos ? { recursos } : {}) };
450
+ }
260
451
  /**
261
452
  * RM-072 (D3): o bloco `check.agile`. Cada chave liga uma peca do Agile; tipo errado e erro do manifesto, porque uma
262
453
  * peca mal escrita que sumisse em silencio mudaria o que o CHECK roda. `produto` so aceita caminho relativo dentro da
@@ -302,16 +493,42 @@ function lerAgile(v, erros, avisos) {
302
493
  }
303
494
  else {
304
495
  const lanes = {};
305
- for (const [lane, comando] of Object.entries(v.test)) {
496
+ const junit = {};
497
+ const filtro = {};
498
+ const relogio = {};
499
+ const selecao = {};
500
+ const recursos = {};
501
+ for (const [lane, valor] of Object.entries(v.test)) {
306
502
  if (!NOME_DE_LANE.test(lane))
307
503
  erros.push(`check.agile.test.${lane}: nome de lane aceita [a-z0-9-], ate 32 caracteres`);
308
- else if (typeof comando !== 'string' || !comando.trim())
309
- erros.push(`check.agile.test.${lane} precisa ser o comando, em texto`);
310
- else
311
- lanes[lane] = comando.trim();
504
+ else {
505
+ const lido = lerComando(`check.agile.test.${lane}`, valor, erros, true, true);
506
+ if (lido.comando)
507
+ lanes[lane] = lido.comando.trim();
508
+ if (lido.junit)
509
+ junit[lane] = lido.junit;
510
+ if (lido.filtro)
511
+ filtro[lane] = lido.filtro;
512
+ if (lido.relogio)
513
+ relogio[lane] = lido.relogio;
514
+ if (lido.selecao)
515
+ selecao[lane] = lido.selecao;
516
+ if (lido.recursos)
517
+ recursos[lane] = lido.recursos;
518
+ }
312
519
  }
313
520
  if (Object.keys(lanes).length)
314
521
  agile.test = lanes;
522
+ if (Object.keys(junit).length)
523
+ agile.junit = junit;
524
+ if (Object.keys(filtro).length)
525
+ agile.filtro = filtro;
526
+ if (Object.keys(relogio).length)
527
+ agile.relogio = relogio;
528
+ if (Object.keys(selecao).length)
529
+ agile.selecao = selecao;
530
+ if (Object.keys(recursos).length)
531
+ agile.recursos = recursos;
315
532
  }
316
533
  }
317
534
  if (v.apos_a_varredura !== undefined && v.apos_a_varredura !== null) {
@@ -666,6 +883,14 @@ function carregarManifesto(dirInicial = diretorioDoProjeto()) {
666
883
  '(esperado nome de branch do git, sem "-" no começo e sem "..")');
667
884
  }
668
885
  const verify = mapa(dados.verify);
886
+ const comandosVerify = Object.fromEntries(['build', 'test', 'typecheck', 'security_scan']
887
+ .map((nome) => [nome, lerComando(`verify.${nome}`, verify[nome], erros)]));
888
+ const junitVerify = Object.fromEntries(Object.entries(comandosVerify)
889
+ .flatMap(([nome, lido]) => lido.junit ? [[nome, lido.junit]] : []));
890
+ const filtroVerify = Object.fromEntries(Object.entries(comandosVerify)
891
+ .flatMap(([nome, lido]) => lido.filtro ? [[nome, lido.filtro]] : []));
892
+ const relogioVerify = Object.fromEntries(Object.entries(comandosVerify)
893
+ .flatMap(([nome, lido]) => lido.relogio ? [[nome, lido.relogio]] : []));
669
894
  const ci = mapa(dados.ci);
670
895
  const concurrency = mapa(dados.concurrency);
671
896
  const handoff = mapa(dados.handoff);
@@ -879,15 +1104,14 @@ function carregarManifesto(dirInicial = diretorioDoProjeto()) {
879
1104
  ...dependenciasDaWorktree(worktree.dependencias, erros),
880
1105
  },
881
1106
  verify: {
882
- build: verify.build === null || verify.build === undefined ? undefined : texto(verify.build, ''),
883
- test: verify.test === null || verify.test === undefined ? undefined : texto(verify.test, ''),
1107
+ build: comandosVerify.build.comando,
1108
+ test: comandosVerify.test.comando,
884
1109
  ...prazosDoVerify(verify, erros),
885
- typecheck: verify.typecheck === null || verify.typecheck === undefined
886
- ? undefined
887
- : texto(verify.typecheck, ''),
888
- security_scan: verify.security_scan === null || verify.security_scan === undefined
889
- ? undefined
890
- : texto(verify.security_scan, ''),
1110
+ typecheck: comandosVerify.typecheck.comando,
1111
+ security_scan: comandosVerify.security_scan.comando,
1112
+ ...(Object.keys(junitVerify).length ? { junit: junitVerify } : {}),
1113
+ ...(Object.keys(filtroVerify).length ? { filtro: filtroVerify } : {}),
1114
+ ...(Object.keys(relogioVerify).length ? { relogio: relogioVerify } : {}),
891
1115
  // I-54 (RM-037, D10): o preparo unico da rodada de verify, quando declarado.
892
1116
  ...(typeof verify.preparo === 'string' && verify.preparo.trim() ? { preparo: verify.preparo.trim() } : {}),
893
1117
  },
package/dist/retry.js CHANGED
@@ -117,6 +117,8 @@ const postura_local_1 = require("./postura-local");
117
117
  const locale_1 = require("./locale");
118
118
  const locale_retry_1 = require("./locale-retry");
119
119
  const retomada_checkpoint_1 = require("./retomada-checkpoint");
120
+ const codex_task_complete_1 = require("./codex-task-complete");
121
+ const session_events_1 = require("./session-events");
120
122
  /** RM-061 (F2b, D3): `porque` e `correcao` de cada motivo tem a fonte no catalogo pt-BR; o `--json` nao muda. */
121
123
  const TEXTOS_DA_POLITICA = locale_retry_1.RETRY_PT_BR.politica;
122
124
  /**
@@ -385,6 +387,31 @@ function blocoPausa(thread, fase) {
385
387
  function estadoTerminalDeRetry(estado) {
386
388
  return ['completed', 'failed', 'stopped', 'cancelled', 'interrupted', 'terminated'].includes(estado.trim().toLowerCase());
387
389
  }
390
+ /** Prova estreita: somente o rollout fixado da sessão Codex que originou o gate da fase. */
391
+ function faseJaTerminouPorTaskComplete(raiz, thread, fase, evento) {
392
+ const candidata = [...thread.sessoes].reverse().find(s => s.runtime === 'codex' && s.fase === fase &&
393
+ typeof s.despachadaEm === 'string' && (typeof evento?.sessionId !== 'string' || s.sessionId === evento.sessionId) &&
394
+ (typeof evento?.despachoEm !== 'string' || s.despachadaEm === evento.despachoEm));
395
+ if (!candidata)
396
+ return false;
397
+ try {
398
+ const prova = (0, codex_task_complete_1.taskCompleteFixadoDaSessao)(raiz, thread, (0, session_events_1.exigirSessaoDespachada)(candidata, candidata.sessionId), (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, thread.id)));
399
+ // task_complete encerra o turno, mas pode carregar uma falha estruturada de runtime.
400
+ // Só o resultado que concluiu a fase impede nova tentativa; cota/auth/modelo mantêm
401
+ // a política tipada de recuperação já existente.
402
+ return prova?.terminal.classificacao === 'fase_concluida';
403
+ }
404
+ catch {
405
+ return false;
406
+ }
407
+ }
408
+ function planoProtegidoPorTaskComplete(thread, fase, limite, effort) {
409
+ return { thread: thread.id, modo: thread.modo, fase, motivo: null,
410
+ detalhe: 'rollout autenticado da sessão despachada já contém task_complete', politica: null,
411
+ acao: 'sem-retry', tentativas: 0, limite, effort, effortAnterior: effort, automatica: false,
412
+ bloqueio: 'gate.sem-reprovacao',
413
+ razao: 'rollout autenticado já contém task_complete; a fase terminou e não será redespachada' };
414
+ }
388
415
  /**
389
416
  * Calcula o plano de retry SEM executar nada.
390
417
  *
@@ -417,6 +444,8 @@ function planejarRetry(carregado, threadId, opcoes = {}) {
417
444
  };
418
445
  }
419
446
  const fase = opcoes.fase ?? (evento ? faseDoEvento(evento, thread) : thread.faseAtual);
447
+ if (faseJaTerminouPorTaskComplete(raiz, thread, fase, evento))
448
+ return planoProtegidoPorTaskComplete(thread, fase, limite, effortBase);
420
449
  const politica = politicaDoMotivo(motivo);
421
450
  const tentativas = tentativasFeitas(raiz, threadId, fase, motivo);
422
451
  const detalhe = evento && typeof evento.detalhe === 'string' ? evento.detalhe : '';
@@ -1429,6 +1458,12 @@ function executarRetry(carregado, threadId, opcoes = {}) {
1429
1458
  }
1430
1459
  }
1431
1460
  const thread = (0, thread_1.lerThread)(raiz, threadId);
1461
+ // D4: reconfirma imediatamente antes do primeiro efeito do retry; o plano anterior pode ter
1462
+ // sido calculado enquanto o runtime ainda anexava o terminal ao rollout.
1463
+ if (plano.motivo !== null && plano.fase &&
1464
+ faseJaTerminouPorTaskComplete(raiz, thread, plano.fase, ultimaReprovacao(raiz, threadId))) {
1465
+ plano = planoProtegidoPorTaskComplete(thread, plano.fase, plano.limite, plano.effortAnterior);
1466
+ }
1432
1467
  const dir = (0, thread_1.dirThread)(raiz, threadId);
1433
1468
  const vazio = {
1434
1469
  plano,
@@ -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';