@orkastery/cli 0.5.0 → 0.5.3
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 +11 -2
- package/adapters/claude-code/agents/ork-master.md +2 -0
- package/adapters/claude-code/commands/master.md +8 -0
- package/adapters/claude-code/commands/onboarding.md +3 -2
- package/adapters/claude-code/commands/ork.md +9 -0
- package/adapters/codex/skills/ork/SKILL.md +18 -3
- package/adapters/hermes/README.md +1 -0
- package/adapters/hermes/bin/ork-network-status.sh +10 -0
- package/adapters/hermes/hermes.plugin.json +1 -0
- package/adapters/hermes/hitl-ingress/__init__.py +2 -2
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +6 -5
- package/adapters/openclaw/README.md +35 -4
- package/adapters/openclaw/dist/hitl-ingress.js +2 -2
- package/adapters/openclaw/dist/index.js +47 -12
- package/adapters/openclaw/openclaw.plugin.json +5 -1
- package/adapters/openclaw/package.json +2 -2
- package/adapters/openclaw/src/hitl-ingress.ts +2 -2
- package/adapters/openclaw/src/index.ts +50 -12
- package/assets/orkmind_bridge.py +117 -9
- package/dist/adapters/claude-bg.js +59 -11
- package/dist/adapters/codex.js +4 -3
- package/dist/agents-md.js +1 -0
- package/dist/auditrun.js +6 -2
- package/dist/board.js +16 -14
- package/dist/branch-de-estado.js +66 -10
- package/dist/busca-semantica.js +52 -12
- package/dist/canarios-rm057.js +149 -0
- package/dist/canarios.js +2 -0
- package/dist/ci.js +8 -1
- package/dist/claim-lint.js +3 -1
- package/dist/claims.js +39 -1
- package/dist/company-brain-context.js +158 -22
- package/dist/docs.js +110 -3
- package/dist/doctor-pulse-cron.js +194 -0
- package/dist/doctor-rede.js +149 -0
- package/dist/doctor.js +294 -37
- package/dist/entrega-pr.js +27 -8
- package/dist/esgotamentos.js +181 -0
- package/dist/experiencia.js +12 -2
- package/dist/fabrica-estado.js +35 -9
- package/dist/fabrica-publicar.js +6 -0
- package/dist/fechamento.js +20 -66
- package/dist/fix.js +3 -3
- package/dist/forja.js +31 -7
- package/dist/gates.js +5 -1
- package/dist/hitl-contract.js +59 -4
- package/dist/hitl-curto.js +11 -6
- package/dist/hitl-gates.js +5 -0
- package/dist/hitl-lote.js +5 -2
- package/dist/hitl-resumo.js +33 -1
- package/dist/hitl-tempo-parado.js +201 -0
- package/dist/hitl-texto-livre.js +5 -5
- package/dist/horario.js +27 -0
- package/dist/hosts.js +59 -13
- package/dist/impedimento.js +205 -0
- package/dist/index.js +433 -88
- package/dist/indice-vetorial.js +87 -10
- package/dist/indice.js +3 -1
- package/dist/init.js +28 -5
- package/dist/integracoes-locais.js +4 -0
- package/dist/intelligence-graph-cli.js +267 -30
- package/dist/intelligence-graph-contexto.js +215 -0
- package/dist/intelligence-graph-contract.js +71 -12
- package/dist/intelligence-graph-extract-md.js +41 -23
- package/dist/intelligence-graph-extract-ts.js +181 -11
- package/dist/intelligence-graph-extract.js +186 -9
- package/dist/intelligence-graph-index.js +205 -24
- package/dist/intelligence-graph-parsers.js +6 -4
- package/dist/intelligence-graph-repo.js +10 -0
- package/dist/leases.js +680 -87
- package/dist/ledger-stats.js +15 -1
- package/dist/ledger.js +12 -0
- package/dist/licoes.js +6 -4
- package/dist/liveness.js +2 -1
- package/dist/maestro-runtime.js +7 -1
- package/dist/manifest.js +44 -1
- package/dist/master-nota.js +14 -5
- package/dist/master.js +129 -12
- package/dist/mcp-artifacts.js +36 -3
- package/dist/mcp-git.js +80 -15
- package/dist/mcp-grafo-worker.js +93 -0
- package/dist/mcp-grafo.js +279 -0
- package/dist/mcp-install.js +2 -1
- package/dist/mcp-server.js +23 -1
- package/dist/mcp-ship.js +2 -0
- package/dist/mcp-verify.js +3 -1
- package/dist/memoria.js +48 -6
- package/dist/network-roadmap.js +195 -20
- package/dist/ocupacao.js +17 -5
- package/dist/onboarding.js +9 -4
- package/dist/orkmind.js +90 -5
- package/dist/orquestracao.js +14 -11
- package/dist/parado-no-condutor.js +1089 -0
- package/dist/phase.js +68 -15
- package/dist/policies.js +79 -3
- package/dist/portfolio.js +4 -1
- package/dist/projeto-alvo.js +5 -1
- package/dist/prompts.js +36 -1
- package/dist/prova-ativacao.js +414 -0
- package/dist/pulse-delivery.js +60 -7
- package/dist/pulse.js +99 -3
- package/dist/ratelimit.js +20 -4
- package/dist/recall.js +3 -1
- package/dist/rede-adesao.js +399 -0
- package/dist/rede-forja.js +289 -0
- package/dist/rede-projetos.js +239 -0
- package/dist/rede-status.js +336 -0
- package/dist/rede.js +980 -0
- package/dist/retry.js +69 -2
- package/dist/roadmap-status.js +162 -7
- package/dist/runtime-context.js +5 -1
- package/dist/runtime-profiles.js +22 -1
- package/dist/saida-segura.js +58 -0
- package/dist/session-watcher-claude.js +113 -23
- package/dist/sessoes-contas.js +187 -0
- package/dist/sessoes-inventario.js +6 -12
- package/dist/sessoes.js +87 -3
- package/dist/ship.js +110 -20
- package/dist/thread.js +212 -14
- package/dist/util.js +71 -4
- package/dist/verify.js +2 -2
- package/dist/worktree.js +34 -8
- package/eval/casos/onboarding.json +1 -1
- package/eval/fixtures/fx-pedido-colado/caso.json +20 -0
- package/package.json +7 -3
- package/references/definition-of-done.md +1 -1
- package/skills/README.md +1 -1
- package/skills/core/onboarding/SKILL.md +3 -2
- package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
- package/skills/phases/master-metrics/SKILL.md +8 -3
- package/skills/phases/ship-release/SKILL.md +6 -3
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CONTEXT_SCHEMA = void 0;
|
|
3
|
+
exports.SERVER_CONTEXT_SCHEMA = exports.CONTEXT_SCHEMA = void 0;
|
|
4
4
|
exports.buildContext = buildContext;
|
|
5
5
|
const portfolio_1 = require("./portfolio");
|
|
6
6
|
const company_brain_client_1 = require("./company-brain-client");
|
|
7
7
|
const company_brain_contract_1 = require("./company-brain-contract");
|
|
8
8
|
const company_brain_source_1 = require("./company-brain-source");
|
|
9
9
|
exports.CONTEXT_SCHEMA = 'ork.brain-context/v1';
|
|
10
|
+
exports.SERVER_CONTEXT_SCHEMA = 'orkmind.company-brain-context/v1';
|
|
11
|
+
const SEM_MODO_CONTEXT = 'brain.selection.context-unsupported';
|
|
12
|
+
const SERVIDOR_INVALIDO = 'brain.context.server-invalid';
|
|
10
13
|
const ID = /^(prod|proj|init)-[a-z0-9][a-z0-9-]{2,47}$/;
|
|
11
14
|
const ORDEM = { prod: 0, proj: 1, init: 2 };
|
|
12
15
|
const LIMITE = 1000;
|
|
@@ -27,25 +30,122 @@ function citacaoDe(source) {
|
|
|
27
30
|
return null;
|
|
28
31
|
return { instance, source_ref, source_hash, source_version, location };
|
|
29
32
|
}
|
|
33
|
+
/** O servidor exige também `authority`: o pacote dele só traz item com a citação inteira; a consulta exige o mesmo. */
|
|
34
|
+
const citadoPeloServidor = (source) => citacaoDe(source) !== null && typeof source.authority === 'string' && source.authority !== '';
|
|
30
35
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
36
|
+
* Confere um pacote `orkmind.company-brain-context/v1` contra o lote pedido: schema, tenant, pedido,
|
|
37
|
+
* digest recalculado, citação inteira, ordem, fecho de pais e destino de cada id pedido.
|
|
33
38
|
*/
|
|
34
|
-
function
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
function conferirPacoteDoServidor(r, tenant, lote) {
|
|
40
|
+
const ctx = r.context;
|
|
41
|
+
if (!['ok', 'empty'].includes(r.state) || !ctx || typeof ctx !== 'object' || Array.isArray(ctx))
|
|
42
|
+
return false;
|
|
43
|
+
if (Object.keys(ctx).sort().join(',') !== 'digest,gaps,items,requested,schema,tenant_id')
|
|
44
|
+
return false;
|
|
45
|
+
const { digest: recebido, ...corpo } = ctx;
|
|
46
|
+
if (ctx.schema !== exports.SERVER_CONTEXT_SCHEMA || ctx.tenant_id !== tenant || (0, company_brain_contract_1.canonical)(ctx.requested) !== (0, company_brain_contract_1.canonical)(lote) ||
|
|
47
|
+
!Array.isArray(ctx.items) || !Array.isArray(ctx.gaps) || typeof recebido !== 'string' || (0, company_brain_contract_1.digest)(corpo) !== recebido)
|
|
48
|
+
return false;
|
|
49
|
+
if (r.state !== (ctx.items.length ? 'ok' : 'empty'))
|
|
50
|
+
return false;
|
|
51
|
+
const pedidos = new Set(lote), vistos = new Map(), codigos = new Map();
|
|
52
|
+
for (const g of ctx.gaps) {
|
|
53
|
+
if (!g || typeof g.id !== 'string' || typeof g.code !== 'string' || !ID.test(g.id))
|
|
54
|
+
return false;
|
|
55
|
+
if (!codigos.has(g.id))
|
|
56
|
+
codigos.set(g.id, new Set());
|
|
57
|
+
codigos.get(g.id).add(g.code);
|
|
58
|
+
}
|
|
59
|
+
for (const item of ctx.items) {
|
|
60
|
+
if (!item || typeof item.id !== 'string' || !ID.test(item.id) || vistos.has(item.id))
|
|
61
|
+
return false;
|
|
62
|
+
if (item.state === 'withheld') {
|
|
63
|
+
if (Object.keys(item).length !== 2)
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
else if (item.state !== 'ok' || !item.entity || typeof item.entity !== 'object' || item.entity.kind !== kindDe(item.id) ||
|
|
67
|
+
!Number.isInteger(item.entity.version) || !citadoPeloServidor(item.entity.source))
|
|
68
|
+
return false;
|
|
69
|
+
vistos.set(item.id, item);
|
|
70
|
+
}
|
|
71
|
+
const ordem = ctx.items.map((i) => i.id);
|
|
72
|
+
const ordenado = [...ordem].sort((a, b) => ORDEM[kindDe(a)] - ORDEM[kindDe(b)] || comparar(a, b));
|
|
73
|
+
if ((0, company_brain_contract_1.canonical)(ordem) !== (0, company_brain_contract_1.canonical)(ordenado))
|
|
74
|
+
return false;
|
|
75
|
+
// Fecho: todo item fora do pedido é pai, um kind acima, de um item citado do pacote.
|
|
76
|
+
const paisCitados = new Set();
|
|
77
|
+
for (const item of ctx.items)
|
|
78
|
+
if (item.state === 'ok') {
|
|
79
|
+
const parent = item.entity.parent_id;
|
|
80
|
+
if (typeof parent === 'string' && ID.test(parent) && ORDEM[kindDe(parent)] < ORDEM[kindDe(item.id)])
|
|
81
|
+
paisCitados.add(parent);
|
|
82
|
+
}
|
|
83
|
+
for (const id of vistos.keys())
|
|
84
|
+
if (!pedidos.has(id) && !paisCitados.has(id))
|
|
85
|
+
return false;
|
|
86
|
+
// Destino: todo id pedido sai como item ou como lacuna de desconhecido ou de citação incompleta.
|
|
87
|
+
for (const id of lote) {
|
|
88
|
+
const c = codigos.get(id);
|
|
89
|
+
if (!vistos.has(id) && !(c?.has('entity.unknown') || c?.has('citation.incomplete')))
|
|
90
|
+
return false;
|
|
91
|
+
if (vistos.has(id) && (c?.has('entity.unknown') || c?.has('citation.incomplete')))
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* B4.2: resolve o escopo pelo modo `context` do servidor. Devolve null quando o OrkMind não tem o modo
|
|
98
|
+
* (o chamador usa a consulta), ou a resposta que encerra o pacote.
|
|
99
|
+
*/
|
|
100
|
+
function resolverPeloServidor(transport, tenant, pedido, fonte, pai) {
|
|
101
|
+
const escopo = new Set(pedido), brain = new Map(), retidos = new Set(), semCitacao = new Set();
|
|
102
|
+
let primeiro = true;
|
|
103
|
+
for (let pendentes = pedido; pendentes.length;) {
|
|
104
|
+
const resolvidosNaVolta = [];
|
|
105
|
+
for (let inicio = 0; inicio < pendentes.length; inicio += LIMITE) {
|
|
106
|
+
const lote = pendentes.slice(inicio, inicio + LIMITE);
|
|
107
|
+
const r = transport({ schema: company_brain_client_1.BRAIN_API, operation: 'query', payload: (0, company_brain_contract_1.validateContract)({ schema: 'orkmind.company-brain-selection/v1',
|
|
108
|
+
tenant_id: tenant, facets: { ids: lote, kinds: [], workspace_ids: [], source_instances: [] }, mode: 'context',
|
|
109
|
+
limit: lote.length, offset: 0 }) });
|
|
110
|
+
if (r.state === 'unavailable' && r.error === SEM_MODO_CONTEXT)
|
|
111
|
+
return primeiro ? null : { state: 'conflict', error: SERVIDOR_INVALIDO };
|
|
112
|
+
primeiro = false;
|
|
113
|
+
if (falha(r.state))
|
|
114
|
+
return r;
|
|
115
|
+
if (!conferirPacoteDoServidor(r, tenant, lote))
|
|
116
|
+
return { state: 'conflict', error: SERVIDOR_INVALIDO };
|
|
117
|
+
const ctx = r.context;
|
|
118
|
+
for (const g of ctx.gaps)
|
|
119
|
+
if (g.code === 'citation.incomplete')
|
|
120
|
+
semCitacao.add(g.id);
|
|
121
|
+
for (const item of ctx.items) {
|
|
122
|
+
// Pai que outro lote ou volta já resolveu vale uma vez só: a primeira resposta fica.
|
|
123
|
+
if (escopo.has(item.id) && !lote.includes(item.id))
|
|
124
|
+
continue;
|
|
125
|
+
escopo.add(item.id);
|
|
126
|
+
resolvidosNaVolta.push(item.id);
|
|
127
|
+
if (item.state === 'withheld')
|
|
128
|
+
retidos.add(item.id);
|
|
129
|
+
else
|
|
130
|
+
brain.set(item.id, { ...item.entity, id: item.id });
|
|
131
|
+
}
|
|
132
|
+
resolvidosNaVolta.push(...lote);
|
|
133
|
+
}
|
|
134
|
+
// O servidor fecha os pais que o Brain conhece; os que só a fonte local conhece vão na próxima volta.
|
|
135
|
+
const novos = new Set();
|
|
136
|
+
for (const id of resolvidosNaVolta)
|
|
137
|
+
if (!retidos.has(id))
|
|
138
|
+
for (const parent of [pai(id, fonte.get(id)?.parent_id), pai(id, brain.get(id)?.parent_id)])
|
|
139
|
+
if (parent && !escopo.has(parent))
|
|
140
|
+
novos.add(parent);
|
|
141
|
+
novos.forEach(id => escopo.add(id));
|
|
142
|
+
pendentes = [...novos].sort(comparar);
|
|
143
|
+
}
|
|
144
|
+
return { brain, retidos, semCitacao, escopo };
|
|
145
|
+
}
|
|
146
|
+
/** B4.1: o caminho anterior ao modo `context`, por `query` e `get`, para um OrkMind sem o modo. */
|
|
147
|
+
function resolverPelaConsulta(transport, tenant, pedido, fonte, pai) {
|
|
46
148
|
const escopo = new Set(pedido), brain = new Map(), retidos = new Set();
|
|
47
|
-
// O pai só entra por um filho visível e tem kind acima dele: init, proj e prod fecham em três voltas.
|
|
48
|
-
const pai = (id, parent) => typeof parent === 'string' && ID.test(parent) && ORDEM[kindDe(parent)] < ORDEM[kindDe(id)] ? parent : null;
|
|
49
149
|
let gets = 0;
|
|
50
150
|
for (let pendentes = pedido; pendentes.length;) {
|
|
51
151
|
for (let inicio = 0; inicio < pendentes.length; inicio += LIMITE) {
|
|
@@ -54,7 +154,7 @@ function buildContext(c, ids, transport, thread) {
|
|
|
54
154
|
tenant_id: tenant, facets: { ids: lote, kinds: [], workspace_ids: [], source_instances: [] }, mode: 'selection',
|
|
55
155
|
limit: lote.length, offset: 0 }) });
|
|
56
156
|
if (falha(r.state))
|
|
57
|
-
return
|
|
157
|
+
return r;
|
|
58
158
|
const itens = Array.isArray(r.items) ? r.items : [];
|
|
59
159
|
for (const item of itens)
|
|
60
160
|
if (item?.state === 'ok' && doLote.has(item.entity?.id))
|
|
@@ -67,10 +167,10 @@ function buildContext(c, ids, transport, thread) {
|
|
|
67
167
|
else if (retidosNoLote)
|
|
68
168
|
for (const id of faltantes) {
|
|
69
169
|
if (++gets > LIMITE_DE_GET)
|
|
70
|
-
return
|
|
170
|
+
return { state: 'unavailable', error: 'brain.context.get-limit' };
|
|
71
171
|
const g = transport({ schema: company_brain_client_1.BRAIN_API, operation: 'get', payload: { tenant_id: tenant, id } });
|
|
72
172
|
if (falha(g.state))
|
|
73
|
-
return
|
|
173
|
+
return g;
|
|
74
174
|
if (g.state === 'withheld')
|
|
75
175
|
retidos.add(id);
|
|
76
176
|
else if (g.state === 'ok' && g.entity?.id === id)
|
|
@@ -87,6 +187,35 @@ function buildContext(c, ids, transport, thread) {
|
|
|
87
187
|
}
|
|
88
188
|
pendentes = novos.sort(comparar);
|
|
89
189
|
}
|
|
190
|
+
return { brain, retidos, semCitacao: new Set(), escopo };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Monta o pacote dos ids pedidos mais a cadeia de pais. A fonte canônica é o portfólio local;
|
|
194
|
+
* o Brain é projeção, e a diferença entre os dois sai como frescor e lacuna, nunca é corrigida aqui.
|
|
195
|
+
*/
|
|
196
|
+
function buildContext(c, ids, transport, thread) {
|
|
197
|
+
const tenant = c.manifesto.memory.tenant;
|
|
198
|
+
const pedido = [...new Set(ids)].sort(comparar);
|
|
199
|
+
if (!pedido.length)
|
|
200
|
+
throw Error('brain.context.ids-required');
|
|
201
|
+
if (pedido.some(id => !ID.test(id)))
|
|
202
|
+
throw Error('brain.context.id-invalid');
|
|
203
|
+
if (pedido.length > LIMITE)
|
|
204
|
+
throw Error('brain.context.too-many');
|
|
205
|
+
const fonte = new Map((0, company_brain_source_1.portfolioEntities)((0, portfolio_1.readPortfolio)(c.raiz), { tenant, instance: c.manifesto.project.name, thread, aclRef: 'ork-factory' }).map(e => [e.id, e]));
|
|
206
|
+
const base = { schema: exports.CONTEXT_SCHEMA, tenant, contrato: company_brain_contract_1.CONTRACT_HASH, thread, pedido };
|
|
207
|
+
// O pai só entra por um filho visível e tem kind acima dele: init, proj e prod fecham em três voltas.
|
|
208
|
+
const pai = (id, parent) => typeof parent === 'string' && ID.test(parent) && ORDEM[kindDe(parent)] < ORDEM[kindDe(id)] ? parent : null;
|
|
209
|
+
const doServidor = resolverPeloServidor(transport, tenant, pedido, fonte, pai);
|
|
210
|
+
const caminho = doServidor === null ? 'consulta' : 'servidor';
|
|
211
|
+
const encerrar = (r) => ({ ...base, state: r.state, ...(r.error ? { error: r.error } : {}), itens: [], lacunas: [],
|
|
212
|
+
digest: null, consultadoEm: new Date().toISOString(), caminho });
|
|
213
|
+
if (doServidor && 'state' in doServidor)
|
|
214
|
+
return encerrar(doServidor);
|
|
215
|
+
const resolvido = doServidor ?? resolverPelaConsulta(transport, tenant, pedido, fonte, pai);
|
|
216
|
+
if ('state' in resolvido)
|
|
217
|
+
return encerrar(resolvido);
|
|
218
|
+
const { escopo, brain, retidos, semCitacao } = resolvido;
|
|
90
219
|
const itens = [], lacunas = [];
|
|
91
220
|
const lacuna = (id, codigo) => lacunas.push({ id, codigo });
|
|
92
221
|
for (const id of escopo) {
|
|
@@ -95,12 +224,19 @@ function buildContext(c, ids, transport, thread) {
|
|
|
95
224
|
lacuna(id, 'brain.retido');
|
|
96
225
|
continue;
|
|
97
226
|
}
|
|
227
|
+
// Sem citação no Brain vale como no caminho da consulta: o Brain tem a entidade, mas ela não é citável.
|
|
228
|
+
if (semCitacao.has(id)) {
|
|
229
|
+
lacuna(id, 'citacao.incompleta');
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
98
232
|
const b = brain.get(id), f = fonte.get(id);
|
|
99
233
|
if (!b && !f) {
|
|
100
234
|
lacuna(id, 'entidade.desconhecida');
|
|
101
235
|
continue;
|
|
102
236
|
}
|
|
103
|
-
|
|
237
|
+
// Suspeitas da revisao de 03/10: a citacao exige `authority` nos dois caminhos, como o servidor (o
|
|
238
|
+
// schema do Source a exige); sem isso, um Brain fora do contrato dava pacote e digest diferentes por caminho.
|
|
239
|
+
const entidade = b ?? f, citacao = citadoPeloServidor(entidade.source) ? citacaoDe(entidade.source) : null;
|
|
104
240
|
if (!citacao) {
|
|
105
241
|
lacuna(id, 'citacao.incompleta');
|
|
106
242
|
continue;
|
|
@@ -128,5 +264,5 @@ function buildContext(c, ids, transport, thread) {
|
|
|
128
264
|
lacunas.sort((a, b) => comparar(a.id, b.id) || comparar(a.codigo, b.codigo));
|
|
129
265
|
// O digest cobre o que foi dito e de onde veio; o horário da consulta fica fora para o pacote ser reproduzível.
|
|
130
266
|
const corpo = { ...base, state: (itens.length ? 'ok' : 'empty'), itens, lacunas };
|
|
131
|
-
return { ...corpo, digest: (0, company_brain_contract_1.digest)(corpo), consultadoEm: new Date().toISOString() };
|
|
267
|
+
return { ...corpo, digest: (0, company_brain_contract_1.digest)(corpo), consultadoEm: new Date().toISOString(), caminho };
|
|
132
268
|
}
|
package/dist/docs.js
CHANGED
|
@@ -43,9 +43,11 @@ exports.comandosDaAjuda = comandosDaAjuda;
|
|
|
43
43
|
exports.verificarDocs = verificarDocs;
|
|
44
44
|
exports.renderizarRelance = renderizarRelance;
|
|
45
45
|
exports.renderizarEstado = renderizarEstado;
|
|
46
|
+
exports.docsPendentesDoMerge = docsPendentesDoMerge;
|
|
46
47
|
exports.textoDaVerificacao = textoDaVerificacao;
|
|
47
48
|
exports.escreverYaml = escreverYaml;
|
|
48
49
|
exports.mergeDaThread = mergeDaThread;
|
|
50
|
+
exports.mergesDaThreadNoGit = mergesDaThreadNoGit;
|
|
49
51
|
exports.escopoPadraoDoSync = escopoPadraoDoSync;
|
|
50
52
|
exports.sincronizarDocs = sincronizarDocs;
|
|
51
53
|
exports.tabelaDoRoadmap = tabelaDoRoadmap;
|
|
@@ -262,9 +264,12 @@ function verificarDocs(raiz, opcoes = {}) {
|
|
|
262
264
|
verificarFontes(raiz, d, comandos, achados);
|
|
263
265
|
if (d.tipo === 'roadmap') {
|
|
264
266
|
verificarEstadoDoRoadmap(raiz, d, base, achados);
|
|
267
|
+
if (base)
|
|
268
|
+
verificarMergeDaThread(raiz, d, base, achados, opcoes.pr === true);
|
|
265
269
|
verificarTabelasDeEstado(d, achados);
|
|
266
270
|
}
|
|
267
271
|
}
|
|
272
|
+
verificarIndices(raiz, docs, achados, opcoes.pr === true);
|
|
268
273
|
return { docs, achados };
|
|
269
274
|
}
|
|
270
275
|
function verificarChaves(d, achados) {
|
|
@@ -564,6 +569,95 @@ function verificarEstadoDoRoadmap(raiz, d, base, achados) {
|
|
|
564
569
|
}
|
|
565
570
|
}
|
|
566
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* RM-037 (fatia 3, defeito 1): o outro sentido da paridade com o git. Depois do merge do PR, o
|
|
574
|
+
* frontmatter da RM-049 (#26) e da RM-051 (#33) seguiu em "Branch criada", e o verificador dizia OK:
|
|
575
|
+
* ele so conferia que `Mesclado` tem o commit na base. O merge desta fabrica e pelo GitHub, sem o
|
|
576
|
+
* `ork ship` no caminho, entao a prova mora aqui, no job que o CI obrigatorio roda. A regra usa o
|
|
577
|
+
* mesmo `ship(<thread>)` do `sincronizarDocs`: tudo o que ela acusa o `sincronizar` corrige.
|
|
578
|
+
*/
|
|
579
|
+
function verificarMergeDaThread(raiz, d, base, achados, pr = false) {
|
|
580
|
+
const pendente = pendenciaDeMerge(raiz, d, base);
|
|
581
|
+
if (!pendente)
|
|
582
|
+
return;
|
|
583
|
+
const { thread, merges, codigo } = pendente;
|
|
584
|
+
achados.push((pr ? aviso : erro)(d.arquivo, d.id, 'docs.paridade.merge', `a thread ${thread} entrou na ${base} pelo merge ${merges[0].slice(0, 7)} (ship(${thread})), e o estado.codigo diz "${codigo || 'vazio'}"` +
|
|
585
|
+
(pr ? ' (no PR, aviso: o push da main reprova)' : ''), `abra um PR de docs sobre a ${base} atualizada com ork docs sincronizar --escrever --so ${d.id} (o item e os índices); ` +
|
|
586
|
+
`se o item tem outra fatia em curso, aponte sdlc.thread para a thread dela`));
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* A regra do `docs.paridade.merge`, isolada para o `ork ship registrar-pr` (RM-044, A3): a pagina cujo
|
|
590
|
+
* `sdlc.thread` ja tem merge `ship(<thread>)` na base e cujo `estado.codigo` ainda nao diz `Mesclado`.
|
|
591
|
+
* `thread`, quando dada, restringe a pagina a essa thread.
|
|
592
|
+
*/
|
|
593
|
+
function pendenciaDeMerge(raiz, d, base, thread) {
|
|
594
|
+
const sdlc = ehMapa(d.dados.sdlc) ? d.dados.sdlc : null;
|
|
595
|
+
const daPagina = sdlc && typeof sdlc.thread === 'string' ? sdlc.thread.trim() : '';
|
|
596
|
+
if (!daPagina || (thread !== undefined && daPagina !== thread))
|
|
597
|
+
return null;
|
|
598
|
+
const codigo = String((ehMapa(d.dados.estado) ? d.dados.estado.codigo : null) ?? '');
|
|
599
|
+
if (codigo === 'Mesclado')
|
|
600
|
+
return null;
|
|
601
|
+
const merges = mergesDaThreadNoGit(raiz, daPagina, base);
|
|
602
|
+
if (merges.length === 0)
|
|
603
|
+
return null;
|
|
604
|
+
return { thread: daPagina, merges, codigo };
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* RM-044 (A3): as paginas do roadmap, lidas da `ref` (a base remota depois do fetch, nao a copia local,
|
|
608
|
+
* que pode estar atras), que a regra `docs.paridade.merge` vai acusar por causa da `thread`. Nao grava
|
|
609
|
+
* nada: so lista. Ref ilegivel ou sem `docs/roadmap` devolve lista vazia.
|
|
610
|
+
*/
|
|
611
|
+
function docsPendentesDoMerge(raiz, thread, ref) {
|
|
612
|
+
const ls = git(raiz, ['ls-tree', '--name-only', `${ref}:${exports.DIR_ROADMAP}`]);
|
|
613
|
+
if (!ls.ok || !ls.saida)
|
|
614
|
+
return [];
|
|
615
|
+
const pendentes = [];
|
|
616
|
+
for (const nome of ls.saida.split('\n').filter(ehPaginaDeDocs).sort()) {
|
|
617
|
+
const arquivo = `${exports.DIR_ROADMAP}/${nome}`;
|
|
618
|
+
const blob = git(raiz, ['show', `${ref}:${arquivo}`]);
|
|
619
|
+
if (!blob.ok)
|
|
620
|
+
continue;
|
|
621
|
+
const lido = documentoDeTexto(arquivo, `${blob.saida}\n`);
|
|
622
|
+
if (!('doc' in lido) || lido.doc.tipo !== 'roadmap')
|
|
623
|
+
continue;
|
|
624
|
+
const p = pendenciaDeMerge(raiz, lido.doc, ref, thread);
|
|
625
|
+
if (!p)
|
|
626
|
+
continue;
|
|
627
|
+
pendentes.push({ id: lido.doc.id, arquivo, codigo: p.codigo, merge: p.merges[0],
|
|
628
|
+
comando: `ork docs sincronizar --escrever --so ${lido.doc.id}` });
|
|
629
|
+
}
|
|
630
|
+
return pendentes;
|
|
631
|
+
}
|
|
632
|
+
/** Os indices gerados (`docs/roadmap/README.md`, `docs/produto/README.md`) e o que o frontmatter diz. */
|
|
633
|
+
const INDICES_GERADOS = [
|
|
634
|
+
[`${exports.DIR_ROADMAP}/README.md`, (docs) => tabelaDoRoadmap(docs)],
|
|
635
|
+
[`${exports.DIR_PRODUTO}/README.md`, (docs) => tabelaDoProduto(docs)],
|
|
636
|
+
];
|
|
637
|
+
/**
|
|
638
|
+
* RM-037 (fatia 3, defeito 1): o indice e uma vista do frontmatter, como as tabelas do corpo, e mentia
|
|
639
|
+
* do mesmo jeito: sem a RM-051 e com a RM-031 de antes. Divergente reprova; vazio (o modelo recem-copiado
|
|
640
|
+
* pelo `ork docs init`) so avisa; sem os marcadores, nao ha indice gerado para conferir.
|
|
641
|
+
*/
|
|
642
|
+
function verificarIndices(raiz, docs, achados, pr = false) {
|
|
643
|
+
for (const [arquivo, gerar] of INDICES_GERADOS) {
|
|
644
|
+
const abs = path.join(raiz, arquivo);
|
|
645
|
+
if (!fs.existsSync(abs))
|
|
646
|
+
continue;
|
|
647
|
+
// Checkout com CRLF (Windows com core.autocrlf) nao e divergencia: as paginas tambem normalizam.
|
|
648
|
+
const atual = blocoEntre(fs.readFileSync(abs, 'utf8').replace(/\r\n/g, '\n'), INICIO, FIM);
|
|
649
|
+
if (atual === null)
|
|
650
|
+
continue;
|
|
651
|
+
if (atual === '') {
|
|
652
|
+
if (docs.length > 0) {
|
|
653
|
+
achados.push(aviso(arquivo, undefined, 'docs.paridade.indice', 'o índice ainda não foi gerado', 'rode ork docs sincronizar --escrever'));
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
else if (atual !== gerar(docs)) {
|
|
657
|
+
achados.push((pr ? aviso : erro)(arquivo, undefined, 'docs.paridade.indice', `o índice diverge do frontmatter das páginas${pr ? ' (no PR, aviso: o push da main reprova)' : ''}`, 'o índice é gerado do frontmatter: rode ork docs sincronizar --escrever e commite o índice'));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
567
661
|
// --------------------------------------------------------------------------------------------
|
|
568
662
|
// Saida para humano (TDAH) e para agente
|
|
569
663
|
function textoDaVerificacao(docs, achados) {
|
|
@@ -666,13 +760,26 @@ function mergeDaThread(raiz, thread, base) {
|
|
|
666
760
|
return { sha: String(ship.mergeSha).slice(0, 7), fonte: 'ledger ship_done' };
|
|
667
761
|
}
|
|
668
762
|
catch { /* thread sem estado nesta maquina: cai para o git */ }
|
|
669
|
-
|
|
670
|
-
const log = git(raiz, ['log', base, '--reverse', '--format=%H', '--fixed-strings', `--grep=ship(${thread})`]);
|
|
671
|
-
const primeiro = log.ok ? log.saida.split('\n')[0] : '';
|
|
763
|
+
const primeiro = mergesDaThreadNoGit(raiz, thread, base)[0];
|
|
672
764
|
if (primeiro)
|
|
673
765
|
return { sha: primeiro.slice(0, 7), fonte: `git log ${base}` };
|
|
674
766
|
return null;
|
|
675
767
|
}
|
|
768
|
+
/**
|
|
769
|
+
* Os commits `ship(<thread>)` da base, do mais velho ao mais novo, so pelo git (o CI nao tem ledger).
|
|
770
|
+
* RM-037 (fatia 3): um lugar so para o `sincronizarDocs` e o `verificarDocs` acharem o mesmo merge.
|
|
771
|
+
*/
|
|
772
|
+
function mergesDaThreadNoGit(raiz, thread, base) {
|
|
773
|
+
// Sem `-n 1`: o git corta antes de inverter, e `--reverse -n 1` devolveria o mais recente.
|
|
774
|
+
// GO-FIX 2 (achado 4 do CHECK): so a linha de primeiro pai da base e o assunto que COMECA com
|
|
775
|
+
// `ship(<thread>)`, como no `mergeDaEntrega`. Commit que cita o merge no corpo, ou o Revert dele, nao e merge.
|
|
776
|
+
const log = git(raiz, ['log', base, '--first-parent', '--reverse', '--format=%H%x09%s', '--fixed-strings', `--grep=ship(${thread})`]);
|
|
777
|
+
if (!log.ok)
|
|
778
|
+
return [];
|
|
779
|
+
return log.saida.split('\n').map((l) => l.split('\t'))
|
|
780
|
+
.filter(([sha, assunto]) => !!sha && typeof assunto === 'string' && assunto.startsWith(`ship(${thread})`))
|
|
781
|
+
.map(([sha]) => sha);
|
|
782
|
+
}
|
|
676
783
|
function temBranch(raiz, thread) {
|
|
677
784
|
const r = git(raiz, ['for-each-ref', '--format=%(refname:short)', `refs/heads/ork/${thread}-*`, `refs/remotes/origin/ork/${thread}-*`]);
|
|
678
785
|
return r.ok && r.saida.length > 0;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RM-039 (B6): o `ork doctor` acusa o pulse que bate devagar demais.
|
|
4
|
+
*
|
|
5
|
+
* A cadencia por tag (#OrkPulseOn-15m e as outras) so vale inteira com a varredura do cron de 15
|
|
6
|
+
* em 15 minutos. A instalacao antiga fica em `0 * * * *`, e nada avisava. Com o transporte do
|
|
7
|
+
* pulse configurado (`.orkastery/monitor/pulse-host.json`), o doctor le o `crontab -l` e avisa,
|
|
8
|
+
* sem bloquear, quando a linha da varredura falta ou bate mais devagar que o template
|
|
9
|
+
* `monitor/pulse.cron`. O doctor nunca edita o crontab: a correcao traz a linha pronta.
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.COMANDO_DO_TEMPLATE = exports.SCRIPT_DA_VARREDURA = exports.BATIDA_DO_TEMPLATE = void 0;
|
|
46
|
+
exports.lerCrontabDoSistema = lerCrontabDoSistema;
|
|
47
|
+
exports.intervaloEmMinutos = intervaloEmMinutos;
|
|
48
|
+
exports.linhasDaVarredura = linhasDaVarredura;
|
|
49
|
+
exports.scriptDaVarreduraDoOrk = scriptDaVarreduraDoOrk;
|
|
50
|
+
exports.comandoDaVarredura = comandoDaVarredura;
|
|
51
|
+
exports.checarCronDoPulse = checarCronDoPulse;
|
|
52
|
+
const fs = __importStar(require("node:fs"));
|
|
53
|
+
const path = __importStar(require("node:path"));
|
|
54
|
+
const util_1 = require("./util");
|
|
55
|
+
/** A batida do template `monitor/pulse.cron` (I-50): a menor cadencia das tags. */
|
|
56
|
+
exports.BATIDA_DO_TEMPLATE = '*/15 * * * *';
|
|
57
|
+
/** O script que o template agenda; a linha do crontab e achada por ele. */
|
|
58
|
+
exports.SCRIPT_DA_VARREDURA = 'varredura-pulse.sh';
|
|
59
|
+
/** Comando do template antes da troca do caminho pelo checkout de quem instala. */
|
|
60
|
+
exports.COMANDO_DO_TEMPLATE = '/CAMINHO/orkastery/monitor/varredura-pulse.sh';
|
|
61
|
+
/** Le o crontab do usuario do processo, sem shell e com prazo. */
|
|
62
|
+
function lerCrontabDoSistema() {
|
|
63
|
+
const r = (0, util_1.exec)('crontab', ['-l'], undefined, 5000);
|
|
64
|
+
if (r.ok)
|
|
65
|
+
return { ok: true, texto: r.stdout };
|
|
66
|
+
if (r.error === 'ENOENT')
|
|
67
|
+
return { ok: false, motivo: 'comando crontab ausente nesta maquina' };
|
|
68
|
+
const msg = (r.stderr || r.stdout).trim().split('\n')[0] ?? '';
|
|
69
|
+
return { ok: false, motivo: msg ? `crontab -l falhou: ${msg}` : `crontab -l saiu ${r.code}` };
|
|
70
|
+
}
|
|
71
|
+
const MACROS = { '@hourly': 60, '@daily': 1440, '@midnight': 1440, '@weekly': 10080,
|
|
72
|
+
'@monthly': 43200, '@yearly': 525600, '@annually': 525600 };
|
|
73
|
+
/**
|
|
74
|
+
* Maior intervalo, em minutos, entre duas batidas da linha (so os campos de minuto e hora; dia,
|
|
75
|
+
* mes e dia da semana restritos contam como mais devagar que uma hora). `null` quando a expressao
|
|
76
|
+
* nao e reconhecida: o doctor entao avisa pela linha, sem adivinhar.
|
|
77
|
+
*/
|
|
78
|
+
function intervaloEmMinutos(expressao) {
|
|
79
|
+
const campos = expressao.trim().split(/\s+/);
|
|
80
|
+
if (campos.length === 1 && campos[0].startsWith('@'))
|
|
81
|
+
return MACROS[campos[0]] ?? null;
|
|
82
|
+
if (campos.length !== 5)
|
|
83
|
+
return null;
|
|
84
|
+
const [minuto, hora, dia, mes, semana] = campos;
|
|
85
|
+
const minutos = valoresDoCampo(minuto, 0, 59);
|
|
86
|
+
if (!minutos)
|
|
87
|
+
return null;
|
|
88
|
+
if (hora !== '*' || dia !== '*' || mes !== '*' || semana !== '*')
|
|
89
|
+
return Math.max(60, maiorLacuna(minutos, 60));
|
|
90
|
+
return maiorLacuna(minutos, 60);
|
|
91
|
+
}
|
|
92
|
+
function valoresDoCampo(campo, min, max) {
|
|
93
|
+
const valores = new Set();
|
|
94
|
+
for (const parte of campo.split(',')) {
|
|
95
|
+
const m = /^(\*|(\d+)(?:-(\d+))?)(?:\/(\d+))?$/.exec(parte);
|
|
96
|
+
if (!m)
|
|
97
|
+
return null;
|
|
98
|
+
const inicio = m[1] === '*' ? min : Number(m[2]);
|
|
99
|
+
const fim = m[1] === '*' ? max : m[3] !== undefined ? Number(m[3]) : m[4] !== undefined ? max : inicio;
|
|
100
|
+
const passo = m[4] !== undefined ? Number(m[4]) : 1;
|
|
101
|
+
if (passo < 1 || inicio < min || fim > max || inicio > fim)
|
|
102
|
+
return null;
|
|
103
|
+
for (let v = inicio; v <= fim; v += passo)
|
|
104
|
+
valores.add(v);
|
|
105
|
+
}
|
|
106
|
+
return [...valores].sort((a, b) => a - b);
|
|
107
|
+
}
|
|
108
|
+
/** Maior distancia entre dois valores consecutivos, contando a volta do ciclo. */
|
|
109
|
+
function maiorLacuna(valores, ciclo) {
|
|
110
|
+
let maior = 0;
|
|
111
|
+
for (let i = 0; i < valores.length; i++) {
|
|
112
|
+
const proximo = i + 1 < valores.length ? valores[i + 1] : valores[0] + ciclo;
|
|
113
|
+
maior = Math.max(maior, proximo - valores[i]);
|
|
114
|
+
}
|
|
115
|
+
return maior;
|
|
116
|
+
}
|
|
117
|
+
/** Separa a expressao de horario do comando numa linha do crontab. */
|
|
118
|
+
function dividirLinha(linha) {
|
|
119
|
+
const t = linha.trim();
|
|
120
|
+
if (t.startsWith('@')) {
|
|
121
|
+
const m = /^(@\S+)\s+(.+)$/.exec(t);
|
|
122
|
+
return m ? { quando: m[1], comando: m[2] } : null;
|
|
123
|
+
}
|
|
124
|
+
const m = /^(\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.+)$/.exec(t);
|
|
125
|
+
return m ? { quando: m[1], comando: m[2] } : null;
|
|
126
|
+
}
|
|
127
|
+
/** Linhas ativas (nao comentario, nao variavel) que agendam a varredura do pulse. */
|
|
128
|
+
function linhasDaVarredura(crontab) {
|
|
129
|
+
return crontab.split('\n').map(l => l.trim())
|
|
130
|
+
.filter(l => l && !l.startsWith('#') && !/^[A-Za-z_][A-Za-z0-9_]*\s*=/.test(l) && l.includes(exports.SCRIPT_DA_VARREDURA));
|
|
131
|
+
}
|
|
132
|
+
/** Aspas de shell so quando o caminho precisa (a linha vai colada no crontab, que roda por `sh`). */
|
|
133
|
+
function citar(caminho) {
|
|
134
|
+
return /^[A-Za-z0-9_./:@+-]+$/.test(caminho) ? caminho : `'${caminho.replace(/'/g, `'\\''`)}'`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* O script da varredura que este `ork` traz: no pacote do npm, `monitor/` fica ao lado de `dist/`; no
|
|
138
|
+
* checkout, na raiz do repositorio, dois niveis acima de `core/dist/`. `null` quando nenhum existe.
|
|
139
|
+
*/
|
|
140
|
+
function scriptDaVarreduraDoOrk(dirDoDist = __dirname) {
|
|
141
|
+
for (const dir of [path.join(dirDoDist, '..', 'monitor'), path.join(dirDoDist, '..', '..', 'monitor')]) {
|
|
142
|
+
const script = path.join(dir, exports.SCRIPT_DA_VARREDURA);
|
|
143
|
+
if (fs.existsSync(script))
|
|
144
|
+
return path.resolve(script);
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* O comando que a linha nova do crontab roda. Ensaio de 03/10/2026 (RM-049): a correcao apontava
|
|
150
|
+
* `<projeto>/monitor/varredura-pulse.sh`, que so existe no checkout do proprio Orkastery; quem instalou
|
|
151
|
+
* pelo npm recebia um caminho que nao existe. Quando o projeto nao traz o script, a linha usa o do `ork`
|
|
152
|
+
* instalado e diz o projeto por `ORK_PULSE_PROJECT` (o cron nao roda no diretorio do projeto).
|
|
153
|
+
*/
|
|
154
|
+
function comandoDaVarredura(raizDoProjeto, scriptDoOrk = scriptDaVarreduraDoOrk()) {
|
|
155
|
+
const doProjeto = path.join(raizDoProjeto, 'monitor', exports.SCRIPT_DA_VARREDURA);
|
|
156
|
+
if (fs.existsSync(doProjeto) || !scriptDoOrk)
|
|
157
|
+
return citar(doProjeto);
|
|
158
|
+
return `ORK_PULSE_PROJECT=${citar(raizDoProjeto)} ${citar(scriptDoOrk)}`;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Check "cadencia do pulse no cron". `null` quando o projeto nao tem transporte do pulse: sem
|
|
162
|
+
* `pulse-host.json` nao ha resumo para entregar, e o doctor nao fala de cron.
|
|
163
|
+
*/
|
|
164
|
+
function checarCronDoPulse(dirMonitor, raizDoCheckout, ler = lerCrontabDoSistema, scriptDoOrk = scriptDaVarreduraDoOrk()) {
|
|
165
|
+
if (!fs.existsSync(path.join(dirMonitor, 'pulse-host.json')))
|
|
166
|
+
return null;
|
|
167
|
+
const nome = 'cadencia do pulse no cron';
|
|
168
|
+
const comandoSugerido = comandoDaVarredura(raizDoCheckout, scriptDoOrk);
|
|
169
|
+
const linhaNova = (comando) => `${exports.BATIDA_DO_TEMPLATE} ${comando}`;
|
|
170
|
+
const ausente = (detalhe) => ({ nome, nivel: 'warn', detalhe,
|
|
171
|
+
correcao: `adicione ao crontab (crontab -e), como no template monitor/pulse.cron: ${linhaNova(comandoSugerido)}` });
|
|
172
|
+
const leitura = ler();
|
|
173
|
+
if (!leitura.ok)
|
|
174
|
+
return ausente(`pulse-host.json presente, mas sem crontab para ler (${leitura.motivo}): a varredura nao bate`);
|
|
175
|
+
const linhas = linhasDaVarredura(leitura.texto);
|
|
176
|
+
if (linhas.length === 0)
|
|
177
|
+
return ausente(`pulse-host.json presente, e nenhuma linha do crontab roda ${exports.SCRIPT_DA_VARREDURA}: o resumo nao sai`);
|
|
178
|
+
const lentas = [];
|
|
179
|
+
for (const linha of linhas) {
|
|
180
|
+
const partes = dividirLinha(linha);
|
|
181
|
+
const intervalo = partes ? intervaloEmMinutos(partes.quando) : null;
|
|
182
|
+
if (intervalo === null || intervalo > 15)
|
|
183
|
+
lentas.push({ linha, comando: partes?.comando ?? comandoSugerido, intervalo });
|
|
184
|
+
}
|
|
185
|
+
if (lentas.length < linhas.length) {
|
|
186
|
+
return { nome, nivel: 'ok', detalhe: `varredura no cron a cada 15 minutos ou menos (${linhas.length} linha(s))` };
|
|
187
|
+
}
|
|
188
|
+
const l = lentas[0];
|
|
189
|
+
const ritmo = l.intervalo === null ? 'com horario que o doctor nao le' : l.intervalo === 60 ? 'de hora em hora'
|
|
190
|
+
: `a cada ${l.intervalo} minutos`;
|
|
191
|
+
return { nome, nivel: 'warn',
|
|
192
|
+
detalhe: `a varredura do pulse bate ${ritmo} (\`${l.linha}\`): a cadencia por tag (RM-039) pede 15 minutos`,
|
|
193
|
+
correcao: `troque a linha no crontab (crontab -e) por: ${linhaNova(l.comando)}` };
|
|
194
|
+
}
|