@orkastery/cli 0.4.2 → 0.5.0
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/README.md +11 -0
- package/adapters/claude-code/.claude-plugin/plugin.json +3 -1
- package/adapters/claude-code/agents/ork-check.md +1 -1
- package/adapters/claude-code/agents/ork-go.md +2 -2
- package/adapters/claude-code/agents/ork-goal.md +1 -1
- package/adapters/claude-code/agents/ork-master.md +1 -1
- package/adapters/claude-code/agents/ork-plan.md +1 -1
- package/adapters/claude-code/agents/ork-ship.md +1 -1
- package/adapters/claude-code/commands/master.md +11 -8
- package/adapters/claude-code/commands/ork.md +8 -0
- package/adapters/claude-code/commands/ship.md +2 -2
- package/adapters/codex/skills/ork/SKILL.md +12 -1
- package/adapters/hermes/README.md +3 -0
- package/adapters/hermes/bin/ork-abrir-thread.sh +17 -2
- package/adapters/hermes/bin/ork-brain.sh +3 -0
- package/adapters/hermes/bin/ork-maestro.sh +4 -1
- package/adapters/hermes/bin/ork-network-roadmap.sh +11 -0
- package/adapters/hermes/bin/ork-roadmap-status.sh +9 -0
- package/adapters/hermes/hermes.plugin.json +2 -0
- package/adapters/hermes/hitl-ingress/__init__.py +36 -3
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +19 -20
- package/adapters/openclaw/README.md +34 -3
- package/adapters/openclaw/dist/hitl-ingress.js +37 -3
- package/adapters/openclaw/dist/index.js +92 -39
- package/adapters/openclaw/openclaw.plugin.json +10 -3
- package/adapters/openclaw/package.json +2 -2
- package/adapters/openclaw/src/hitl-ingress.ts +32 -3
- package/adapters/openclaw/src/index.ts +108 -41
- package/assets/docs/padroes/documentacao-de-produto.md +2 -2
- package/assets/docs/padroes/roadmap-de-produto.md +1 -1
- package/assets/docs/produto/_modelo-feature.md +2 -0
- package/assets/orkmind_bridge.py +214 -1
- package/dist/adapters/claude-bg.js +88 -9
- package/dist/adapters/codex-events.js +3 -1
- package/dist/board.js +74 -0
- package/dist/branch-de-estado.js +5 -4
- package/dist/busca-semantica.js +161 -0
- package/dist/ci.js +141 -10
- package/dist/company-brain-cli.js +27 -2
- package/dist/company-brain-context.js +132 -0
- package/dist/company-brain-dossie.js +376 -0
- package/dist/company-brain-mcp.js +4 -1
- package/dist/conducao.js +117 -31
- package/dist/decisao-autonoma.js +42 -0
- package/dist/docs.js +64 -22
- package/dist/doctor.js +33 -0
- package/dist/entrega-pr.js +109 -5
- package/dist/experiencia-instalacao.js +256 -0
- package/dist/experiencia.js +72 -0
- package/dist/fabrica-estado.js +43 -10
- package/dist/fechamento.js +105 -0
- package/dist/fix.js +1 -1
- package/dist/forja.js +435 -0
- package/dist/gates.js +10 -1
- package/dist/hitl-classificacao.js +11 -1
- package/dist/hitl-contract.js +50 -7
- package/dist/hitl-curto.js +158 -0
- package/dist/hitl-gates.js +67 -1
- package/dist/hitl-lote.js +30 -15
- package/dist/hitl-presentation.js +10 -7
- package/dist/hitl-resumo.js +33 -12
- package/dist/hitl-texto-livre.js +150 -0
- package/dist/hosts.js +86 -1
- package/dist/index.js +605 -41
- package/dist/indice-vetorial.js +317 -0
- package/dist/init.js +13 -0
- package/dist/integracoes-locais.js +1 -1
- package/dist/intelligence-benchmark-contract.js +570 -0
- package/dist/intelligence-graph-cli.js +430 -0
- package/dist/intelligence-graph-contract.js +577 -0
- package/dist/intelligence-graph-extract-md.js +469 -0
- package/dist/intelligence-graph-extract-ts.js +810 -0
- package/dist/intelligence-graph-extract.js +369 -0
- package/dist/intelligence-graph-index.js +512 -0
- package/dist/intelligence-graph-parsers.js +327 -0
- package/dist/intelligence-graph-query.js +344 -0
- package/dist/intelligence-graph-repo.js +187 -0
- package/dist/leases.js +84 -0
- package/dist/ledger.js +14 -0
- package/dist/maestro-cli.js +10 -1
- package/dist/maestro-contract.js +5 -1
- package/dist/maestro-discovery.js +16 -2
- package/dist/maestro-snapshot.js +6 -2
- package/dist/manifest.js +142 -5
- package/dist/master-batch.js +3 -2
- package/dist/master-digest.js +2 -2
- package/dist/master-nota.js +203 -0
- package/dist/master.js +4 -0
- package/dist/mcp-experiencia.js +164 -0
- package/dist/mcp-git.js +36 -10
- package/dist/mcp-install.js +3 -1
- package/dist/mcp-maestro.js +3 -2
- package/dist/mcp-server.js +84 -5
- package/dist/memoria.js +147 -1
- package/dist/network-roadmap.js +907 -0
- package/dist/ocupacao.js +38 -2
- package/dist/onboarding.js +113 -7
- package/dist/orkmind.js +165 -9
- package/dist/phase.js +166 -13
- package/dist/policies.js +2 -0
- package/dist/projeto-alvo.js +551 -0
- package/dist/prompts.js +1 -1
- package/dist/pulse-consentimento.js +4 -2
- package/dist/pulse-delivery.js +5 -0
- package/dist/pulse-resposta.js +674 -49
- package/dist/pulse.js +21 -0
- package/dist/ratelimit.js +3 -2
- package/dist/retry.js +230 -9
- package/dist/roadmap-reservas.js +262 -20
- package/dist/roadmap-status.js +213 -0
- package/dist/runtime-ambiente.js +12 -1
- package/dist/session-watcher-claude.js +26 -0
- package/dist/sessoes.js +49 -15
- package/dist/thread-close.js +3 -0
- package/dist/verify.js +5 -4
- package/dist/worktree.js +77 -0
- package/eval/casos/orchestration-experience-pt-br.json +48 -0
- package/eval/casos/orchestration-experience.json +48 -0
- package/package.json +1 -1
- package/schemas/code-artifact-graph.v1.schema.json +581 -0
- package/schemas/graph-benchmark.v1.schema.json +2240 -0
- package/schemas/maestro-snapshot.schema.json +23 -0
- package/skills/README.md +7 -5
- package/skills/core/onboarding/SKILL.md +7 -0
- package/skills/core/orchestration-experience/SKILL.md +113 -0
- package/skills/core/orchestration-experience-pt-br/SKILL.md +112 -0
- package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
- package/skills/phases/go-implementation/SKILL.md +2 -0
package/dist/pulse-resposta.js
CHANGED
|
@@ -33,15 +33,26 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.chaveDoGate = exports.GRAMATICA_DO_PULSE = exports.CONTRATO_LOTE_SERVIDO = exports.CONTRATO_RESPOSTA_DO_PULSE = void 0;
|
|
36
|
+
exports.CONTRATO_ESCUTA = exports.chaveDoGate = exports.GUARDA_DA_PERGUNTA_MS = exports.GRAMATICA_DO_PULSE = exports.CONTRATO_LOTE_SERVIDO = exports.CONTRATO_RESPOSTA_DO_PULSE = void 0;
|
|
37
37
|
exports.interpretarRespostaDoPulse = interpretarRespostaDoPulse;
|
|
38
38
|
exports.arquivoDoLoteServido = arquivoDoLoteServido;
|
|
39
39
|
exports.lerLoteServido = lerLoteServido;
|
|
40
40
|
exports.perguntaViva = perguntaViva;
|
|
41
|
+
exports.perguntaEmAberto = perguntaEmAberto;
|
|
41
42
|
exports.threadsComPerguntaViva = threadsComPerguntaViva;
|
|
42
43
|
exports.gatesJaServidos = gatesJaServidos;
|
|
43
44
|
exports.avaliarFila = avaliarFila;
|
|
44
45
|
exports.servirLote = servirLote;
|
|
46
|
+
exports.pedidoParaResponder = pedidoParaResponder;
|
|
47
|
+
exports.pedidosComCodigo = pedidosComCodigo;
|
|
48
|
+
exports.codigosEmUso = codigosEmUso;
|
|
49
|
+
exports.textoDoDetalhe = textoDoDetalhe;
|
|
50
|
+
exports.opcoesReais = opcoesReais;
|
|
51
|
+
exports.registrarPorCodigo = registrarPorCodigo;
|
|
52
|
+
exports.pedidosAbertosDaThread = pedidosAbertosDaThread;
|
|
53
|
+
exports.abertosDoDono = abertosDoDono;
|
|
54
|
+
exports.arquivoDaEscuta = arquivoDaEscuta;
|
|
55
|
+
exports.atualizarEscuta = atualizarEscuta;
|
|
45
56
|
exports.responderPeloPulse = responderPeloPulse;
|
|
46
57
|
/**
|
|
47
58
|
* I-41 (GO-FIX 1, B1 e B2): o caminho de volta.
|
|
@@ -73,6 +84,15 @@ const hitl_gates_1 = require("./hitl-gates");
|
|
|
73
84
|
const hitl_contract_1 = require("./hitl-contract");
|
|
74
85
|
const monitor_lock_1 = require("./monitor-lock");
|
|
75
86
|
const hitl_lote_1 = require("./hitl-lote");
|
|
87
|
+
const hitl_curto_1 = require("./hitl-curto");
|
|
88
|
+
const hitl_texto_livre_1 = require("./hitl-texto-livre");
|
|
89
|
+
const hitl_presentation_1 = require("./hitl-presentation");
|
|
90
|
+
const roadmap_status_1 = require("./roadmap-status");
|
|
91
|
+
const master_nota_1 = require("./master-nota");
|
|
92
|
+
const master_1 = require("./master");
|
|
93
|
+
const master_batch_1 = require("./master-batch");
|
|
94
|
+
const master_digest_1 = require("./master-digest");
|
|
95
|
+
const hitl_classificacao_1 = require("./hitl-classificacao");
|
|
76
96
|
const pulse_cadencia_1 = require("./pulse-cadencia");
|
|
77
97
|
const ledger_1 = require("./ledger");
|
|
78
98
|
const thread_1 = require("./thread");
|
|
@@ -95,31 +115,60 @@ exports.CONTRATO_LOTE_SERVIDO = 'ork.pulse-lote-servido/v1';
|
|
|
95
115
|
* Tag no meio de uma frase continua sendo conversa com o assistente.
|
|
96
116
|
*/
|
|
97
117
|
exports.GRAMATICA_DO_PULSE = Object.freeze({
|
|
98
|
-
|
|
118
|
+
// RM-048 (D9): o texto depois do codigo vai a 200 caracteres, para caber o porque da nota.
|
|
119
|
+
consentimento: '^[ \\t]*(?=[A-HJKMNP-TV-Z][2-9A-HJKMNP-TV-Z]{0,2}[2-9])[A-HJKMNP-TV-Z][2-9A-HJKMNP-TV-Z]{3}[ \\t]+[^\\r\\n]{1,200}$',
|
|
99
120
|
lote: '^[ \\t]*[0-9]{1,2}[ \\t]*[a-zA-Z](?:[ \\t,;]*[0-9]{1,2}[ \\t]*[a-zA-Z])*[ \\t]*[.!]?[ \\t]*$',
|
|
100
121
|
cadencia: '^[ \\t]*#OrkPulse(?:On(?:-(?:15|30|60)m)?|Off)[ \\t]*[.!]?[ \\t]*$',
|
|
122
|
+
// RM-048 (D2 e D3): as formas do texto livre, geradas do vocabulario fechado. `lista` e
|
|
123
|
+
// interceptada sempre; `livre` so dentro da janela de escuta (`pulse-escuta.json`).
|
|
124
|
+
lista: hitl_texto_livre_1.FORMAS_DO_TEXTO_LIVRE.lista,
|
|
125
|
+
livre: hitl_texto_livre_1.FORMAS_DO_TEXTO_LIVRE.livre,
|
|
126
|
+
// RM-048 (item 8): o teclado do digest semanal ("ratificar <thread> <assinatura> <classe>" e
|
|
127
|
+
// "ratificar-lote <semana> <assinatura>") passa pelo mesmo endereco assinado, e nao pelo agente.
|
|
128
|
+
ratificacao: '^[ \\t]*ratificar(?:-lote)?(?:[ \\t]+[A-Za-z0-9._-]{1,80}){2,3}[ \\t]*$',
|
|
101
129
|
});
|
|
130
|
+
const RATIFICACAO = new RegExp(exports.GRAMATICA_DO_PULSE.ratificacao, 'i');
|
|
102
131
|
const CONSENTIMENTO = new RegExp(exports.GRAMATICA_DO_PULSE.consentimento, 'i');
|
|
103
132
|
const LOTE = new RegExp(exports.GRAMATICA_DO_PULSE.lote);
|
|
104
133
|
const CADENCIA = new RegExp(exports.GRAMATICA_DO_PULSE.cadencia, 'i');
|
|
105
134
|
/** O que o dono quis dizer, pela forma. Nada aqui le intencao: ou a forma bate, ou nao. */
|
|
106
135
|
function interpretarRespostaDoPulse(texto) {
|
|
107
|
-
if (typeof texto !== 'string' || texto.length >
|
|
136
|
+
if (typeof texto !== 'string' || texto.length > hitl_texto_livre_1.TETO_DA_FORMA)
|
|
108
137
|
return { forma: 'desconhecida' };
|
|
138
|
+
// S2 do CHECK: a lista vem antes, porque "1 s" e "1 n" sao palavras do vocabulario e nao letras.
|
|
139
|
+
// "1a 2c" casa as duas e sai igual pelas duas; o lote segue para letra fora de a-d ("1e").
|
|
140
|
+
const lista = (0, hitl_texto_livre_1.lerLista)(texto);
|
|
141
|
+
if (lista && lista.some(i => i.trecho.tipo !== 'letra'))
|
|
142
|
+
return { forma: 'lista', itens: lista };
|
|
109
143
|
if (LOTE.test(texto)) {
|
|
110
144
|
const escolhas = [...texto.matchAll(/([0-9]{1,2})[ \t]*([a-zA-Z])/g)]
|
|
111
145
|
.map(m => ({ numero: Number(m[1]), letra: m[2].toLowerCase() }));
|
|
112
146
|
return { forma: 'lote', escolhas };
|
|
113
147
|
}
|
|
148
|
+
if (lista)
|
|
149
|
+
return { forma: 'lista', itens: lista };
|
|
150
|
+
if (RATIFICACAO.test(texto))
|
|
151
|
+
return { forma: 'ratificacao', texto: texto.trim() };
|
|
114
152
|
if (CONSENTIMENTO.test(texto))
|
|
115
153
|
return { forma: 'consentimento', codigo: texto.trim().slice(0, 4).toUpperCase() };
|
|
116
154
|
const tag = CADENCIA.test(texto) ? (0, pulse_cadencia_1.extrairTagDoPulse)(texto) : null;
|
|
117
155
|
if (tag)
|
|
118
156
|
return { forma: 'cadencia', tag };
|
|
157
|
+
const solta = (0, hitl_texto_livre_1.lerSolta)(texto);
|
|
158
|
+
if (solta)
|
|
159
|
+
return { forma: 'livre', trecho: solta };
|
|
119
160
|
return { forma: 'desconhecida' };
|
|
120
161
|
}
|
|
121
162
|
/** Quanto tempo uma pergunta respondida continua no registro, para "ja estava registrada". */
|
|
122
163
|
const GUARDA_DO_RESPONDIDO_MS = 24 * 60 * 60 * 1000;
|
|
164
|
+
/**
|
|
165
|
+
* RM-048 (D4): quanto tempo o NUMERO de uma pergunta nao respondida continua valendo depois de
|
|
166
|
+
* servido. O prazo do pedido e de uma hora; o numero que o dono recebeu nao pode vencer junto:
|
|
167
|
+
* respondido depois, ele vai ao pedido renovado quando a pergunta e a mesma.
|
|
168
|
+
*/
|
|
169
|
+
exports.GUARDA_DA_PERGUNTA_MS = 24 * 60 * 60 * 1000;
|
|
170
|
+
/** O maior numero que a gramatica do ingresso le (dois digitos). */
|
|
171
|
+
const MAIOR_NUMERO = 99;
|
|
123
172
|
const sha = (s) => (0, node_crypto_1.createHash)('sha256').update(s).digest('hex');
|
|
124
173
|
function arquivoDoLoteServido(raiz, estadoDir) {
|
|
125
174
|
return path.join(estadoDir ?? path.join((0, estado_thread_1.raizDoEstado)(raiz), '.orkastery', 'monitor'), 'pulse-lote.json');
|
|
@@ -145,16 +194,39 @@ function gravarLoteServido(raiz, lote, estadoDir) {
|
|
|
145
194
|
function perguntaViva(p, quando) {
|
|
146
195
|
return !p.respondida && (p.prazo === null || Date.parse(quando) < Date.parse(p.prazo));
|
|
147
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* RM-048 (D4): a pergunta que o dono ainda pode responder pelo numero. Sem resposta e dentro da
|
|
199
|
+
* guarda de 24 h desde que saiu: depois do prazo do pedido ela vai ao pedido renovado.
|
|
200
|
+
*/
|
|
201
|
+
function perguntaEmAberto(p, quando) {
|
|
202
|
+
return !p.respondida && (perguntaViva(p, quando) || Date.parse(quando) - Date.parse(p.servidaEm) < exports.GUARDA_DA_PERGUNTA_MS);
|
|
203
|
+
}
|
|
148
204
|
/** O que sai do registro: vencida sem resposta, ou respondida ha mais de um dia. */
|
|
149
205
|
function podar(lote, quando) {
|
|
150
206
|
const agora = Date.parse(quando);
|
|
151
207
|
const perguntas = lote.perguntas.filter(p => perguntaViva(p, quando) ||
|
|
208
|
+
(!p.respondida && agora - Date.parse(p.servidaEm) < exports.GUARDA_DA_PERGUNTA_MS) ||
|
|
152
209
|
(p.respondida && agora - Date.parse(p.respondida.em) < GUARDA_DO_RESPONDIDO_MS));
|
|
153
210
|
const numeros = new Set(perguntas.map(p => p.numero));
|
|
154
211
|
const porCodigo = Object.fromEntries(Object.entries(lote.porCodigo)
|
|
155
212
|
.map(([codigo, ns]) => [codigo, ns.filter(n => numeros.has(n))]).filter(([, ns]) => ns.length));
|
|
156
213
|
return { ...lote, perguntas, porCodigo };
|
|
157
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* O primeiro numero do proximo lote. Continua os do registro, para "1a" nunca querer dizer duas
|
|
217
|
+
* perguntas; quando passaria de 99, que a gramatica nao le, volta ao menor bloco livre.
|
|
218
|
+
*/
|
|
219
|
+
function numeroInicialLivre(servido) {
|
|
220
|
+
const usados = new Set(servido.perguntas.map(p => p.numero));
|
|
221
|
+
const seguinte = Math.max(0, ...usados) + 1;
|
|
222
|
+
if (seguinte + hitl_lote_1.TETO_DE_PERGUNTAS_POR_LOTE - 1 <= MAIOR_NUMERO)
|
|
223
|
+
return seguinte;
|
|
224
|
+
for (let n = 1; n + hitl_lote_1.TETO_DE_PERGUNTAS_POR_LOTE - 1 <= MAIOR_NUMERO; n++) {
|
|
225
|
+
if (Array.from({ length: hitl_lote_1.TETO_DE_PERGUNTAS_POR_LOTE }, (_, i) => n + i).every(k => !usados.has(k)))
|
|
226
|
+
return n;
|
|
227
|
+
}
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
158
230
|
/** Threads com pergunta servida esperando resposta: elas nao entram de novo no resumo. */
|
|
159
231
|
function threadsComPerguntaViva(raiz, quando, estadoDir) {
|
|
160
232
|
return new Set(lerLoteServido(raiz, estadoDir).perguntas.filter(p => perguntaViva(p, quando)).map(p => p.thread));
|
|
@@ -189,7 +261,9 @@ function avaliarFila(raiz, itens, quando, opcoes = {}) {
|
|
|
189
261
|
const candidatos = [], atos = new Map();
|
|
190
262
|
let consertos = 0;
|
|
191
263
|
for (const [thread, doThread] of porThread) {
|
|
192
|
-
|
|
264
|
+
// RM-048 (D6): escalacao tecnica nao vira pergunta ao dono; o resumo a mostra como "Conosco".
|
|
265
|
+
const escalacoes = doThread.map(i => i.motivo).filter(m => hitl_gates_1.MOTIVOS_DE_ESCALACAO_HUMANA.includes(m) &&
|
|
266
|
+
(0, hitl_classificacao_1.quemDecide)(m) === 'dono');
|
|
193
267
|
let achado;
|
|
194
268
|
for (const motivo of [...new Set([...escalacoes, 'human.pending'])]) {
|
|
195
269
|
try {
|
|
@@ -214,7 +288,7 @@ function avaliarFila(raiz, itens, quando, opcoes = {}) {
|
|
|
214
288
|
// Sem gate que espere, um pedido aberto ainda e pergunta se for respondivel agora; senao, e
|
|
215
289
|
// conserto quando o formato e o problema, e historia quando o prazo ja passou.
|
|
216
290
|
const pedido = doThread.map(i => i.pedido).find((p) => !!p);
|
|
217
|
-
if (!pedido)
|
|
291
|
+
if (!pedido || (0, hitl_classificacao_1.quemDecide)((0, hitl_contract_1.motivoDoPedido)(pedido)) !== 'dono')
|
|
218
292
|
continue;
|
|
219
293
|
let aberto = false;
|
|
220
294
|
try {
|
|
@@ -261,7 +335,7 @@ function servirLote(raiz, entrada) {
|
|
|
261
335
|
const { quando } = entrada;
|
|
262
336
|
let servido = podar(lerLoteServido(raiz, entrada.estadoDir), quando);
|
|
263
337
|
const vivas = new Set(servido.perguntas.filter(p => perguntaViva(p, quando)).map(p => p.thread));
|
|
264
|
-
const inicio = servido
|
|
338
|
+
const inicio = numeroInicialLivre(servido);
|
|
265
339
|
const abertos = [];
|
|
266
340
|
// Thread ocupada agora (despacho ou outra resposta segurando o lock) nao e thread que deixou de
|
|
267
341
|
// esperar: ela vai para o proximo lote, e nao some do que o dono pediu.
|
|
@@ -289,12 +363,25 @@ function servirLote(raiz, entrada) {
|
|
|
289
363
|
}
|
|
290
364
|
const lote = (0, hitl_lote_1.montarLote)(abertos.map(a => a.pedido), { numeroInicial: inicio });
|
|
291
365
|
const restantes = [...ocupadas, ...candidatos.slice(i)];
|
|
366
|
+
// RM-048 (D1): "desde" sai do ledger, do primeiro pedido com o mesmo codigo; renovar nao zera.
|
|
367
|
+
for (const p of lote.perguntas) {
|
|
368
|
+
const pedido = abertos.find(a => a.pedido.id === p.pedidoId).pedido;
|
|
369
|
+
try {
|
|
370
|
+
p.desde = (0, hitl_curto_1.desdeDoPedido)((0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, pedido.thread)), pedido);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
p.desde = pedido.criadoEm;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
292
376
|
const novas = lote.perguntas.map(p => {
|
|
293
377
|
const pedido = abertos.find(a => a.pedido.id === p.pedidoId).pedido;
|
|
378
|
+
const recomendada = p.alternativas.find(a => a.recomendada);
|
|
294
379
|
return {
|
|
295
380
|
numero: p.numero, thread: pedido.thread, fase: pedido.fase, pedidoId: pedido.id,
|
|
296
381
|
pedidoSha256: sha(JSON.stringify(pedido)), letras: p.alternativas.map(a => a.letra),
|
|
297
382
|
alternativas: p.alternativas.map(a => a.texto), consequencias: p.alternativas.map(a => a.consequencia),
|
|
383
|
+
...(recomendada ? { recomendada: { letra: recomendada.letra, porque: recomendada.recomendada.porque } } : {}),
|
|
384
|
+
...(p.corpo ? { corpo: p.corpo } : {}), ...(p.ato ? { ato: p.ato } : {}), desde: p.desde ?? null,
|
|
298
385
|
pergunta: p.pergunta, prazo: (0, hitl_contract_1.prazoDoPedido)(pedido) ?? null, servidaEm: quando,
|
|
299
386
|
};
|
|
300
387
|
});
|
|
@@ -308,7 +395,7 @@ function servirLote(raiz, entrada) {
|
|
|
308
395
|
quando, resumoSha256: entrada.pedido.resumoSha256, candidatos: restantes,
|
|
309
396
|
prazoMin: pulse_consentimento_1.PRAZO_PADRAO_MIN, estadoDir: entrada.estadoDir,
|
|
310
397
|
}) : undefined;
|
|
311
|
-
const texto = (0, hitl_lote_1.textoDoLote)(lote, { canal: entrada.canal, naoEsperam,
|
|
398
|
+
const texto = (0, hitl_lote_1.textoDoLote)(lote, { canal: entrada.canal, naoEsperam, quando,
|
|
312
399
|
...(proximo ? { proximo: { codigo: proximo.codigo, faltam: restantes.length } } : {}) });
|
|
313
400
|
return { texto, numeros, restantes, naoEsperam, ...(proximo ? { proximo } : {}) };
|
|
314
401
|
}
|
|
@@ -316,21 +403,514 @@ function servirLote(raiz, entrada) {
|
|
|
316
403
|
* O lote de um sim ja servido, para quem pede de novo, ou o aviso de que ele ja nao serve. Reenviar
|
|
317
404
|
* pergunta morta (toda respondida ou vencida) seria pedir uma resposta que o gate vai recusar.
|
|
318
405
|
*/
|
|
319
|
-
function loteDeNovo(servido, numeros, codigo, quando, canal, aberto) {
|
|
320
|
-
const vivas = servido.perguntas.filter(p => numeros.includes(p.numero) &&
|
|
406
|
+
function loteDeNovo(raiz, servido, numeros, codigo, quando, canal, aberto) {
|
|
407
|
+
const vivas = servido.perguntas.filter(p => numeros.includes(p.numero) && perguntaEmAberto(p, quando)).map(p => p.numero);
|
|
321
408
|
if (vivas.length)
|
|
322
|
-
return reenviarLote(servido, vivas, canal);
|
|
409
|
+
return reenviarLote(raiz, servido, vivas, canal, quando);
|
|
323
410
|
return `As perguntas do código ${codigo} já foram respondidas ou venceram. ` +
|
|
324
411
|
(aberto ? `Para as de agora, responda ${aberto.codigo} a.` : 'O próximo resumo traz as que ainda esperarem você.');
|
|
325
412
|
}
|
|
326
413
|
/** O MESMO lote de novo, para quem repete o sim porque nao viu a mensagem chegar. */
|
|
327
|
-
function reenviarLote(servido, numeros, canal) {
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
414
|
+
function reenviarLote(raiz, servido, numeros, canal, quando) {
|
|
415
|
+
const semRecomendada = [];
|
|
416
|
+
const perguntas = servido.perguntas.filter(p => numeros.includes(p.numero)).flatMap(p => {
|
|
417
|
+
// Registro gravado antes do RM-048 nao tem a recomendada: ela sai de novo do pedido servido.
|
|
418
|
+
// Sem ela, a pergunta nao e reenviada no contrato curto (que exige uma recomendada), e o
|
|
419
|
+
// nucleo nunca inventa uma: o dono recebe a linha para responder pelo numero.
|
|
420
|
+
const recomendada = p.recomendada ?? recomendadaDoRegistro(raiz, p);
|
|
421
|
+
if (!recomendada) {
|
|
422
|
+
semRecomendada.push(p.numero);
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
return [{
|
|
426
|
+
numero: p.numero, thread: p.thread, fase: p.fase, pergunta: p.pergunta, pedidoId: p.pedidoId,
|
|
427
|
+
...(p.corpo ? { corpo: p.corpo } : {}), ...(p.ato ? { ato: p.ato } : {}), desde: p.desde ?? null,
|
|
428
|
+
alternativas: p.letras.map((letra, i) => ({ letra: letra, texto: p.alternativas[i],
|
|
429
|
+
consequencia: p.consequencias[i], ...(recomendada?.letra === letra ? { recomendada: { porque: recomendada.porque } } : {}) })),
|
|
430
|
+
}];
|
|
431
|
+
});
|
|
432
|
+
const aviso = semRecomendada.length
|
|
433
|
+
? [`Pergunta${semRecomendada.length === 1 ? '' : 's'} ${semRecomendada.join(', ')}: responda pelo número e a letra, por exemplo ${semRecomendada[0]}a.`] : [];
|
|
434
|
+
if (!perguntas.length)
|
|
435
|
+
return aviso.join('\n') || 'Nenhuma pergunta para reenviar agora.';
|
|
436
|
+
return [(0, hitl_lote_1.textoDoLote)({ contrato: 'ork.hitl-lote/v1', perguntas, restantes: 0, recusadas: [], abertas: 0 }, { canal, quando }), ...aviso].join('\n');
|
|
437
|
+
}
|
|
438
|
+
/** A recomendada de uma pergunta servida, relida do pedido exato que saiu com o numero. */
|
|
439
|
+
function recomendadaDoRegistro(raiz, p) {
|
|
440
|
+
try {
|
|
441
|
+
const evento = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, p.thread))
|
|
442
|
+
.find(e => e.tipo === 'hitl_requested' && e.pedido?.id === p.pedidoId);
|
|
443
|
+
const pergunta = (0, hitl_lote_1.perguntaDoPedido)(evento.pedido, p.numero);
|
|
444
|
+
if ('motivo' in pergunta)
|
|
445
|
+
return undefined;
|
|
446
|
+
const r = pergunta.alternativas.find(a => a.recomendada);
|
|
447
|
+
return r ? { letra: r.letra, porque: r.recomendada.porque } : undefined;
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// RM-048 (D4): a linha que o dono recebeu vale enquanto o gate esperar a mesma pergunta.
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
/**
|
|
457
|
+
* O pedido que recebe a resposta: o proprio, quando ainda esta aberto ou ja foi respondido (ai o
|
|
458
|
+
* caminho de sempre diz "repetida" ou "divergente"), ou o renovado, quando venceu sem resposta.
|
|
459
|
+
*/
|
|
460
|
+
function pedidoParaResponder(raiz, pedido, quando) {
|
|
461
|
+
const respondido = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, pedido.thread))
|
|
462
|
+
.some(e => ['human_gate', 'session_answered'].includes(e.tipo) && e.pedidoId === pedido.id);
|
|
463
|
+
if (respondido || (0, hitl_contract_1.estadoDoPedido)(pedido, quando) === 'aberto')
|
|
464
|
+
return { vigente: pedido };
|
|
465
|
+
return { vigente: (0, hitl_gates_1.renovarPedidoDoGate)(raiz, pedido, quando), renovadoDe: pedido.id };
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* As threads abertas cujo ultimo pedido de gate com este codigo existe. Codigo que casa com mais
|
|
469
|
+
* de uma thread e devolvido inteiro: quem chama pergunta de volta, nunca escolhe (R3).
|
|
470
|
+
*/
|
|
471
|
+
function pedidosComCodigo(raiz, codigo) {
|
|
472
|
+
const alvo = codigo.trim().toUpperCase(), achados = [];
|
|
473
|
+
for (const id of (0, thread_1.listarIds)(raiz)) {
|
|
474
|
+
try {
|
|
475
|
+
if ((0, thread_1.lerThread)(raiz, id).status === 'fechada')
|
|
476
|
+
continue;
|
|
477
|
+
const eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, id));
|
|
478
|
+
const ultimo = eventos.filter(e => e.tipo === 'hitl_requested').map(e => e.pedido)
|
|
479
|
+
.filter(q => (0, hitl_contract_1.ehV2)(q) && q.classe === 'pergunta' && q.alvo.tipo === 'gate' && String(q.codigo).toUpperCase() === alvo).at(-1);
|
|
480
|
+
if (ultimo)
|
|
481
|
+
achados.push({ thread: id, pedido: ultimo, eventos });
|
|
482
|
+
}
|
|
483
|
+
catch { /* thread ilegivel nao responde por codigo */ }
|
|
484
|
+
}
|
|
485
|
+
return achados;
|
|
486
|
+
}
|
|
487
|
+
/** A1 do CHECK: os codigos curtos em uso agora (gates abertos e o resumo), para a nota nao repetir. */
|
|
488
|
+
function codigosEmUso(raiz, estadoDir) {
|
|
489
|
+
const codigos = [];
|
|
490
|
+
const estado = (() => { try {
|
|
491
|
+
return (0, pulse_consentimento_1.lerConsentimento)(raiz, estadoDir);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return undefined;
|
|
495
|
+
} })();
|
|
496
|
+
if (estado)
|
|
497
|
+
codigos.push(estado.pedido.codigo);
|
|
498
|
+
for (const id of (0, thread_1.listarIds)(raiz)) {
|
|
499
|
+
try {
|
|
500
|
+
if ((0, thread_1.lerThread)(raiz, id).status === 'fechada')
|
|
501
|
+
continue;
|
|
502
|
+
for (const e of (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, id))) {
|
|
503
|
+
const c = e.pedido?.codigo;
|
|
504
|
+
if (e.tipo === 'hitl_requested' && typeof c === 'string')
|
|
505
|
+
codigos.push(c);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch { /* thread ilegivel nao contribui */ }
|
|
509
|
+
}
|
|
510
|
+
return codigos;
|
|
511
|
+
}
|
|
512
|
+
/** O bloco de evidencia a um pedido de distancia (item 4): artefato, claims, riscos e diff. */
|
|
513
|
+
function textoDoDetalhe(raiz, pedido, chave, canal) {
|
|
514
|
+
const a = (0, hitl_presentation_1.apresentarHitl)(raiz, pedido.thread, pedido.profundidade);
|
|
515
|
+
const tg = canal === 'telegram';
|
|
516
|
+
return [
|
|
517
|
+
`${tg ? '🔎 ' : ''}Evidências de ${chave} (${pedido.thread} · ${pedido.fase})`,
|
|
518
|
+
...(a.artefato ? ['', 'Artefato:', a.artefato] : []),
|
|
519
|
+
'', 'Claims:', a.claims, '', 'Riscos:', a.riscos,
|
|
520
|
+
...(a.diff ? ['', 'Diff:', a.diff] : []),
|
|
521
|
+
].join('\n').slice(0, hitl_lote_1.TETO_DA_MENSAGEM);
|
|
522
|
+
}
|
|
523
|
+
/** As alternativas reais em uma linha, para devolver a pergunta sem chutar veredito. */
|
|
524
|
+
function opcoesReais(pedido, prefixo) {
|
|
525
|
+
return (0, hitl_contract_1.escolhasDoPedido)(pedido).map(o => `${prefixo}${(0, hitl_contract_1.chaveDaEscolha)(pedido, o.numero)} (${o.texto})`).join(', ');
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* "DE6H a": a resposta a um gate pelo codigo curto que o dono recebeu, sem numero de lote e sem
|
|
529
|
+
* identificador longo. A prova e a do endereco do pulse, a mesma do lote: o codigo esta DENTRO do
|
|
530
|
+
* texto assinado, e o rastro grava o que o nucleo traduziu. Devolve `undefined` quando nenhum gate
|
|
531
|
+
* aberto usa o codigo, para o chamador dizer isso com as palavras dele.
|
|
532
|
+
*/
|
|
533
|
+
function responderPorCodigoDeGate(raiz, envelope, codigo, opcoes) {
|
|
534
|
+
const { quando, canal } = opcoes, tg = canal === 'telegram';
|
|
535
|
+
const achados = pedidosComCodigo(raiz, codigo);
|
|
536
|
+
if (!achados.length)
|
|
537
|
+
return undefined;
|
|
538
|
+
if (achados.length > 1) {
|
|
539
|
+
return resultado('codigo', `O código ${codigo} está em ${achados.length} threads (${achados.map(a => a.thread).join(', ')}). ` +
|
|
540
|
+
'Responda pelo número da pergunta no lote, por exemplo 1a.', { recusas: [{ motivo: 'codigo.ambiguo' }] });
|
|
541
|
+
}
|
|
542
|
+
const { thread, pedido, eventos } = achados[0];
|
|
543
|
+
// A3 do CHECK: a MESMA mensagem entregue de novo pelo gateway e repetida, nunca renova nem grava.
|
|
544
|
+
const mesma = eventos.find(e => e.tipo === 'human_gate' && e.mensagem === envelope.mensagem);
|
|
545
|
+
if (mesma) {
|
|
546
|
+
return resultado('codigo', `${codigo}: esta mensagem já estava registrada (${String(mesma.estado)}).`, { repetida: true, registradas: [{ numero: 0, thread, letra: '', estado: String(mesma.estado), repetida: true }] });
|
|
547
|
+
}
|
|
548
|
+
const resto = envelope.resposta.trim().slice(codigo.length).trim();
|
|
549
|
+
if (/^(detalhes?|evid[eê]ncias?|\?)[.!]?$/i.test(resto))
|
|
550
|
+
return resultado('codigo', textoDoDetalhe(raiz, pedido, codigo, canal));
|
|
551
|
+
const anterior = eventos.find(e => e.tipo === 'human_gate' && e.pedidoId === pedido.id);
|
|
552
|
+
if (anterior && anterior.estado !== 'aguardando') {
|
|
553
|
+
return resultado('codigo', `${tg ? '⚠️ ' : '! '}${codigo} já foi respondido (${String(anterior.estado)}) às ` +
|
|
554
|
+
`${(0, horario_1.formatarHora)(anterior.ts, { agora: quando })}; a resposta não muda.`, { recusas: [{ motivo: 'ja-respondido' }] });
|
|
555
|
+
}
|
|
556
|
+
const trecho = (0, hitl_texto_livre_1.lerTrecho)(resto);
|
|
557
|
+
const r = trecho ? (0, hitl_texto_livre_1.resolverTrecho)(trecho, (0, hitl_contract_1.escolhasDoPedido)(pedido).map(o => o.acao), (0, hitl_contract_1.escolhasDoPedido)(pedido).map(o => (0, hitl_contract_1.chaveDaEscolha)(pedido, o.numero)))
|
|
558
|
+
: { tipo: 'nenhuma' };
|
|
559
|
+
if (r.tipo === 'detalhe')
|
|
560
|
+
return resultado('codigo', textoDoDetalhe(raiz, pedido, codigo, canal));
|
|
561
|
+
if (r.tipo !== 'escolha') {
|
|
562
|
+
return resultado('codigo', `${r.tipo === 'ambigua' ? `"${resto.slice(0, 40)}" serve para mais de uma alternativa de ${codigo}` : `Não entendi "${resto.slice(0, 40)}" para ${codigo}`}; ` +
|
|
563
|
+
`nada foi registrado. Responda com uma destas: ${opcoesReais(pedido, `${codigo} `)}.`, { recusas: [{ motivo: r.tipo === 'ambigua' ? 'ambigua' : 'nao-entendida' }] });
|
|
564
|
+
}
|
|
565
|
+
// D2: com mais de um pedido aberto na mesma thread, palavra nao registra; letra continua.
|
|
566
|
+
if (trecho.tipo === 'intencao') {
|
|
567
|
+
const abertos = pedidosAbertosDaThread(raiz, thread, quando);
|
|
568
|
+
if (abertos > 1) {
|
|
569
|
+
return resultado('codigo', `A thread de ${codigo} tem ${abertos} pedidos abertos; resposta por palavra não registra. ` +
|
|
570
|
+
`Responda com a letra: ${opcoesReais(pedido, `${codigo} `)}.`, { recusas: [{ motivo: 'varios-pedidos-na-thread' }] });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return registrarPorCodigo(raiz, envelope, { thread, pedido, codigo, indice: r.indice, quando, canal,
|
|
574
|
+
rastroExtra: trecho.tipo === 'intencao' ? { textoLivre: trecho.palavra } : {} });
|
|
575
|
+
}
|
|
576
|
+
/** Registra a escolha num pedido achado pelo codigo, renovando-o se venceu. Nao escolhe nada. */
|
|
577
|
+
function registrarPorCodigo(raiz, envelope, e) {
|
|
578
|
+
const tg = e.canal === 'telegram';
|
|
579
|
+
let vigente, renovadoDe;
|
|
580
|
+
try {
|
|
581
|
+
const anterior = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, e.thread)).find(x => x.tipo === 'human_gate' && x.pedidoId === e.pedido.id);
|
|
582
|
+
// Respondido com "continuar esperando", o gate ainda espera: a nova resposta vai ao renovado.
|
|
583
|
+
({ vigente, renovadoDe } = anterior && anterior.estado === 'aguardando'
|
|
584
|
+
? { vigente: (0, hitl_gates_1.renovarPedidoDoGate)(raiz, e.pedido, e.quando), renovadoDe: e.pedido.id }
|
|
585
|
+
: pedidoParaResponder(raiz, e.pedido, e.quando));
|
|
586
|
+
}
|
|
587
|
+
catch (erro) {
|
|
588
|
+
const m = erro.message;
|
|
589
|
+
if (m.includes('a pergunta mudou')) {
|
|
590
|
+
// A pergunta de agora e outra, com outro codigo: ela e aberta e mostrada, nunca respondida.
|
|
591
|
+
try {
|
|
592
|
+
const agora = (0, hitl_gates_1.abrirPedidoGate)(raiz, e.thread, (0, hitl_contract_1.motivoDoPedido)(e.pedido), e.quando);
|
|
593
|
+
const novo = agora.codigo;
|
|
594
|
+
const texto = typeof novo === 'string' ? (0, hitl_curto_1.textoDoPedidoCurto)((0, hitl_curto_1.montarPedidoCurto)((0, hitl_curto_1.entradaDoPedido)(agora), { quando: e.quando, responder: { tipo: 'codigo', codigo: novo } }), e.canal) : '';
|
|
595
|
+
return resultado('codigo', [`${tg ? '⚠️ ' : '! '}A pergunta de ${e.codigo} mudou desde que saiu; nada foi registrado. A de agora:`, '', texto].join('\n'), { recusas: [{ motivo: 'pergunta-mudou' }] });
|
|
596
|
+
}
|
|
597
|
+
catch { /* cai na frase de baixo */ }
|
|
598
|
+
}
|
|
599
|
+
return resultado('codigo', m.includes('o gate já foi respondido')
|
|
600
|
+
? `${tg ? '⚠️ ' : '! '}${e.codigo} já foi respondido por outra linha; a resposta não muda.`
|
|
601
|
+
: `${tg ? '⚠️ ' : '! '}${e.codigo} já não espera você: a fase mudou desde que a pergunta saiu. Nada foi registrado.`, { recusas: [{ motivo: 'pedido-antigo' }] });
|
|
602
|
+
}
|
|
603
|
+
const chave = (0, hitl_contract_1.chaveDaEscolha)(vigente, e.indice + 1);
|
|
604
|
+
const derivada = { ...envelope, resposta: chave };
|
|
605
|
+
const assinado = { alvo: pulse_consentimento_1.ALVO_DO_PULSE, endereco: pulse_consentimento_1.ENDERECO_DA_RESPOSTA, envelope,
|
|
606
|
+
rastro: { contrato: exports.CONTRATO_RESPOSTA_DO_PULSE, codigo: e.codigo, letra: chave, respostaDoDonoSha256: sha(envelope.resposta),
|
|
607
|
+
...(renovadoDe ? { renovadoDe } : {}), ...e.rastroExtra } };
|
|
608
|
+
try {
|
|
609
|
+
const r = (0, hitl_gates_1.responderGate)(raiz, e.thread, vigente.id, derivada, e.quando, assinado);
|
|
610
|
+
const o = (0, hitl_contract_1.escolhasDoPedido)(vigente)[e.indice];
|
|
611
|
+
const consequencia = (0, hitl_contract_1.ehV2)(vigente) && vigente.classe === 'pergunta' ? vigente.alternativas[e.indice]?.consequencia : undefined;
|
|
612
|
+
return resultado('codigo', `${tg ? '✅ ' : ''}${e.codigo} → ${chave}) ${o.texto}${consequencia ? `: ${consequencia}` : ''}.`, { registradas: [{ numero: 0, thread: e.thread, letra: chave, estado: r.estado, repetida: r.repetida }], repetida: r.repetida });
|
|
613
|
+
}
|
|
614
|
+
catch (erro) {
|
|
615
|
+
const m = erro.message;
|
|
616
|
+
const motivo = m.includes('ocupado') ? `a thread de ${e.codigo} estava ocupada; mande de novo em um minuto.`
|
|
617
|
+
: m.includes('proveniência') || m.includes('não autenticada') ? `não consegui provar a origem da resposta a tempo; mande ${e.codigo} ${chave} de novo.`
|
|
618
|
+
: m.includes('já respondido') ? `${e.codigo} já tinha resposta registrada; a resposta não muda.`
|
|
619
|
+
: m.includes('pedido antigo') ? `${e.codigo} já não espera você: a fase mudou desde que a pergunta saiu. Nada foi registrado.`
|
|
620
|
+
: `não consegui registrar ${e.codigo}: ${m.slice(0, 120)}`;
|
|
621
|
+
return resultado('codigo', `${tg ? '⚠️ ' : '! '}${motivo}`, { recusas: [{ motivo }] });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
// RM-048 (item 2): o texto livre, quando e inequivoco.
|
|
626
|
+
// ---------------------------------------------------------------------------
|
|
627
|
+
/**
|
|
628
|
+
* Quantos pedidos (gate ou sessao) estao abertos agora nesta thread: sem resposta, dentro do
|
|
629
|
+
* prazo e no contexto corrente. D2: com mais de um, palavra nao registra.
|
|
630
|
+
*/
|
|
631
|
+
function pedidosAbertosDaThread(raiz, thread, quando) {
|
|
632
|
+
const t = (0, thread_1.lerThread)(raiz, thread), eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, thread));
|
|
633
|
+
const contexto = (0, hitl_gates_1.contextoHitlDosEventos)(t, eventos);
|
|
634
|
+
const respondidos = new Set(eventos.filter(e => ['human_gate', 'session_answered'].includes(e.tipo)).map(e => String(e.pedidoId)));
|
|
635
|
+
const abertos = new Set();
|
|
636
|
+
for (const e of eventos) {
|
|
637
|
+
if (e.tipo !== 'hitl_requested' || e.contexto !== contexto)
|
|
638
|
+
continue;
|
|
639
|
+
const q = e.pedido;
|
|
640
|
+
try {
|
|
641
|
+
if (!(0, hitl_contract_1.alvoDoPedido)(q) || respondidos.has(q.id) || (0, hitl_contract_1.estadoDoPedido)(q, quando) !== 'aberto')
|
|
642
|
+
continue;
|
|
643
|
+
abertos.add(q.id);
|
|
644
|
+
}
|
|
645
|
+
catch { /* pedido invalido nao conta */ }
|
|
646
|
+
}
|
|
647
|
+
return abertos.size;
|
|
648
|
+
}
|
|
649
|
+
/** O pedido exato que saiu com o numero, relido do ledger. */
|
|
650
|
+
function pedidoDaServida(raiz, p) {
|
|
651
|
+
try {
|
|
652
|
+
const evento = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, p.thread))
|
|
653
|
+
.find(e => e.tipo === 'hitl_requested' && e.pedido?.id === p.pedidoId);
|
|
654
|
+
(0, hitl_contract_1.validarPedidoHitl)(evento?.pedido);
|
|
655
|
+
return evento.pedido;
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Traduz uma palavra numa letra para a pergunta servida, ou devolve por que nao da. Nunca escolhe
|
|
663
|
+
* pelo dono: ambigua, fora do vocabulario ou com mais de um pedido aberto na thread volta como
|
|
664
|
+
* pergunta, com as letras reais.
|
|
665
|
+
*/
|
|
666
|
+
function letraDaPalavra(raiz, p, trecho, quando) {
|
|
667
|
+
const pedido = pedidoDaServida(raiz, p);
|
|
668
|
+
if (!pedido)
|
|
669
|
+
return { recusa: `não consegui ler a pergunta ${p.numero}; responda com o número e a letra.` };
|
|
670
|
+
const acoes = (0, hitl_contract_1.escolhasDoPedido)(pedido).map(o => o.acao);
|
|
671
|
+
const r = (0, hitl_texto_livre_1.resolverTrecho)(trecho, acoes, p.letras);
|
|
672
|
+
const reais = p.letras.map((l, i) => `${p.numero}${l} (${p.alternativas[i]})`).join(', ');
|
|
673
|
+
if (r.tipo === 'detalhe')
|
|
674
|
+
return { detalhe: pedido };
|
|
675
|
+
if (r.tipo === 'ambigua')
|
|
676
|
+
return { recusa: `para a pergunta ${p.numero}, "${trecho.tipo === 'intencao' ? trecho.palavra : ''}" serve para mais de uma alternativa; mande uma: ${reais}.` };
|
|
677
|
+
if (r.tipo === 'nenhuma')
|
|
678
|
+
return { recusa: `a pergunta ${p.numero} não tem alternativa para essa resposta; mande uma destas: ${reais}.` };
|
|
679
|
+
if (trecho.tipo === 'intencao') {
|
|
680
|
+
const abertos = pedidosAbertosDaThread(raiz, p.thread, quando);
|
|
681
|
+
if (abertos > 1)
|
|
682
|
+
return { recusa: `a thread da pergunta ${p.numero} tem ${abertos} pedidos abertos; resposta por palavra não registra. Mande a letra: ${reais}.` };
|
|
683
|
+
return { letra: p.letras[r.indice], palavra: trecho.palavra };
|
|
684
|
+
}
|
|
685
|
+
return { letra: p.letras[r.indice] };
|
|
686
|
+
}
|
|
687
|
+
/** "1. B, 2. aprovo, 3 detalhes": cada item vira letra, detalhe ou recusa, e as letras seguem o lote. */
|
|
688
|
+
function responderALista(raiz, envelope, itens, opcoes) {
|
|
689
|
+
const { quando, canal, estadoDir } = opcoes, tg = canal === 'telegram';
|
|
690
|
+
const servido = lerLoteServido(raiz, estadoDir);
|
|
691
|
+
const escolhas = [], avisos = [], detalhes = [];
|
|
692
|
+
for (const { numero, trecho } of itens) {
|
|
693
|
+
const p = servido.perguntas.find(q => q.numero === numero);
|
|
694
|
+
if (trecho.tipo === 'letra' || !p) {
|
|
695
|
+
escolhas.push({ numero, letra: trecho.tipo === 'letra' ? trecho.letra : '?' });
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const r = letraDaPalavra(raiz, p, trecho, quando);
|
|
699
|
+
if ('detalhe' in r)
|
|
700
|
+
detalhes.push(textoDoDetalhe(raiz, r.detalhe, String(numero), canal));
|
|
701
|
+
else if ('recusa' in r)
|
|
702
|
+
avisos.push(`${tg ? '⚠️ ' : '! '}${r.recusa}`);
|
|
703
|
+
else
|
|
704
|
+
escolhas.push({ numero, letra: r.letra, ...(r.palavra ? { palavra: r.palavra } : {}) });
|
|
705
|
+
}
|
|
706
|
+
if (!escolhas.length) {
|
|
707
|
+
return resultado('lote', [...avisos, ...detalhes].join('\n\n') || 'Nada para registrar.', { recusas: avisos.map(motivo => ({ motivo })) });
|
|
708
|
+
}
|
|
709
|
+
const r = responderAsPerguntas(raiz, envelope, escolhas, { quando, canal, estadoDir, avisos });
|
|
710
|
+
return detalhes.length ? { ...r, mensagem: [r.mensagem, ...detalhes].join('\n\n').slice(0, hitl_lote_1.TETO_DA_MENSAGEM) } : r;
|
|
711
|
+
}
|
|
712
|
+
function abertosDoDono(raiz, quando, estadoDir) {
|
|
713
|
+
const janela = hitl_texto_livre_1.JANELA_DO_TEXTO_LIVRE_MIN * 60000, agora = Date.parse(quando);
|
|
714
|
+
const abertos = [];
|
|
715
|
+
const estado = (0, pulse_consentimento_1.lerConsentimento)(raiz, estadoDir);
|
|
716
|
+
if (estado && !estado.respondido && estado.pedido.candidatos.length) {
|
|
717
|
+
abertos.push({ tipo: 'consentimento', codigo: estado.pedido.codigo, rotulo: `${estado.pedido.codigo} a (receber as perguntas)`,
|
|
718
|
+
recente: agora - Date.parse(estado.pedido.criadoEm) < janela });
|
|
719
|
+
}
|
|
720
|
+
// A mesma pergunta (mesma thread e mesmo alvo) conta uma vez, servida ou nao, vencida ou nao.
|
|
721
|
+
const chave = (q) => { const a = (0, hitl_contract_1.alvoDoPedido)(q); return `${q.thread}|${a?.tipo === 'session' ? `s:${a.sessionId}` : `g:${q.fase}`}`; };
|
|
722
|
+
// Os gates que o resumo aberto oferece sao o que o "sim" pede: o consentimento os representa.
|
|
723
|
+
const vistas = new Set(estado && !estado.respondido
|
|
724
|
+
? estado.pedido.candidatos.map(c => `${c.thread}|g:${c.fase ?? ''}`) : []);
|
|
725
|
+
for (const p of lerLoteServido(raiz, estadoDir).perguntas.filter(q => perguntaEmAberto(q, quando))) {
|
|
726
|
+
const pedido = pedidoDaServida(raiz, p);
|
|
727
|
+
if (pedido)
|
|
728
|
+
vistas.add(chave(pedido));
|
|
729
|
+
abertos.push({ tipo: 'pergunta', servida: p, rotulo: `${p.numero} com a letra (${[p.thread, p.fase].filter(Boolean).join(' · ')})`,
|
|
730
|
+
recente: agora - Date.parse(p.servidaEm) < janela });
|
|
731
|
+
}
|
|
732
|
+
for (const id of (0, thread_1.listarIds)(raiz)) {
|
|
733
|
+
try {
|
|
734
|
+
const t = (0, thread_1.lerThread)(raiz, id);
|
|
735
|
+
if (t.status === 'fechada')
|
|
736
|
+
continue;
|
|
737
|
+
const eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, id));
|
|
738
|
+
const contexto = (0, hitl_gates_1.contextoHitlDosEventos)(t, eventos);
|
|
739
|
+
const respondidos = new Set(eventos.filter(e => ['human_gate', 'session_answered'].includes(e.tipo)).map(e => String(e.pedidoId)));
|
|
740
|
+
for (const e of eventos) {
|
|
741
|
+
if (e.tipo !== 'hitl_requested' || e.contexto !== contexto)
|
|
742
|
+
continue;
|
|
743
|
+
const q = e.pedido;
|
|
744
|
+
try {
|
|
745
|
+
if (!(0, hitl_contract_1.alvoDoPedido)(q) || respondidos.has(q.id) || (0, hitl_contract_1.estadoDoPedido)(q, quando) !== 'aberto' || (0, hitl_classificacao_1.quemDecide)((0, hitl_contract_1.motivoDoPedido)(q)) !== 'dono')
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if (vistas.has(chave(q)))
|
|
752
|
+
continue;
|
|
753
|
+
vistas.add(chave(q));
|
|
754
|
+
const codigo = (0, hitl_contract_1.ehV2)(q) && q.classe === 'pergunta' ? q.codigo : undefined;
|
|
755
|
+
abertos.push({ tipo: 'pedido', recente: false, ...(codigo ? { codigo } : {}),
|
|
756
|
+
rotulo: codigo ? `${codigo} a (${q.thread} · ${q.fase})` : `${q.thread} · ${q.fase}` });
|
|
757
|
+
}
|
|
758
|
+
// Segunda passada do CHECK: o gate cujo pedido venceu continua esperando o dono, e a linha
|
|
759
|
+
// dele ("DE6H a") nao vence (D4). Ele conta mesmo sem pedido aberto agora.
|
|
760
|
+
const gate = `${id}|g:${t.faseAtual}`;
|
|
761
|
+
if (!vistas.has(gate)) {
|
|
762
|
+
const espera = (0, roadmap_status_1.esperaDoDono)(raiz, t, quando);
|
|
763
|
+
if (espera && !abertos.some(a => a.servida?.thread === id)) {
|
|
764
|
+
vistas.add(gate);
|
|
765
|
+
abertos.push({ tipo: 'pedido', recente: false, ...(espera.codigo ? { codigo: espera.codigo } : {}),
|
|
766
|
+
rotulo: espera.codigo ? `${espera.codigo} a (${id} · ${t.faseAtual})` : `${id} · ${t.faseAtual}` });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
catch { /* thread ilegivel nao entra na conta; a palavra solta so fica mais cautelosa */ }
|
|
771
|
+
}
|
|
772
|
+
return abertos;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* "aprovo", "sim", "a", "1": a palavra solta. Registra quando ha UMA pergunta recente e ela nao e
|
|
776
|
+
* sem volta; senao devolve a pergunta com as opcoes reais. A prova ja foi conferida por quem chama.
|
|
777
|
+
*/
|
|
778
|
+
function responderLivre(raiz, envelope, trecho, opcoes) {
|
|
779
|
+
const { quando, canal, estadoDir } = opcoes, tg = canal === 'telegram';
|
|
780
|
+
const abertos = abertosDoDono(raiz, quando, estadoDir);
|
|
781
|
+
const volta = (mensagem, motivo) => resultado('nao-entendida', mensagem, { recusas: [{ motivo }] });
|
|
782
|
+
if (!abertos.length) {
|
|
783
|
+
return volta('Não há pergunta aberta para responder só com uma palavra; nada foi registrado. ' +
|
|
784
|
+
'Responda com o número e a letra (por exemplo 1a) ou com o código da pergunta (por exemplo DE6H a).', 'sem-pergunta-recente');
|
|
785
|
+
}
|
|
786
|
+
if (abertos.length > 1) {
|
|
787
|
+
const lista = abertos.slice(0, 5).map(a => a.rotulo).join('; ') + (abertos.length > 5 ? `; e mais ${abertos.length - 5}` : '');
|
|
788
|
+
return volta(`Há ${abertos.length} perguntas esperando você, e uma palavra só não diz qual; nada foi registrado. ` +
|
|
789
|
+
`Responda pelo número ou pelo código: ${lista}.`, 'varias-perguntas');
|
|
790
|
+
}
|
|
791
|
+
const [unico] = abertos;
|
|
792
|
+
if (!unico.recente) {
|
|
793
|
+
return volta(`A pergunta que espera você não acabou de sair por aqui; palavra solta não registra. Responda: ${unico.rotulo}.`, 'sem-pergunta-recente');
|
|
794
|
+
}
|
|
795
|
+
if (unico.tipo === 'consentimento') {
|
|
796
|
+
// O resumo pergunta sim ou nao: a palavra vira a letra, e o caminho e o do codigo, com a prova de sempre.
|
|
797
|
+
return responderAoResumo(raiz, envelope, unico.codigo, { quando, canal, estadoDir, palavra: trecho });
|
|
798
|
+
}
|
|
799
|
+
const p = unico.servida;
|
|
800
|
+
const pedido = pedidoDaServida(raiz, p);
|
|
801
|
+
if (pedido && (0, hitl_contract_1.ehV2)(pedido) && pedido.classe === 'pergunta' && pedido.irreversivel) {
|
|
802
|
+
return volta(`${tg ? '🔒 ' : '! '}A pergunta ${p.numero} é sem volta; palavra solta não registra. Responda com o número e a letra: ` +
|
|
803
|
+
`${p.letras.map((l, i) => `${p.numero}${l} (${p.alternativas[i]})`).join(', ')}.`, 'irreversivel');
|
|
804
|
+
}
|
|
805
|
+
// D2: a resposta solta nao diz a qual pedido se refere; com mais de um aberto na thread, nem
|
|
806
|
+
// letra nem digito soltos registram. Numerados ("1a") e por codigo ("DE6H a") continuam valendo.
|
|
807
|
+
const naThread = pedidosAbertosDaThread(raiz, p.thread, quando);
|
|
808
|
+
if (naThread > 1) {
|
|
809
|
+
return volta(`${tg ? '⚠️ ' : '! '}a thread da pergunta ${p.numero} tem ${naThread} pedidos abertos; resposta solta não registra. ` +
|
|
810
|
+
`Mande o número e a letra: ${p.letras.map((l, i) => `${p.numero}${l} (${p.alternativas[i]})`).join(', ')}.`, 'varios-pedidos-na-thread');
|
|
811
|
+
}
|
|
812
|
+
const r = letraDaPalavra(raiz, p, trecho, quando);
|
|
813
|
+
if ('detalhe' in r)
|
|
814
|
+
return resultado('lote', textoDoDetalhe(raiz, r.detalhe, String(p.numero), canal));
|
|
815
|
+
if ('recusa' in r)
|
|
816
|
+
return volta(`${tg ? '⚠️ ' : '! '}${r.recusa}`, 'nao-registrou');
|
|
817
|
+
return responderAsPerguntas(raiz, envelope, [{ numero: p.numero, letra: r.letra, palavra: r.palavra ?? normalizarTrecho(trecho) }], { quando, canal, estadoDir });
|
|
818
|
+
}
|
|
819
|
+
const normalizarTrecho = (t) => t.tipo === 'letra' ? t.letra : t.tipo === 'digito' ? String(t.numero) : t.palavra;
|
|
820
|
+
exports.CONTRATO_ESCUTA = 'ork.pulse-escuta/v1';
|
|
821
|
+
function arquivoDaEscuta(raiz, estadoDir) {
|
|
822
|
+
return path.join(estadoDir ?? path.join((0, estado_thread_1.raizDoEstado)(raiz), '.orkastery', 'monitor'), 'pulse-escuta.json');
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* D3: a janela da palavra solta, para o adaptador decidir a ROTA de "sim", "ok", "a". Nao e prova:
|
|
826
|
+
* a mensagem interceptada ainda passa pela assinatura do ingresso e por esta mesma regra no nucleo.
|
|
827
|
+
* Janela fechada (ou arquivo ausente), a palavra vai ao assistente, como sempre foi.
|
|
828
|
+
*/
|
|
829
|
+
function atualizarEscuta(raiz, quando, estadoDir) {
|
|
830
|
+
const janela = hitl_texto_livre_1.JANELA_DO_TEXTO_LIVRE_MIN * 60000;
|
|
831
|
+
const limites = [];
|
|
832
|
+
const estado = (0, pulse_consentimento_1.lerConsentimento)(raiz, estadoDir);
|
|
833
|
+
if (estado && !estado.respondido && estado.pedido.candidatos.length)
|
|
834
|
+
limites.push(Date.parse(estado.pedido.criadoEm) + janela);
|
|
835
|
+
for (const p of lerLoteServido(raiz, estadoDir).perguntas)
|
|
836
|
+
if (perguntaEmAberto(p, quando))
|
|
837
|
+
limites.push(Date.parse(p.servidaEm) + janela);
|
|
838
|
+
const ate = limites.filter(l => l > Date.parse(quando)).sort((a, b) => b - a)[0];
|
|
839
|
+
const escuta = { contrato: exports.CONTRATO_ESCUTA, livreAte: ate ? new Date(ate).toISOString() : null, atualizadoEm: quando };
|
|
840
|
+
const arquivo = arquivoDaEscuta(raiz, estadoDir);
|
|
841
|
+
fs.mkdirSync(path.dirname(arquivo), { recursive: true });
|
|
842
|
+
const tmp = `${arquivo}.${process.pid}.tmp`;
|
|
843
|
+
fs.writeFileSync(tmp, JSON.stringify(escuta) + '\n', { mode: 0o600 });
|
|
844
|
+
fs.renameSync(tmp, arquivo);
|
|
845
|
+
return { livreAte: escuta.livreAte };
|
|
846
|
+
}
|
|
847
|
+
// ---------------------------------------------------------------------------
|
|
848
|
+
// RM-048 (item 8): a nota do MASTER e a ratificacao, pelo mesmo endereco assinado.
|
|
849
|
+
// ---------------------------------------------------------------------------
|
|
850
|
+
/**
|
|
851
|
+
* "K7QX 4 entregou o que pedi": a nota vai ao ledger com `por` = o remetente autenticado e com o
|
|
852
|
+
* recibo do canal. Quem chama ja conferiu a prova; aqui so se le o conteudo. Nota humana que ja
|
|
853
|
+
* existe nao e trocada por este caminho; a aceita por omissao e, porque a nota do dono sobrescreve.
|
|
854
|
+
*/
|
|
855
|
+
function responderNota(raiz, envelope, p, opcoes) {
|
|
856
|
+
const tg = opcoes.canal === 'telegram';
|
|
857
|
+
const nota = (0, master_nota_1.lerNota)(envelope.resposta.trim().slice(p.codigo.length));
|
|
858
|
+
if (!nota) {
|
|
859
|
+
return resultado('nota', `Para dar a nota de ${p.thread}, responda ${p.codigo} <0 a 5> <porquê>, por exemplo: ` +
|
|
860
|
+
`${p.codigo} 4 entregou o que pedi. Nada foi registrado.`, { recusas: [{ motivo: 'nota.formato' }] });
|
|
861
|
+
}
|
|
862
|
+
if ((0, master_nota_1.mensagemJaUsadaEmNota)(raiz, p.thread, envelope.mensagem)) {
|
|
863
|
+
return resultado('nota', `A nota de ${p.thread} desta mensagem já estava registrada.`, { repetida: true });
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
// A4 do CHECK: ler a nota que existe e gravar a nova sob o lock da thread, para uma nota humana
|
|
867
|
+
// dada no mesmo instante por outro caminho nao ser sobrescrita.
|
|
868
|
+
const ja = (0, hitl_gates_1.comLockHitl)(raiz, p.thread, () => {
|
|
869
|
+
const t = (0, thread_1.lerThread)(raiz, p.thread);
|
|
870
|
+
if (t.score && t.score.regime !== 'omissao' && (0, master_1.autoriaHumana)(t.score.avaliadoPor))
|
|
871
|
+
return t.score.valor;
|
|
872
|
+
const prova = (0, master_nota_1.reciboDoCanal)(raiz, envelope, { notaPedida: { contrato: master_nota_1.CONTRATO_PEDIDO_DE_NOTA, pedidoId: p.pedidoId, codigo: p.codigo },
|
|
873
|
+
enderecoAssinado: { alvo: pulse_consentimento_1.ALVO_DO_PULSE, endereco: pulse_consentimento_1.ENDERECO_DA_RESPOSTA, contrato: exports.CONTRATO_RESPOSTA_DO_PULSE, codigo: p.codigo,
|
|
874
|
+
respostaDoDonoSha256: sha(envelope.resposta) } }, opcoes.estadoDir);
|
|
875
|
+
(0, master_1.registrarMaster)(raiz, p.thread, { score: nota.score, justificativa: nota.justificativa, por: envelope.por, refazer: !!t.score, prova });
|
|
876
|
+
return undefined;
|
|
877
|
+
});
|
|
878
|
+
if (ja !== undefined) {
|
|
879
|
+
return resultado('nota', `${tg ? '⚠️ ' : '! '}${p.thread} já tem nota humana ${ja}/5; a nota não muda por aqui.`, { recusas: [{ motivo: 'nota.ja-dada' }] });
|
|
880
|
+
}
|
|
881
|
+
return resultado('nota', `${tg ? '🧾 ' : ''}Nota ${nota.score}/5 registrada para ${p.thread}, por ${envelope.por}. ` +
|
|
882
|
+
'O índice derivado do ledger continua ao lado.', { registradas: [{ numero: 0, thread: p.thread, letra: String(nota.score), estado: 'nota', repetida: false }] });
|
|
883
|
+
}
|
|
884
|
+
catch (e) {
|
|
885
|
+
return resultado('nota', `${tg ? '⚠️ ' : '! '}não consegui registrar a nota de ${p.thread}: ${e.message.slice(0, 160)}`, { recusas: [{ motivo: 'nota.recusada' }] });
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
/** O teclado do digest: a ratificacao da proposta, com o remetente autenticado e o recibo. */
|
|
889
|
+
function responderRatificacao(raiz, envelope, texto, opcoes) {
|
|
890
|
+
const tg = opcoes.canal === 'telegram';
|
|
891
|
+
const partes = texto.split(/\s+/);
|
|
892
|
+
const prova = () => (0, master_nota_1.reciboDoCanal)(raiz, envelope, { ratificacao: { contrato: master_nota_1.CONTRATO_PEDIDO_DE_NOTA, texto },
|
|
893
|
+
enderecoAssinado: { alvo: pulse_consentimento_1.ALVO_DO_PULSE, endereco: pulse_consentimento_1.ENDERECO_DA_RESPOSTA, contrato: exports.CONTRATO_RESPOSTA_DO_PULSE,
|
|
894
|
+
respostaDoDonoSha256: sha(envelope.resposta) } }, opcoes.estadoDir);
|
|
895
|
+
try {
|
|
896
|
+
let feitas;
|
|
897
|
+
if (partes[0].toLowerCase() === 'ratificar-lote' && partes.length === 3) {
|
|
898
|
+
feitas = (0, master_digest_1.responderLoteDigest)(raiz, ['ratificar-lote', partes[1], partes[2]].join(' '), envelope.por, prova())
|
|
899
|
+
.map(r => ({ thread: r.thread.id, score: r.masterLog.score }));
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
const classe = (0, master_1.parseClasse)(partes[3]);
|
|
903
|
+
if (partes[0].toLowerCase() !== 'ratificar' || partes.length !== 4 || !classe) {
|
|
904
|
+
return resultado('nota', 'Para ratificar, use a linha do teclado do digest: ratificar <thread> <assinatura> <classe>.', { recusas: [{ motivo: 'ratificacao.formato' }] });
|
|
905
|
+
}
|
|
906
|
+
feitas = (0, master_batch_1.ratificarBatch)(raiz, [{ thread: partes[1], assinatura: partes[2], classe }], envelope.por, prova())
|
|
907
|
+
.map(r => ({ thread: r.thread.id, score: r.masterLog.score }));
|
|
908
|
+
}
|
|
909
|
+
return resultado('nota', `${tg ? '🧾 ' : ''}Ratificado por ${envelope.por}: ${feitas.map(f => `${f.thread} ${f.score}/5`).join(', ')}.`, { registradas: feitas.map(f => ({ numero: 0, thread: f.thread, letra: String(f.score), estado: 'nota', repetida: false })) });
|
|
910
|
+
}
|
|
911
|
+
catch (e) {
|
|
912
|
+
return resultado('nota', `${tg ? '⚠️ ' : '! '}não consegui ratificar: ${e.message.slice(0, 160)}`, { recusas: [{ motivo: 'ratificacao.recusada' }] });
|
|
913
|
+
}
|
|
334
914
|
}
|
|
335
915
|
const EXEMPLO = 'Responda ao resumo com o código e a letra, por exemplo: P4EJ a. Às perguntas, com o número e a letra, por exemplo: 1a 2c. ' +
|
|
336
916
|
'Para trocar a cadência, mande só a tag: #OrkPulseOn, #OrkPulseOn-15m, -30m, -60m ou #OrkPulseOff.';
|
|
@@ -361,45 +941,73 @@ function responderPeloPulse(raiz, envelope, opcoes = {}) {
|
|
|
361
941
|
}
|
|
362
942
|
// A conversa do pulse tem dois escritores (a varredura e este receptor): um de cada vez.
|
|
363
943
|
const estadoDir = opcoes.estadoDir ?? path.join((0, estado_thread_1.raizDoEstado)(raiz), '.orkastery', 'monitor');
|
|
364
|
-
return (0, monitor_lock_1.comLockDaConversa)(estadoDir, () =>
|
|
365
|
-
|
|
366
|
-
|
|
944
|
+
return (0, monitor_lock_1.comLockDaConversa)(estadoDir, () => {
|
|
945
|
+
const r = (() => {
|
|
946
|
+
try {
|
|
947
|
+
return lido.forma === 'lote' ? responderAsPerguntas(raiz, envelope, lido.escolhas, { quando, canal, estadoDir })
|
|
948
|
+
: lido.forma === 'lista' ? responderALista(raiz, envelope, lido.itens, { quando, canal, estadoDir })
|
|
949
|
+
: lido.forma === 'livre' ? responderLivre(raiz, envelope, lido.trecho, { quando, canal, estadoDir })
|
|
950
|
+
: lido.forma === 'ratificacao' ? responderRatificacao(raiz, envelope, lido.texto, { canal, estadoDir })
|
|
951
|
+
: responderAoResumo(raiz, envelope, lido.codigo, { quando, canal, estadoDir });
|
|
952
|
+
}
|
|
953
|
+
catch (e) {
|
|
954
|
+
// S3 do CHECK: estado ilegivel ou falha inesperada vira resposta util, nunca silencio nem erro cru.
|
|
955
|
+
// O detalhe fica no stderr de quem chamou (o adaptador nao o repassa): caminho local e
|
|
956
|
+
// mensagem interna nao vao ao Telegram.
|
|
957
|
+
console.error(`pulse responder: ${e.message}`);
|
|
958
|
+
return resultado('nao-entendida', 'Não consegui concluir esta resposta agora (erro interno do núcleo). ' +
|
|
959
|
+
'Confira em ork pulse e mande de novo em um minuto.', { recusas: [{ motivo: 'erro-interno' }] });
|
|
960
|
+
}
|
|
961
|
+
})();
|
|
962
|
+
// RM-048 (D3): a janela da palavra solta acompanha o que ainda espera o dono agora.
|
|
963
|
+
try {
|
|
964
|
+
atualizarEscuta(raiz, quando, estadoDir);
|
|
965
|
+
}
|
|
966
|
+
catch { /* dica de rota; a prova nao depende dela */ }
|
|
967
|
+
return r;
|
|
968
|
+
});
|
|
367
969
|
}
|
|
368
970
|
function responderAoResumo(raiz, envelope, codigo, opcoes) {
|
|
369
971
|
const { quando, canal, estadoDir } = opcoes;
|
|
370
972
|
const estado = (0, pulse_consentimento_1.lerConsentimento)(raiz, estadoDir);
|
|
371
|
-
if (!estado)
|
|
372
|
-
return resultado('consentimento', 'Não há resumo esperando resposta agora. Quando houver pergunta para você, o resumo avisa.');
|
|
373
|
-
if (estado.pedido.codigo !== codigo) {
|
|
973
|
+
if (!estado || estado.pedido.codigo !== codigo) {
|
|
374
974
|
const servido = lerLoteServido(raiz, estadoDir);
|
|
375
975
|
const numeros = servido.porCodigo[codigo];
|
|
376
976
|
// Sim repetido a um codigo que ja serviu lote: devolve o MESMO lote, nunca um novo.
|
|
377
977
|
if (numeros?.length) {
|
|
378
978
|
// O dono pediu de novo, por outra mensagem: ele recebe o mesmo lote, e nao silencio.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return resultado('consentimento', loteDeNovo(servido, numeros, codigo, quando, canal, aberto), { resposta: 'sim', repetida: false });
|
|
979
|
+
// RM-048 (D5): o resumo mais recente nao vence para o sim; o codigo dele continua valendo.
|
|
980
|
+
const aberto = estado && !estado.respondido && estado.pedido.candidatos.length ? estado.pedido : undefined;
|
|
981
|
+
return resultado('consentimento', loteDeNovo(raiz, servido, numeros, codigo, quando, canal, aberto), { resposta: 'sim', repetida: false });
|
|
382
982
|
}
|
|
383
|
-
|
|
384
|
-
|
|
983
|
+
// RM-048 (item 8): o codigo de um pedido de nota do MASTER ("K7QX 4 entregou o que pedi").
|
|
984
|
+
// A1 do CHECK: se o mesmo codigo tambem e de um gate aberto, nada e escolhido pelo dono.
|
|
985
|
+
const nota = (0, master_nota_1.pedidoDeNotaDoCodigo)(raiz, codigo, estadoDir);
|
|
986
|
+
if (nota && pedidosComCodigo(raiz, codigo).length) {
|
|
987
|
+
return resultado('codigo', `O código ${codigo} é de uma nota e de uma pergunta ao mesmo tempo; nada foi registrado. ` +
|
|
988
|
+
'Responda a pergunta pelo número do lote e peça outro código de nota (ork master pedir).', { recusas: [{ motivo: 'codigo.ambiguo' }] });
|
|
989
|
+
}
|
|
990
|
+
// RM-048 (D4): o codigo curto de um gate ("DE6H a") responde ao gate, pelo mesmo endereco assinado.
|
|
991
|
+
const peloGate = responderPorCodigoDeGate(raiz, envelope, codigo, { quando, canal });
|
|
992
|
+
if (peloGate)
|
|
993
|
+
return peloGate;
|
|
994
|
+
if (nota)
|
|
995
|
+
return responderNota(raiz, envelope, nota, { canal, estadoDir });
|
|
996
|
+
if (!estado)
|
|
997
|
+
return resultado('consentimento', `Não reconheço o código ${codigo} agora: nenhum resumo ou pergunta aberta usa ele. ` +
|
|
998
|
+
'Quando houver pergunta para você, o resumo avisa.');
|
|
999
|
+
return resultado('consentimento', `O código ${codigo} não é o do resumo aberto nem de uma pergunta aberta. O resumo aberto usa ` +
|
|
1000
|
+
`${estado.pedido.codigo}: responda ${estado.pedido.codigo} a para receber as perguntas.`);
|
|
385
1001
|
}
|
|
386
1002
|
let r;
|
|
387
1003
|
try {
|
|
388
|
-
|
|
1004
|
+
// RM-048 (D5): o sim ao resumo mais recente vale depois do prazo; o lote reconfere cada gate.
|
|
1005
|
+
r = (0, pulse_consentimento_1.responderConsentimento)(raiz, { codigo, envelope, quando, estadoDir, aceitarVencido: true });
|
|
389
1006
|
}
|
|
390
1007
|
catch (e) {
|
|
391
1008
|
const m = e.message;
|
|
392
1009
|
if (!m.startsWith('consentimento do pulse: '))
|
|
393
1010
|
throw e;
|
|
394
|
-
if (m.includes('vencido')) {
|
|
395
|
-
// Vencido nao autoriza, e o dono nao fica sem caminho: o mesmo conjunto ganha codigo novo.
|
|
396
|
-
if (!estado.pedido.candidatos.length)
|
|
397
|
-
return resultado('consentimento', `Este resumo venceu às ${(0, horario_1.formatarHora)(estado.pedido.prazo, { agora: quando })}.`);
|
|
398
|
-
const novo = (0, pulse_consentimento_1.abrirConsentimento)(raiz, { quando, resumoSha256: estado.pedido.resumoSha256,
|
|
399
|
-
candidatos: estado.pedido.candidatos, prazoMin: pulse_consentimento_1.PRAZO_PADRAO_MIN, estadoDir });
|
|
400
|
-
return resultado('consentimento', `Este resumo venceu às ${(0, horario_1.formatarHora)(estado.pedido.prazo, { agora: quando })}. ` +
|
|
401
|
-
`Para receber as perguntas agora, responda ${novo.codigo} a.`);
|
|
402
|
-
}
|
|
403
1011
|
if (m.includes('divergente')) {
|
|
404
1012
|
const antes = estado.respondido;
|
|
405
1013
|
return resultado('consentimento', antes.resposta === 'sim'
|
|
@@ -417,7 +1025,7 @@ function responderAoResumo(raiz, envelope, codigo, opcoes) {
|
|
|
417
1025
|
`As ${n} perguntas continuam guardadas`}, e o próximo resumo lembra ${n === 1 ? 'dela' : 'delas'}.`, { resposta: 'nao', repetida: mesmaMensagem });
|
|
418
1026
|
}
|
|
419
1027
|
if (r.repetida && r.respondido.numeros) {
|
|
420
|
-
return resultado('consentimento', loteDeNovo(lerLoteServido(raiz, estadoDir), r.respondido.numeros, codigo, quando, canal), { resposta: 'sim', repetida: mesmaMensagem });
|
|
1028
|
+
return resultado('consentimento', loteDeNovo(raiz, lerLoteServido(raiz, estadoDir), r.respondido.numeros, codigo, quando, canal), { resposta: 'sim', repetida: mesmaMensagem });
|
|
421
1029
|
}
|
|
422
1030
|
const entregue = servirLote(raiz, { pedido: r.pedido, quando, canal, estadoDir });
|
|
423
1031
|
marcarLoteEntregueSeAindaFor(raiz, quando, estadoDir, r.pedido.id, entregue.numeros);
|
|
@@ -438,8 +1046,15 @@ function motivoParaODono(p, letra, erro, quando) {
|
|
|
438
1046
|
const m = erro.message, n = p.numero;
|
|
439
1047
|
if (m.includes('expirado'))
|
|
440
1048
|
return `a pergunta ${n} venceu às ${(0, horario_1.formatarHora)(p.prazo, { agora: quando })}; o próximo resumo a traz de novo se ela ainda esperar você.`;
|
|
1049
|
+
if (m.includes('o gate já foi respondido'))
|
|
1050
|
+
return `a pergunta ${n} já foi respondida por outra linha; a resposta não muda.`;
|
|
1051
|
+
if (m.includes('a pergunta mudou'))
|
|
1052
|
+
return `a pergunta ${n} mudou desde que saiu; nada foi registrado, e ela volta no próximo resumo com o texto de agora.`;
|
|
441
1053
|
if (m.includes('pedido antigo'))
|
|
442
1054
|
return `a pergunta ${n} já não espera você: a fase mudou desde que ela saiu.`;
|
|
1055
|
+
// RM-048 (D4): vencida, a renovacao so acontece se o gate ainda espera; senao, ela e historia.
|
|
1056
|
+
if (/pausa humana nesta fase|conclusão da fase|escalação tipada não comprovada/.test(m))
|
|
1057
|
+
return `a pergunta ${n} já não espera você.`;
|
|
443
1058
|
if (m.includes('já respondido'))
|
|
444
1059
|
return `a pergunta ${n} já tinha resposta registrada; a resposta não muda.`;
|
|
445
1060
|
if (m.includes('ocupado'))
|
|
@@ -462,7 +1077,7 @@ function responderAsPerguntas(raiz, envelope, escolhas, opcoes) {
|
|
|
462
1077
|
for (const e of escolhas)
|
|
463
1078
|
porNumero.set(e.numero, new Set([...(porNumero.get(e.numero) ?? []), e.letra]));
|
|
464
1079
|
const vistos = new Set();
|
|
465
|
-
for (const { numero, letra } of escolhas) {
|
|
1080
|
+
for (const { numero, letra, palavra } of escolhas) {
|
|
466
1081
|
if (vistos.has(numero))
|
|
467
1082
|
continue;
|
|
468
1083
|
vistos.add(numero);
|
|
@@ -475,7 +1090,7 @@ function responderAsPerguntas(raiz, envelope, escolhas, opcoes) {
|
|
|
475
1090
|
}
|
|
476
1091
|
const p = servido.perguntas.find(q => q.numero === numero);
|
|
477
1092
|
if (!p) {
|
|
478
|
-
const abertas = servido.perguntas.filter(q =>
|
|
1093
|
+
const abertas = servido.perguntas.filter(q => perguntaEmAberto(q, quando)).map(q => q.numero);
|
|
479
1094
|
const motivo = `não há pergunta ${numero} ${abertas.length ? `aberta; as abertas são ${abertas.join(', ')}` : 'aberta agora'}.`;
|
|
480
1095
|
recusas.push({ numero, motivo });
|
|
481
1096
|
linhas.push(`${telegram ? '⚠️ ' : '! '}${motivo}`);
|
|
@@ -488,6 +1103,12 @@ function responderAsPerguntas(raiz, envelope, escolhas, opcoes) {
|
|
|
488
1103
|
linhas.push(`${telegram ? '⚠️ ' : '! '}${motivo}`);
|
|
489
1104
|
continue;
|
|
490
1105
|
}
|
|
1106
|
+
if (!p.respondida && !perguntaEmAberto(p, quando)) {
|
|
1107
|
+
const motivo = `a pergunta ${numero} saiu há mais de 24 h; ela volta no próximo resumo se ainda esperar você.`;
|
|
1108
|
+
recusas.push({ numero, motivo });
|
|
1109
|
+
linhas.push(`${telegram ? '⚠️ ' : '! '}${motivo}`);
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
491
1112
|
if (p.respondida) {
|
|
492
1113
|
if (p.respondida.letra === letra) {
|
|
493
1114
|
// Repetida para o canal so quando e a MESMA mensagem; o dono que manda de novo e respondido.
|
|
@@ -509,10 +1130,14 @@ function responderAsPerguntas(raiz, envelope, escolhas, opcoes) {
|
|
|
509
1130
|
// O numero aponta para o pedido que saiu, byte a byte. Pedido trocado nao recebe a letra.
|
|
510
1131
|
if (sha(JSON.stringify(pedido)) !== p.pedidoSha256)
|
|
511
1132
|
throw new Error('pedido antigo: fase, modo ou sessão mudou');
|
|
512
|
-
|
|
1133
|
+
// RM-048 (D4): vencido e sem resposta, o numero vai ao pedido renovado, e so quando a
|
|
1134
|
+
// pergunta e o contexto sao os mesmos. Respondido, segue o caminho de sempre (repetida).
|
|
1135
|
+
const { vigente, renovadoDe } = pedidoParaResponder(raiz, pedido, quando);
|
|
1136
|
+
const derivada = { ...envelope, resposta: (0, hitl_contract_1.chaveDaEscolha)(vigente, indice + 1) };
|
|
513
1137
|
const assinado = { alvo: pulse_consentimento_1.ALVO_DO_PULSE, endereco: pulse_consentimento_1.ENDERECO_DA_RESPOSTA, envelope,
|
|
514
|
-
rastro: { contrato: exports.CONTRATO_RESPOSTA_DO_PULSE, numero, letra, respostaDoDonoSha256: sha(envelope.resposta)
|
|
515
|
-
|
|
1138
|
+
rastro: { contrato: exports.CONTRATO_RESPOSTA_DO_PULSE, numero, letra, respostaDoDonoSha256: sha(envelope.resposta),
|
|
1139
|
+
...(renovadoDe ? { renovadoDe } : {}), ...(palavra ? { textoLivre: palavra } : {}) } };
|
|
1140
|
+
const r = (0, hitl_gates_1.responderGate)(raiz, p.thread, vigente.id, derivada, quando, assinado);
|
|
516
1141
|
registradas.push({ numero, thread: p.thread, letra, estado: r.estado, repetida: r.repetida });
|
|
517
1142
|
linhas.push(`${telegram ? '✅ ' : ''}${numero} → ${letra}) ${p.alternativas[indice]}: ${p.consequencias[indice]}.`);
|
|
518
1143
|
servido = { ...servido, perguntas: servido.perguntas.map(q => q.numero === numero
|
|
@@ -528,15 +1153,15 @@ function responderAsPerguntas(raiz, envelope, escolhas, opcoes) {
|
|
|
528
1153
|
const cabecalho = registradas.length
|
|
529
1154
|
? `${telegram ? '📝 ' : ''}Orkastery, ${registradas.length === 1 ? '1 resposta registrada' : `${registradas.length} respostas registradas`}`
|
|
530
1155
|
: `${telegram ? '📝 ' : ''}Orkastery, nenhuma resposta registrada`;
|
|
531
|
-
const pendentes = servido.perguntas.filter(q =>
|
|
1156
|
+
const pendentes = servido.perguntas.filter(q => perguntaEmAberto(q, quando)).map(q => q.numero);
|
|
532
1157
|
const aberto = (0, pulse_consentimento_1.lerConsentimento)(raiz, estadoDir);
|
|
533
|
-
|
|
534
|
-
|
|
1158
|
+
// RM-048 (D5): o codigo do resumo mais recente continua valendo depois do prazo.
|
|
1159
|
+
const proximo = aberto && !aberto.respondido && aberto.pedido.candidatos.length ? aberto.pedido : undefined;
|
|
535
1160
|
const texto = [
|
|
536
|
-
cabecalho, '', ...linhas,
|
|
1161
|
+
cabecalho, '', ...(opcoes.avisos ?? []), ...linhas,
|
|
537
1162
|
...(pendentes.length ? ['', `Ainda sem resposta: ${pendentes.join(', ')}.`] : []),
|
|
538
1163
|
...(proximo ? [`Faltam ${proximo.candidatos.length}. Para receber as próximas, responda ${proximo.codigo} a.`] : []),
|
|
539
1164
|
].join('\n');
|
|
540
|
-
return resultado('lote', texto, { registradas, recusas,
|
|
1165
|
+
return resultado('lote', texto, { registradas, recusas: [...(opcoes.avisos ?? []).map(motivo => ({ motivo })), ...recusas],
|
|
541
1166
|
repetida: registradas.length > 0 && recusas.length === 0 && registradas.every(r => r.repetida) });
|
|
542
1167
|
}
|