@orkastery/cli 0.5.1 → 0.5.6
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 +158 -0
- package/adapters/claude-code/agents/ork-master.md +2 -0
- package/adapters/claude-code/commands/master.md +8 -0
- package/adapters/claude-code/commands/ork.md +9 -0
- package/adapters/claude-code/mods/autoridade.json +56 -0
- package/adapters/claude-code/mods/ork-clima/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-clima/hooks/contas.ts +283 -0
- package/adapters/claude-code/mods/ork-clima/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-clima/hooks/previsao.ts +393 -0
- package/adapters/claude-code/mods/ork-clima/hooks/register.ts +205 -0
- package/adapters/claude-code/mods/ork-clima/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-clima/types/index.d.ts +80 -0
- package/adapters/claude-code/mods/ork-freio/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-freio/hooks/carga.ts +160 -0
- package/adapters/claude-code/mods/ork-freio/hooks/conferencia.ts +309 -0
- package/adapters/claude-code/mods/ork-freio/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-freio/hooks/lote.ts +270 -0
- package/adapters/claude-code/mods/ork-freio/hooks/register.ts +285 -0
- package/adapters/claude-code/mods/ork-freio/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-freio/types/index.d.ts +127 -0
- package/adapters/claude-code/mods/ork-mesa/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/cartoes.tsx +246 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/decidir.ts +110 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/fila.ts +253 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/obvia.ts +98 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/register.tsx +493 -0
- package/adapters/claude-code/mods/ork-mesa/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-mesa/types/index.d.ts +131 -0
- package/adapters/claude-code/mods/ork-pista/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-pista/hooks/acoes.ts +71 -0
- package/adapters/claude-code/mods/ork-pista/hooks/ci.ts +117 -0
- package/adapters/claude-code/mods/ork-pista/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-pista/hooks/pista.tsx +356 -0
- package/adapters/claude-code/mods/ork-pista/hooks/register.tsx +439 -0
- package/adapters/claude-code/mods/ork-pista/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-pista/types/index.d.ts +85 -0
- package/adapters/claude-code/mods/ork-ponte/.claude-plugin/plugin.json +13 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/faixa.tsx +57 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/guarda.ts +410 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/mostrado.ts +55 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/nucleo.ts +157 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/papel.ts +42 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/register.ts +331 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/vigia.ts +76 -0
- package/adapters/claude-code/mods/ork-ponte/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-ponte/types/index.d.ts +178 -0
- package/adapters/claude-code/mods/ork-portao/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-portao/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-portao/hooks/lease.ts +201 -0
- package/adapters/claude-code/mods/ork-portao/hooks/plano.ts +304 -0
- package/adapters/claude-code/mods/ork-portao/hooks/register.ts +339 -0
- package/adapters/claude-code/mods/ork-portao/hooks/verify.ts +152 -0
- package/adapters/claude-code/mods/ork-portao/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-portao/types/index.d.ts +60 -0
- package/adapters/claude-code/mods/ork-radar/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-radar/hooks/colisao.ts +420 -0
- package/adapters/claude-code/mods/ork-radar/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-radar/hooks/mapa.tsx +322 -0
- package/adapters/claude-code/mods/ork-radar/hooks/register.tsx +470 -0
- package/adapters/claude-code/mods/ork-radar/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-radar/types/index.d.ts +72 -0
- package/adapters/claude-code/mods/ork-sentinela/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/alertas.ts +85 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/despacho.ts +156 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/register.ts +511 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/vigia.ts +186 -0
- package/adapters/claude-code/mods/ork-sentinela/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-sentinela/types/index.d.ts +65 -0
- package/adapters/claude-code/mods/ork-torre/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-torre/hooks/agenda.ts +60 -0
- package/adapters/claude-code/mods/ork-torre/hooks/detalhe.tsx +236 -0
- package/adapters/claude-code/mods/ork-torre/hooks/frases.ts +42 -0
- package/adapters/claude-code/mods/ork-torre/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-torre/hooks/painel.tsx +344 -0
- package/adapters/claude-code/mods/ork-torre/hooks/rede.ts +97 -0
- package/adapters/claude-code/mods/ork-torre/hooks/register.tsx +437 -0
- package/adapters/claude-code/mods/ork-torre/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-torre/types/index.d.ts +108 -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 +7 -6
- package/adapters/openclaw/README.md +26 -5
- package/adapters/openclaw/dist/hitl-ingress.js +2 -2
- package/adapters/openclaw/dist/index.js +46 -11
- 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 +49 -11
- package/assets/docs-en/markdownlint-cli2.jsonc +22 -0
- package/assets/docs-en/padroes/documentacao-de-produto.md +135 -0
- package/assets/docs-en/padroes/roadmap-de-produto.md +100 -0
- package/assets/docs-en/produto/README.md +14 -0
- package/assets/docs-en/produto/_modelo-feature.md +62 -0
- package/assets/docs-en/roadmap/README.md +14 -0
- package/assets/docs-en/roadmap/_modelo-item.md +75 -0
- package/assets/orkmind_bridge.py +117 -9
- package/dist/adapters/claude-bg.js +116 -18
- package/dist/adapters/codex.js +14 -4
- package/dist/agents-md.js +24 -14
- package/dist/auditrun.js +22 -3
- package/dist/board.js +20 -17
- package/dist/branch-de-estado.js +88 -10
- package/dist/busca-semantica.js +52 -12
- package/dist/canarios-rm057.js +149 -0
- package/dist/canarios.js +2 -0
- package/dist/catalogo.js +33 -0
- package/dist/ci.js +21 -8
- package/dist/claims-conferir.js +342 -0
- package/dist/claims.js +41 -1
- package/dist/company-brain-client.js +36 -1
- package/dist/company-brain-context.js +158 -22
- package/dist/company-brain-journal.js +2 -1
- package/dist/company-brain-migration.js +36 -1
- package/dist/conducao-sentinela.js +388 -0
- package/dist/conducao-texto.js +12 -18
- package/dist/conducao.js +122 -30
- package/dist/conhecimento.js +490 -0
- package/dist/docs.js +70 -17
- package/dist/doctor-pulse-cron.js +349 -0
- package/dist/doctor-rede.js +150 -0
- package/dist/doctor.js +195 -111
- package/dist/entrega-pr.js +42 -29
- package/dist/escopo-escrita.js +10 -1
- package/dist/esgotamentos.js +181 -0
- package/dist/estado-thread.js +23 -1
- package/dist/experiencia-instalacao.js +45 -25
- package/dist/experiencia.js +12 -2
- package/dist/fabrica-batida.js +310 -0
- package/dist/fabrica-estado.js +52 -14
- package/dist/fabrica-publicar.js +9 -1
- package/dist/fechamento.js +20 -66
- package/dist/fila-pesada.js +444 -0
- package/dist/fix.js +3 -2
- package/dist/forja.js +31 -7
- package/dist/gates.js +3 -24
- package/dist/handoff.js +10 -6
- package/dist/hitl-contract.js +59 -4
- package/dist/hitl-curto.js +11 -6
- package/dist/hitl-gates.js +5 -0
- package/dist/hitl-local-receipt.js +2 -0
- package/dist/hitl-lote.js +5 -2
- package/dist/hitl-resumo.js +38 -3
- package/dist/hitl-tempo-parado.js +201 -0
- package/dist/hitl-texto-livre.js +5 -5
- package/dist/hitl.js +29 -33
- package/dist/horario.js +41 -7
- package/dist/hosts.js +104 -11
- package/dist/impedimento.js +299 -0
- package/dist/index.js +961 -542
- package/dist/indice-vetorial.js +87 -10
- package/dist/indice.js +9 -1
- package/dist/init.js +56 -43
- package/dist/instabilidade.js +144 -0
- package/dist/integracoes-locais.js +4 -0
- package/dist/intelligence-graph-cli.js +170 -16
- package/dist/intelligence-graph-contexto.js +254 -0
- package/dist/intelligence-graph-contract.js +2 -1
- package/dist/intelligence-graph-extract-md.js +56 -4
- package/dist/intelligence-graph-extract-ts.js +71 -3
- package/dist/intelligence-graph-parsers.js +6 -4
- package/dist/intelligence-graph-query.js +66 -19
- package/dist/leases.js +664 -87
- package/dist/ledger-stats.js +15 -1
- package/dist/ledger.js +18 -1
- package/dist/licoes.js +1 -0
- package/dist/liveness.js +6 -3
- package/dist/locale-ajuda.js +761 -0
- package/dist/locale-gerados.js +160 -0
- package/dist/locale-master.js +99 -0
- package/dist/locale-onboarding.js +92 -0
- package/dist/locale-phase.js +151 -0
- package/dist/locale-pulse.js +211 -0
- package/dist/locale-retry.js +385 -0
- package/dist/locale-roadmap.js +102 -0
- package/dist/locale-ship.js +494 -0
- package/dist/locale-thread.js +277 -0
- package/dist/locale-verify.js +125 -0
- package/dist/locale.js +714 -0
- package/dist/maestro-runtime.js +4 -1
- package/dist/manifest.js +149 -4
- package/dist/maquina.js +23 -4
- package/dist/master-digest.js +6 -1
- package/dist/master-nota.js +18 -11
- package/dist/master.js +145 -31
- package/dist/mcp-artifacts.js +43 -3
- package/dist/mcp-git.js +42 -8
- package/dist/mcp-grafo-worker.js +5 -3
- package/dist/mcp-grafo.js +55 -6
- package/dist/mcp-server.js +22 -3
- package/dist/mcp-ship.js +3 -1
- package/dist/mcp-verify.js +6 -1
- package/dist/memoria.js +48 -6
- package/dist/modos.js +3 -6
- package/dist/network-roadmap.js +196 -18
- package/dist/noctum-arco.js +911 -0
- package/dist/noctum-capacidade.js +141 -0
- package/dist/noctum-envelope.js +810 -0
- package/dist/noctum-execucao.js +304 -0
- package/dist/noctum-fila.js +280 -0
- package/dist/ocupacao.js +40 -5
- package/dist/onboarding.js +40 -21
- package/dist/orkmind.js +99 -7
- package/dist/orquestracao.js +24 -21
- package/dist/parado-no-condutor.js +219 -38
- package/dist/phase.js +160 -52
- package/dist/policies.js +56 -5
- package/dist/portfolio.js +8 -3
- package/dist/postura-local.js +234 -0
- package/dist/preflight.js +11 -10
- package/dist/procedencia.js +225 -0
- package/dist/projeto-alvo.js +132 -5
- package/dist/prompts.js +40 -1
- package/dist/prova-ativacao.js +75 -6
- package/dist/pulse-delivery.js +47 -4
- package/dist/pulse.js +98 -19
- package/dist/ratelimit.js +24 -6
- package/dist/recall.js +3 -1
- package/dist/redacao-saida.js +63 -0
- 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 +337 -0
- package/dist/rede.js +980 -0
- package/dist/retry.js +291 -140
- package/dist/roadmap-reservas.js +20 -3
- package/dist/roadmap-status.js +150 -34
- package/dist/runtime-context.js +181 -3
- package/dist/runtime-profiles.js +22 -1
- package/dist/runtimes.js +2 -1
- package/dist/saida-segura.js +58 -0
- package/dist/session-events.js +7 -3
- package/dist/session-watcher-claude.js +120 -24
- package/dist/session-watcher.js +185 -33
- package/dist/sessoes-contas.js +187 -0
- package/dist/sessoes-inventario.js +6 -18
- package/dist/sessoes.js +83 -2
- package/dist/ship-publicar.js +800 -0
- package/dist/ship.js +157 -88
- package/dist/steal.js +77 -0
- package/dist/thread-close.js +3 -2
- package/dist/thread.js +299 -49
- package/dist/util.js +8 -1
- package/dist/verify.js +141 -52
- package/dist/vida-da-thread.js +254 -0
- package/dist/worktree-local.js +165 -0
- package/dist/worktree.js +25 -2
- package/dist/yaml.js +27 -8
- package/eval/fixtures/fx-pedido-colado/caso.json +20 -0
- package/package.json +7 -3
- package/references/definition-of-done.md +1 -1
- package/schemas/code-artifact-graph.v1.schema.json +2 -1
- package/skills/README.md +1 -1
- package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
- package/skills/phases/master-metrics/SKILL.md +8 -3
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// O Clima de Cota (RM-059, onda 1, F4; T4.1): a serie das janelas de limite da conta desta sessao e a
|
|
2
|
+
// previsao de quando cada uma acaba no ritmo atual (D4 a D6 e D12 a D15 da thread). Modulo puro: so dados
|
|
3
|
+
// entram e saem, para o teste do nucleo transpila-lo sem o Claude Code. Nenhum `$` passa por aqui, e o
|
|
4
|
+
// fuso chega explicito: quem chama decide qual e.
|
|
5
|
+
import type { ClimaEstado, ClimaJanela, ClimaPonto, ClimaPrevisao, ClimaProblema, ClimaSerie } from '../types'
|
|
6
|
+
|
|
7
|
+
export const FORMATO_DA_SERIE = 'ork.clima-serie/v1'
|
|
8
|
+
/** A chave da serie no `$.store` do mod, que dura entre sessoes. */
|
|
9
|
+
export const CHAVE_DA_SERIE = 'serie'
|
|
10
|
+
export const TETO_DE_PONTOS = 120
|
|
11
|
+
export const TETO_DE_JANELAS = 8
|
|
12
|
+
/** Diferenca de `resetsAt` que ainda e a mesma janela. */
|
|
13
|
+
export const TOLERANCIA_DA_RENOVACAO_MS = 60 * 1000
|
|
14
|
+
/** Dentro da janela o uso so cresce: uma queda deste tamanho so acontece quando ela renova. */
|
|
15
|
+
export const QUEDA_QUE_RENOVA = 1
|
|
16
|
+
/** O ritmo sai deste intervalo recente, proporcional a janela de cota (D4). */
|
|
17
|
+
export const INTERVALO_DO_RITMO_MS: Readonly<Record<string, number>> = {
|
|
18
|
+
five_hour: 60 * 60 * 1000,
|
|
19
|
+
seven_day: 24 * 60 * 60 * 1000,
|
|
20
|
+
}
|
|
21
|
+
export const INTERVALO_PADRAO_DO_RITMO_MS = 60 * 60 * 1000
|
|
22
|
+
/**
|
|
23
|
+
* Serie mais nova que o intervalo so tem ritmo com esta fracao dele entre a primeira e a ultima leitura: 10 min
|
|
24
|
+
* na janela de 5 h e 4 h na de 7 d (D13).
|
|
25
|
+
*/
|
|
26
|
+
export const FRACAO_MINIMA_DO_INTERVALO = 6
|
|
27
|
+
/**
|
|
28
|
+
* Nestas janelas, a serie mais nova que o intervalo so preve o esgotamento ate a distancia que ela ja observou:
|
|
29
|
+
* o ritmo de quatro horas de dia util nao vale pela madrugada (D14), e a serie nova no meio da semana ainda
|
|
30
|
+
* avisa quando o fim esta perto (D15).
|
|
31
|
+
*/
|
|
32
|
+
export const JANELAS_COM_HORIZONTE: readonly string[] = ['seven_day']
|
|
33
|
+
/** A partir deste percentual a faixa avisa mesmo sem ritmo (D5). */
|
|
34
|
+
export const LIMIAR_DE_ATENCAO = 90
|
|
35
|
+
/** Na faixa, o percentual vai em degraus e a hora de acabar vai arredondada, para o texto nao mudar a cada ponto (D13). */
|
|
36
|
+
export const DEGRAU_DO_PERCENTUAL = 10
|
|
37
|
+
export const PASSO_DA_HORA_NA_FAIXA_MS = 15 * 60 * 1000
|
|
38
|
+
/** O texto da faixa nunca passa disto, o mesmo teto da ponte. */
|
|
39
|
+
export const TETO_DA_FAIXA = 200
|
|
40
|
+
/** Quantas threads que nao cabem a faixa nomeia antes do `e mais N`. */
|
|
41
|
+
export const TETO_NA_FAIXA = 3
|
|
42
|
+
export const AVISO_DE_AUTORIDADE = 'O Clima só lê e avisa: troca de conta, de runtime ou de modelo fica com o núcleo e com você.'
|
|
43
|
+
|
|
44
|
+
const KIND = /^[a-z0-9][a-z0-9_.-]{0,63}$/i
|
|
45
|
+
const ORDEM_DOS_KINDS = ['five_hour', 'seven_day']
|
|
46
|
+
const ROTULOS: Readonly<Record<string, string>> = { five_hour: '5 h', seven_day: '7 d', spend_limit: 'gasto' }
|
|
47
|
+
const GRAVIDADE: Readonly<Record<ClimaEstado, number>> = { 'esgotada': 0, 'acaba-antes': 1, 'folga': 2, 'sem-ritmo': 3 }
|
|
48
|
+
|
|
49
|
+
function numero(v: unknown): v is number {
|
|
50
|
+
return typeof v === 'number' && Number.isFinite(v)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `5 h`, `7 d`, `gasto` ou o proprio `kind`. */
|
|
54
|
+
export function rotuloDaJanela(kind: string): string {
|
|
55
|
+
return Object.prototype.hasOwnProperty.call(ROTULOS, kind) ? (ROTULOS[kind] as string) : kind
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ordemDoKind(kind: string): number {
|
|
59
|
+
const i = ORDEM_DOS_KINDS.indexOf(kind)
|
|
60
|
+
return i < 0 ? ORDEM_DOS_KINDS.length : i
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function porKind(a: { kind: string }, b: { kind: string }): number {
|
|
64
|
+
return ordemDoKind(a.kind) - ordemDoKind(b.kind) || (a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** O intervalo recente do ritmo de uma janela. */
|
|
68
|
+
export function intervaloDoRitmo(kind: string): number {
|
|
69
|
+
return Object.prototype.hasOwnProperty.call(INTERVALO_DO_RITMO_MS, kind) ? (INTERVALO_DO_RITMO_MS[kind] as number)
|
|
70
|
+
: INTERVALO_PADRAO_DO_RITMO_MS
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Quanto de serie a janela precisa antes de ter ritmo, enquanto e mais nova que o intervalo. */
|
|
74
|
+
export function minimoDaSerieJovem(kind: string): number {
|
|
75
|
+
return intervaloDoRitmo(kind) / FRACAO_MINIMA_DO_INTERVALO
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Ate onde a serie jovem preve, nas janelas com horizonte: o tempo entre a primeira e a ultima leitura. `null`
|
|
80
|
+
* quando a serie ja cobre o intervalo inteiro ou a janela nao tem horizonte (D15).
|
|
81
|
+
*/
|
|
82
|
+
export function horizonteDaSerie(j: Pick<ClimaJanela, 'kind' | 'pontos'>): number | null {
|
|
83
|
+
const ultimo = j.pontos[j.pontos.length - 1]
|
|
84
|
+
const primeiro = j.pontos[0]
|
|
85
|
+
if (ultimo === undefined || primeiro === undefined || !JANELAS_COM_HORIZONTE.includes(j.kind)) return null
|
|
86
|
+
return primeiro.em >= ultimo.em - intervaloDoRitmo(j.kind) ? ultimo.em - primeiro.em : null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A leitura conferida, ou `null` quando ela nao serve (kind estranho, percentual que nao e numero, data ruim). */
|
|
90
|
+
export function conferirLeitura(bruta: unknown): { kind: string; pct: number; renovaEm: number | null } | null {
|
|
91
|
+
const l = (bruta ?? {}) as Record<string, unknown>
|
|
92
|
+
if (typeof l.kind !== 'string' || !KIND.test(l.kind)) return null
|
|
93
|
+
if (!numero(l.percentUsed) || l.percentUsed < 0 || l.percentUsed > 1000) return null
|
|
94
|
+
let renovaEm: number | null = null
|
|
95
|
+
if (l.resetsAt !== undefined && l.resetsAt !== null) {
|
|
96
|
+
const t = typeof l.resetsAt === 'string' ? Date.parse(l.resetsAt) : NaN
|
|
97
|
+
if (!Number.isFinite(t)) return null
|
|
98
|
+
renovaEm = t
|
|
99
|
+
}
|
|
100
|
+
return { kind: l.kind, pct: Math.round(l.percentUsed * 10) / 10, renovaEm }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A serie guardada, ou uma vazia quando o que esta no store nao e deste formato. */
|
|
104
|
+
export function lerSerie(guardado: unknown): ClimaSerie {
|
|
105
|
+
const s = guardado as Partial<ClimaSerie> | null | undefined
|
|
106
|
+
const janelas: ClimaJanela[] = []
|
|
107
|
+
if (s && s.formato === FORMATO_DA_SERIE && Array.isArray(s.janelas)) {
|
|
108
|
+
for (const bruta of s.janelas) {
|
|
109
|
+
const j = (bruta ?? {}) as Partial<ClimaJanela>
|
|
110
|
+
if (typeof j.kind !== 'string' || !KIND.test(j.kind) || janelas.some(x => x.kind === j.kind)) continue
|
|
111
|
+
if (!(j.renovaEm === null || numero(j.renovaEm))) continue
|
|
112
|
+
const pontos = (Array.isArray(j.pontos) ? j.pontos : [])
|
|
113
|
+
.filter((p): p is ClimaPonto => !!p && numero(p.em) && numero(p.pct))
|
|
114
|
+
.map(p => ({ em: p.em, pct: p.pct }))
|
|
115
|
+
.sort((a, b) => a.em - b.em)
|
|
116
|
+
if (pontos.length > 0) janelas.push({ kind: j.kind, renovaEm: j.renovaEm, pontos: pontos.slice(-TETO_DE_PONTOS) })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { formato: FORMATO_DA_SERIE, janelas: janelas.sort(porKind).slice(0, TETO_DE_JANELAS) }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A janela ja renovou: a ultima leitura dela nao diz mais nada. */
|
|
123
|
+
export function vencida(j: Pick<ClimaJanela, 'renovaEm'>, agora: number): boolean {
|
|
124
|
+
return j.renovaEm !== null && j.renovaEm + TOLERANCIA_DA_RENOVACAO_MS <= agora
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Acrescenta o ponto em ordem de tempo; numa corrida plana ficam so o primeiro e o ultimo ponto. */
|
|
128
|
+
function acrescentar(pontos: readonly ClimaPonto[], novo: ClimaPonto): ClimaPonto[] {
|
|
129
|
+
const ordenados = [...pontos.filter(p => p.em !== novo.em), novo].sort((a, b) => a.em - b.em)
|
|
130
|
+
const saida: ClimaPonto[] = []
|
|
131
|
+
for (const p of ordenados) {
|
|
132
|
+
const ultimo = saida[saida.length - 1]
|
|
133
|
+
const penultimo = saida[saida.length - 2]
|
|
134
|
+
if (ultimo !== undefined && penultimo !== undefined && ultimo.pct === p.pct && penultimo.pct === p.pct) saida[saida.length - 1] = p
|
|
135
|
+
else saida.push(p)
|
|
136
|
+
}
|
|
137
|
+
return saida.slice(-TETO_DE_PONTOS)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* O que a medicao trouxe fora do formato, numa frase, ou `null` quando tudo serviu: a lista de janelas que nem e
|
|
142
|
+
* lista, ou as janelas que `conferirLeitura` descarta. Sem isso, um campo renomeado no motor apagaria o Clima
|
|
143
|
+
* calado (D15).
|
|
144
|
+
*/
|
|
145
|
+
export function problemaDaLeitura(leituras: unknown): string | null {
|
|
146
|
+
if (!Array.isArray(leituras)) return 'a medição avisou leitura nova sem a lista de janelas'
|
|
147
|
+
const descartadas = leituras.filter(l => conferirLeitura(l) === null).length
|
|
148
|
+
if (descartadas === 0) return null
|
|
149
|
+
return `a leitura trouxe ${descartadas === 1 ? '1 janela' : `${descartadas} janelas`} num formato que o Clima não conhece`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Grava as leituras de uma medicao em que as janelas mudaram: uma serie por `kind`, serie nova quando a
|
|
154
|
+
* janela renova (o `resetsAt` muda ou o percentual cai), tetos de pontos e de janelas (D4). A medicao traz
|
|
155
|
+
* todas as janelas da conta, entao a janela vencida e a que saiu da leitura deixam a serie (D13). Com janela
|
|
156
|
+
* fora do formato a leitura nao esta completa, e as outras janelas guardadas ficam (D15).
|
|
157
|
+
*/
|
|
158
|
+
export function registrarLeituras(guardado: unknown, leituras: readonly unknown[] | undefined, agora: number): ClimaSerie {
|
|
159
|
+
const brutas = leituras ?? []
|
|
160
|
+
const lidas = brutas.map(conferirLeitura).filter((l): l is NonNullable<ReturnType<typeof conferirLeitura>> => l !== null)
|
|
161
|
+
const anteriores = lerSerie(guardado).janelas.filter(j => !vencida(j, agora))
|
|
162
|
+
const janelas: ClimaJanela[] = lidas.length === brutas.length ? [] : anteriores.filter(a => !lidas.some(l => l.kind === a.kind))
|
|
163
|
+
for (const l of lidas) {
|
|
164
|
+
if (janelas.some(j => j.kind === l.kind)) continue
|
|
165
|
+
const anterior = anteriores.find(j => j.kind === l.kind)
|
|
166
|
+
const ultimo = anterior?.pontos[anterior.pontos.length - 1]
|
|
167
|
+
const renovou = anterior !== undefined && (
|
|
168
|
+
(anterior.renovaEm !== null && l.renovaEm !== null && Math.abs(anterior.renovaEm - l.renovaEm) > TOLERANCIA_DA_RENOVACAO_MS)
|
|
169
|
+
|| (ultimo !== undefined && ultimo.pct - l.pct >= QUEDA_QUE_RENOVA))
|
|
170
|
+
const ponto = { em: agora, pct: l.pct }
|
|
171
|
+
janelas.push(anterior === undefined || renovou
|
|
172
|
+
? { kind: l.kind, renovaEm: l.renovaEm, pontos: [ponto] }
|
|
173
|
+
: { kind: l.kind, renovaEm: l.renovaEm ?? anterior.renovaEm, pontos: acrescentar(anterior.pontos, ponto) })
|
|
174
|
+
}
|
|
175
|
+
return { formato: FORMATO_DA_SERIE, janelas: janelas.sort(porKind).slice(0, TETO_DE_JANELAS) }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* O ritmo da janela, em pontos por ms, no intervalo recente (D13). Como o uso so cresce, o valor no inicio do
|
|
180
|
+
* intervalo sai dos pontos que o cercam: exato quando o ultimo antes e o primeiro dentro sao iguais, e por
|
|
181
|
+
* interpolacao quando nao sao; com leitura esparsa (D12), isso da a inclinacao desde o ultimo ponto antes do
|
|
182
|
+
* intervalo. Serie mais nova que o intervalo precisa de `minimoDaSerieJovem` entre a primeira e a ultima
|
|
183
|
+
* leitura; antes disso, nao ha ritmo.
|
|
184
|
+
*/
|
|
185
|
+
export function ritmoDaJanela(j: Pick<ClimaJanela, 'kind' | 'pontos'>): number | null {
|
|
186
|
+
const ultimo = j.pontos[j.pontos.length - 1]
|
|
187
|
+
const primeiro = j.pontos[0]
|
|
188
|
+
if (ultimo === undefined || primeiro === undefined) return null
|
|
189
|
+
const intervalo = intervaloDoRitmo(j.kind)
|
|
190
|
+
const inicio = ultimo.em - intervalo
|
|
191
|
+
const antes = [...j.pontos].reverse().find(p => p.em < inicio)
|
|
192
|
+
const dentro = j.pontos.find(p => p.em >= inicio)
|
|
193
|
+
if (antes !== undefined && dentro !== undefined) {
|
|
194
|
+
const noInicio = antes.pct === dentro.pct ? antes.pct
|
|
195
|
+
: antes.pct + (dentro.pct - antes.pct) * (inicio - antes.em) / (dentro.em - antes.em)
|
|
196
|
+
return (ultimo.pct - noInicio) / intervalo
|
|
197
|
+
}
|
|
198
|
+
if (ultimo.em - primeiro.em < minimoDaSerieJovem(j.kind)) return null
|
|
199
|
+
return (ultimo.pct - primeiro.pct) / (ultimo.em - primeiro.em)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Pontos por hora com uma casa, ou duas abaixo de 1, para o ritmo lento da semana nao virar zero (D14). */
|
|
203
|
+
function arredondarRitmo(porHora: number): number {
|
|
204
|
+
return Math.abs(porHora) < 1 ? Math.round(porHora * 100) / 100 : Math.round(porHora * 10) / 10
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A previsao de cada janela que ainda nao renovou, da mais urgente para a mais calma. */
|
|
208
|
+
export function prever(guardado: unknown, agora: number): ClimaPrevisao[] {
|
|
209
|
+
const previsoes: ClimaPrevisao[] = []
|
|
210
|
+
for (const j of lerSerie(guardado).janelas) {
|
|
211
|
+
const ultimo = j.pontos[j.pontos.length - 1]
|
|
212
|
+
if (ultimo === undefined || vencida(j, agora)) continue
|
|
213
|
+
const ritmoPorMs = ritmoDaJanela(j)
|
|
214
|
+
let estado: ClimaEstado
|
|
215
|
+
let acabaEm: number | null = null
|
|
216
|
+
if (ultimo.pct >= 100) estado = 'esgotada'
|
|
217
|
+
else if (ritmoPorMs === null) estado = 'sem-ritmo'
|
|
218
|
+
else if (ritmoPorMs <= 0) estado = 'folga'
|
|
219
|
+
else {
|
|
220
|
+
acabaEm = Math.round(ultimo.em + (100 - ultimo.pct) / ritmoPorMs)
|
|
221
|
+
const horizonte = horizonteDaSerie(j)
|
|
222
|
+
if (j.renovaEm !== null && acabaEm >= j.renovaEm) estado = 'folga'
|
|
223
|
+
else if (horizonte !== null && acabaEm - ultimo.em > horizonte) {
|
|
224
|
+
// A serie jovem nao preve alem do que ja observou (D15).
|
|
225
|
+
estado = 'sem-ritmo'
|
|
226
|
+
acabaEm = null
|
|
227
|
+
} else estado = 'acaba-antes'
|
|
228
|
+
}
|
|
229
|
+
previsoes.push({
|
|
230
|
+
kind: j.kind, rotulo: rotuloDaJanela(j.kind), pct: ultimo.pct, lidaEm: ultimo.em, renovaEm: j.renovaEm,
|
|
231
|
+
ritmoPorHora: ritmoPorMs === null ? null : arredondarRitmo(ritmoPorMs * 3600 * 1000), acabaEm, estado,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
return previsoes.sort((a, b) => GRAVIDADE[a.estado] - GRAVIDADE[b.estado] || porKind(a, b))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** A janela ja se esgotou ou acaba antes de renovar no ritmo atual. */
|
|
238
|
+
export function emAlerta(p: ClimaPrevisao): boolean {
|
|
239
|
+
return p.estado === 'esgotada' || p.estado === 'acaba-antes'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** A janela pede atencao na faixa: em alerta, ou no limiar mesmo sem ritmo (D5). */
|
|
243
|
+
export function pedeAtencao(p: ClimaPrevisao): boolean {
|
|
244
|
+
return emAlerta(p) || p.pct >= LIMIAR_DE_ATENCAO
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
type Partes = { ano: string; mes: string; dia: string; hora: string; minuto: string }
|
|
248
|
+
|
|
249
|
+
function partesLocais(ms: number, fuso: string): Partes {
|
|
250
|
+
let f: Intl.DateTimeFormat
|
|
251
|
+
try {
|
|
252
|
+
f = new Intl.DateTimeFormat('en-US', { timeZone: fuso, year: 'numeric', month: '2-digit', day: '2-digit',
|
|
253
|
+
hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
|
254
|
+
} catch {
|
|
255
|
+
f = new Intl.DateTimeFormat('en-US', { timeZone: 'UTC', year: 'numeric', month: '2-digit', day: '2-digit',
|
|
256
|
+
hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
|
257
|
+
}
|
|
258
|
+
const p: Record<string, string> = {}
|
|
259
|
+
for (const parte of f.formatToParts(new Date(ms))) p[parte.type] = parte.value
|
|
260
|
+
return { ano: p.year ?? '', mes: p.month ?? '', dia: p.day ?? '', hora: p.hour ?? '', minuto: p.minute ?? '' }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* A hora no fuso, com o minuto truncado como o nucleo escreve: `16:40` no mesmo dia de `agora`, `13/10 09:00`
|
|
265
|
+
* em outro dia e `13/10/2027 09:00` em outro ano.
|
|
266
|
+
*/
|
|
267
|
+
export function formatarHora(ms: number, fuso: string, agora: number): string {
|
|
268
|
+
const p = partesLocais(ms, fuso)
|
|
269
|
+
const a = partesLocais(agora, fuso)
|
|
270
|
+
if (p.ano === a.ano && p.mes === a.mes && p.dia === a.dia) return `${p.hora}:${p.minuto}`
|
|
271
|
+
return `${p.dia}/${p.mes}${p.ano !== a.ano ? `/${p.ano}` : ''} ${p.hora}:${p.minuto}`
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** `45min`, `1h30`, `2d 05h`, como o nucleo escreve; abaixo de 1 min, `menos de 1 min`. */
|
|
275
|
+
export function duracao(ms: number): string {
|
|
276
|
+
const minutos = Math.floor(Math.abs(ms) / 60000)
|
|
277
|
+
if (minutos < 1) return 'menos de 1 min'
|
|
278
|
+
if (minutos < 60) return `${minutos}min`
|
|
279
|
+
const horas = Math.floor(minutos / 60)
|
|
280
|
+
if (horas < 24) return `${horas}h${String(minutos % 60).padStart(2, '0')}`
|
|
281
|
+
return `${Math.floor(horas / 24)}d ${String(horas % 24).padStart(2, '0')}h`
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** `em 1h30` ou `há 10min`, relativo a `agora`. */
|
|
285
|
+
export function distancia(ms: number, agora: number): string {
|
|
286
|
+
return ms >= agora ? `em ${duracao(ms - agora)}` : `há ${duracao(agora - ms)}`
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** O numero com virgula decimal e no maximo `casas` casas. */
|
|
290
|
+
export function numeroBr(n: number, casas = 1): string {
|
|
291
|
+
const fator = 10 ** casas
|
|
292
|
+
return String(Math.round(n * fator) / fator).replace('.', ',')
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** O percentual da faixa em degraus: `70% ou mais`, ou `menos de 10%` (D13). */
|
|
296
|
+
export function percentualEmDegrau(pct: number): string {
|
|
297
|
+
const degrau = Math.floor(pct / DEGRAU_DO_PERCENTUAL) * DEGRAU_DO_PERCENTUAL
|
|
298
|
+
return degrau <= 0 ? `menos de ${DEGRAU_DO_PERCENTUAL}%` : `${degrau}% ou mais`
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* O trecho de uma janela na faixa: percentual em degraus e hora de acabar arredondada para baixo ao quarto de
|
|
303
|
+
* hora (D13), que assim nunca passa da renovacao (D14). A hora que nao fica depois da leitura vira `acaba em
|
|
304
|
+
* instantes` (D15).
|
|
305
|
+
*/
|
|
306
|
+
function trechoDaFaixa(p: ClimaPrevisao, fuso: string, agora: number): string {
|
|
307
|
+
const renova = p.renovaEm === null ? '' : `, renova ${formatarHora(p.renovaEm, fuso, agora)}`
|
|
308
|
+
if (p.estado === 'esgotada') return `${p.rotulo} esgotou${renova}`
|
|
309
|
+
if (p.estado === 'acaba-antes' && p.acabaEm !== null) {
|
|
310
|
+
const acaba = Math.floor(p.acabaEm / PASSO_DA_HORA_NA_FAIXA_MS) * PASSO_DA_HORA_NA_FAIXA_MS
|
|
311
|
+
const quando = acaba > p.lidaEm ? `acaba ~${formatarHora(acaba, fuso, agora)}` : 'acaba em instantes'
|
|
312
|
+
return `${p.rotulo} com ${percentualEmDegrau(p.pct)}, ${quando}${renova}`
|
|
313
|
+
}
|
|
314
|
+
return `${p.rotulo} com ${percentualEmDegrau(p.pct)}${renova}`
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function cortar(texto: string, teto: number): string {
|
|
318
|
+
return texto.length <= teto ? texto : texto.slice(0, teto - 1) + '…'
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* A linha do Clima na faixa da ponte, ou `null` quando nenhuma janela pede atencao (a linha sai). As threads
|
|
323
|
+
* que nao cabem vem primeiro, porque a faixa corta o fim da linha (D13).
|
|
324
|
+
*/
|
|
325
|
+
export function textoDaFaixa(previsoes: readonly ClimaPrevisao[], fuso: string, agora: number, naoCabem: readonly string[] = []): string | null {
|
|
326
|
+
const trechos = previsoes.filter(pedeAtencao).map(p => trechoDaFaixa(p, fuso, agora))
|
|
327
|
+
if (trechos.length === 0) return null
|
|
328
|
+
const nomeadas = naoCabem.slice(0, TETO_NA_FAIXA)
|
|
329
|
+
const mais = naoCabem.length > nomeadas.length ? ` e mais ${naoCabem.length - nomeadas.length}` : ''
|
|
330
|
+
const inicio = nomeadas.length > 0 ? `não cabe: ${nomeadas.join(', ')}${mais}; ` : ''
|
|
331
|
+
return cortar(inicio + trechos.join(' · '), TETO_DA_FAIXA)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** `às 14:30` hoje, `em 11/10 14:30` em outro dia. */
|
|
335
|
+
function quandoLida(ms: number, fuso: string, agora: number): string {
|
|
336
|
+
const hora = formatarHora(ms, fuso, agora)
|
|
337
|
+
return hora.includes('/') ? `em ${hora}` : `às ${hora}`
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Uma janela no `/clima`, com o ritmo, a previsao, a renovacao, as distancias e a hora da leitura. */
|
|
341
|
+
export function linhaDaJanela(p: ClimaPrevisao, fuso: string, agora: number): string {
|
|
342
|
+
const ritmo = p.ritmoPorHora === null ? 'ritmo ainda sem leituras suficientes'
|
|
343
|
+
: `ritmo de ${numeroBr(p.ritmoPorHora, Math.abs(p.ritmoPorHora) < 1 ? 2 : 1)} pontos por hora`
|
|
344
|
+
let previsao: string
|
|
345
|
+
if (p.estado === 'esgotada') previsao = 'esgotou'
|
|
346
|
+
else if (p.estado === 'acaba-antes' && p.acabaEm !== null) {
|
|
347
|
+
previsao = `acaba ~${formatarHora(p.acabaEm, fuso, agora)} (${distancia(p.acabaEm, agora)}), antes de renovar`
|
|
348
|
+
} else if (p.estado === 'folga') previsao = 'folga no ritmo atual'
|
|
349
|
+
else previsao = 'sem previsão ainda'
|
|
350
|
+
const renova = p.renovaEm === null ? 'sem hora de renovação' : `renova ${formatarHora(p.renovaEm, fuso, agora)} (${distancia(p.renovaEm, agora)})`
|
|
351
|
+
return `- ${p.rotulo}: ${numeroBr(p.pct)}%, ${ritmo}, ${previsao}, ${renova}. Lida ${quandoLida(p.lidaEm, fuso, agora)}.`
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** O problema da ultima medicao numa linha: `A medicao falhou (...) as 14:30.` (D15). */
|
|
355
|
+
export function textoDoProblema(p: ClimaProblema, fuso: string, agora: number): string {
|
|
356
|
+
const quando = p.em === null ? '' : ` ${quandoLida(p.em, fuso, agora)}`
|
|
357
|
+
return `${p.motivo.charAt(0).toUpperCase()}${p.motivo.slice(1)}${quando}.`
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* O texto do `/clima`: as janelas da conta desta sessao, o problema da ultima medicao, o que vier depois e o aviso
|
|
362
|
+
* de autoridade. Com problema guardado, a falta de leitura nao e atribuida a conta (D15).
|
|
363
|
+
*/
|
|
364
|
+
export function textoDoClima(previsoes: readonly ClimaPrevisao[], fuso: string, agora: number, extras: readonly string[] = [],
|
|
365
|
+
problema: ClimaProblema | null = null): string {
|
|
366
|
+
const linhas = ['Clima de cota da conta desta sessão']
|
|
367
|
+
if (previsoes.length > 0) {
|
|
368
|
+
for (const p of previsoes) linhas.push(linhaDaJanela(p, fuso, agora))
|
|
369
|
+
} else if (problema === null) {
|
|
370
|
+
linhas.push('Sem leitura de cota ainda. A leitura chega na medição depois de uma resposta da API desta sessão, e só existe em conta de assinatura.')
|
|
371
|
+
} else {
|
|
372
|
+
linhas.push('Sem leitura de cota guardada.')
|
|
373
|
+
}
|
|
374
|
+
if (problema !== null) linhas.push(textoDoProblema(problema, fuso, agora))
|
|
375
|
+
return [...linhas, ...extras, AVISO_DE_AUTORIDADE].join('\n')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** O `/clima` fora da sessao do dono: o Clima so roda nela (D3). */
|
|
379
|
+
export function textoForaDoDono(papel: string): string {
|
|
380
|
+
const base = `O Clima de Cota só roda na sessão do dono de um projeto do Orkastery; o papel desta sessão é ${papel}.`
|
|
381
|
+
return papel === 'dormente' ? `${base} Se o servidor orkastery ainda está conectando, tente de novo em instantes.` : base
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** O `/clima` sem a ponte, de quem o Clima depende para tudo; o erro interno nao vai ao dono (D13). */
|
|
385
|
+
export const TEXTO_SEM_PONTE = 'O Clima de Cota depende da ponte ork-ponte, que não respondeu nesta sessão; confira se ela está instalada e carregada.'
|
|
386
|
+
/** O mesmo motivo, como problema da medicao (D15). */
|
|
387
|
+
export const MOTIVO_SEM_PONTE = 'a ponte ork-ponte não respondeu; confira se ela está instalada e carregada'
|
|
388
|
+
|
|
389
|
+
/** O motivo de um erro em uma linha curta. */
|
|
390
|
+
export function motivoDoErro(erro: unknown): string {
|
|
391
|
+
const texto = erro instanceof Error ? erro.message : String(erro)
|
|
392
|
+
return texto.replace(/\s+/g, ' ').trim().slice(0, 200)
|
|
393
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// O Clima de Cota (RM-059, onda 1, F4). Le as janelas de limite da conta desta sessao a cada medicao do
|
|
2
|
+
// motor, guarda a serie no `$.store` do mod, preve quando cada janela acaba no ritmo atual e avisa na faixa
|
|
3
|
+
// da ponte antes de a fase morrer no meio, com as retomadas que nao cabem (T4.2). So na sessao do dono, so
|
|
4
|
+
// le e so avisa: troca de conta, runtime ou modelo fica com o nucleo (rodizio) e com o dono. O `$.ork` vem
|
|
5
|
+
// da `ork-ponte`; a logica fica nos modulos puros ao lado, e aqui so as chamadas ao `$`, sempre escritas
|
|
6
|
+
// pelo nome do noun e do metodo, como o validador exige.
|
|
7
|
+
import type { Register } from 'claude-code'
|
|
8
|
+
import type { ClimaNaoCabe, ClimaProblema } from '../types'
|
|
9
|
+
import {
|
|
10
|
+
cruzar, linhasDoCruzamento, marcasDoRelatorio, naoCabemGuardados, paradasDoRetrato, resumoDoClima, retomadasDaFila,
|
|
11
|
+
textoDoNaoCabe, threadsAbertas, threadsQueNaoCabem,
|
|
12
|
+
} from './contas'
|
|
13
|
+
import {
|
|
14
|
+
CHAVE_DA_SERIE, emAlerta, MOTIVO_SEM_PONTE, motivoDoErro, prever, problemaDaLeitura, registrarLeituras, TEXTO_SEM_PONTE,
|
|
15
|
+
textoDaFaixa, textoDoClima, textoForaDoDono,
|
|
16
|
+
} from './previsao'
|
|
17
|
+
|
|
18
|
+
const MOD = 'ork-clima'
|
|
19
|
+
const FAIXA = { plugin: 'ork-clima', key: 'faixa' } as const
|
|
20
|
+
const NAO_CABEM = { plugin: 'ork-clima', key: 'naoCabem' } as const
|
|
21
|
+
const PROBLEMA = { plugin: 'ork-clima', key: 'problema' } as const
|
|
22
|
+
|
|
23
|
+
/** O fuso da sessao (D6): a ponte nao entrega o `owner.timezone`. */
|
|
24
|
+
function fusoDaSessao(): string {
|
|
25
|
+
try {
|
|
26
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
27
|
+
} catch {
|
|
28
|
+
return 'UTC'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const register: Register = on => {
|
|
33
|
+
// O `/clima` fica disponivel fora da sessao de fase (D3), e tambem sem a ponte, para dizer o motivo. A faixa
|
|
34
|
+
// so sai na primeira medicao: antes de o servidor `orkastery` conectar, a ponte ainda responde `dormente`.
|
|
35
|
+
// O registro recusado nao segura o inicio da sessao, mas o motivo vai ao transcript (D15).
|
|
36
|
+
on('session.start', async ($, e, next) => {
|
|
37
|
+
let papel: string | null = null
|
|
38
|
+
try {
|
|
39
|
+
papel = await $.ork.papel()
|
|
40
|
+
} catch {
|
|
41
|
+
papel = null
|
|
42
|
+
}
|
|
43
|
+
if (papel !== 'fase') {
|
|
44
|
+
await $.command.register({ name: 'clima', description: 'Previsão da cota da conta desta sessão: quando cada janela acaba no ritmo atual' })
|
|
45
|
+
.catch(erro => $.ui.log(`Clima de Cota: o /clima não foi registrado (${motivoDoErro(erro)}).`))
|
|
46
|
+
}
|
|
47
|
+
return next(e)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Cada medicao: grava a leitura quando as janelas mudaram (D2) e republica a faixa so quando o texto muda (D5).
|
|
51
|
+
// Com janela em alerta e leitura nova, le a fila inteira de retomada e guarda as que nao cabem (D7, D13); a
|
|
52
|
+
// medicao sem leitura nova usa o que ficou guardado. So vai ao registro do mostrado o que a faixa mostrou, e o
|
|
53
|
+
// registro que falhar fica pendente para a medicao seguinte (D15). Fora do papel `dono`, a linha sai (D13).
|
|
54
|
+
// Uma falha nunca segura a medicao: o motivo novo vai uma vez ao transcript e fica no estado para o `/clima`,
|
|
55
|
+
// ate uma medicao boa limpa-lo (D15).
|
|
56
|
+
on('session.measure', async ($, e, next) => {
|
|
57
|
+
let agora: number | null = null
|
|
58
|
+
let problema: ClimaProblema | null = null
|
|
59
|
+
let semLeituraNova = false
|
|
60
|
+
try {
|
|
61
|
+
agora = await $.clock.now()
|
|
62
|
+
let papel: string | null = null
|
|
63
|
+
try {
|
|
64
|
+
papel = await $.ork.papel()
|
|
65
|
+
} catch {
|
|
66
|
+
papel = null
|
|
67
|
+
}
|
|
68
|
+
const anterior = await $.state.get(FAIXA)
|
|
69
|
+
if (papel === null) {
|
|
70
|
+
problema = { motivo: MOTIVO_SEM_PONTE, em: agora, daLeitura: false }
|
|
71
|
+
} else if (papel !== 'dono') {
|
|
72
|
+
if ((anterior.value ?? null) !== null) {
|
|
73
|
+
await $.ork.faixa({ mod: MOD })
|
|
74
|
+
await $.state.set(FAIXA, null, { ifVersion: anterior.version })
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
const fuso = fusoDaSessao()
|
|
78
|
+
const leu = e.changed.includes('rateLimits')
|
|
79
|
+
semLeituraNova = !leu
|
|
80
|
+
let serie = await $.store.get(CHAVE_DA_SERIE)
|
|
81
|
+
if (leu) {
|
|
82
|
+
const motivo = problemaDaLeitura(e.rateLimits)
|
|
83
|
+
if (motivo !== null) problema = { motivo, em: agora, daLeitura: true }
|
|
84
|
+
// A medicao sem a lista de janelas nao apaga a serie guardada (D15).
|
|
85
|
+
if (Array.isArray(e.rateLimits)) {
|
|
86
|
+
serie = registrarLeituras(serie, e.rateLimits, agora)
|
|
87
|
+
await $.store.set(CHAVE_DA_SERIE, serie)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const previsoes = prever(serie, agora)
|
|
91
|
+
const alerta = previsoes.some(emAlerta)
|
|
92
|
+
const guardados = await $.state.get(NAO_CABEM)
|
|
93
|
+
let naoCabem: ClimaNaoCabe[] = alerta ? naoCabemGuardados(guardados.value) : []
|
|
94
|
+
if (alerta && leu) {
|
|
95
|
+
try {
|
|
96
|
+
const fila = retomadasDaFila(await $.ork.ler({ consulta: 'retry.list' }))
|
|
97
|
+
if (fila !== null) {
|
|
98
|
+
const antes = naoCabem
|
|
99
|
+
const lidaEm = agora
|
|
100
|
+
naoCabem = cruzar(previsoes, fila, lidaEm).filter(d => d.classe === 'nao-cabe').map(d =>
|
|
101
|
+
antes.find(n => n.id === d.retomada.id)
|
|
102
|
+
?? { id: d.retomada.id, thread: d.retomada.thread, texto: textoDoNaoCabe(d, fuso, lidaEm), registrada: false })
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Fila indisponivel: vale o que a ultima leitura deixou, e o `/clima` diz o motivo quando le a fila.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
let versao = guardados.version
|
|
109
|
+
if (JSON.stringify(naoCabem) !== JSON.stringify(guardados.value ?? [])) {
|
|
110
|
+
versao = (await $.state.set(NAO_CABEM, naoCabem, { ifVersion: guardados.version })).version
|
|
111
|
+
}
|
|
112
|
+
const threads = naoCabem.map(n => n.thread).filter((t, i, todas) => todas.indexOf(t) === i)
|
|
113
|
+
const texto = textoDaFaixa(previsoes, fuso, agora, threads)
|
|
114
|
+
let mostrada = (anterior.value ?? null) === texto
|
|
115
|
+
if (!mostrada) {
|
|
116
|
+
const r = await $.ork.faixa(texto === null ? { mod: MOD } : { mod: MOD, texto })
|
|
117
|
+
if (texto === null || r.publicada) {
|
|
118
|
+
await $.state.set(FAIXA, texto, { ifVersion: anterior.version })
|
|
119
|
+
mostrada = true
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (mostrada && texto !== null && naoCabem.some(n => !n.registrada)) {
|
|
123
|
+
const registradas: ClimaNaoCabe[] = []
|
|
124
|
+
for (const n of naoCabem) {
|
|
125
|
+
if (n.registrada) {
|
|
126
|
+
registradas.push(n)
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await $.ork.mostrado({ mod: MOD, canal: 'faixa', chave: `clima:nao-cabe:${n.id}`, thread: n.thread, texto: n.texto })
|
|
131
|
+
registradas.push({ ...n, registrada: true })
|
|
132
|
+
} catch (erro) {
|
|
133
|
+
registradas.push(n)
|
|
134
|
+
if (problema === null) problema = { motivo: `o registro do mostrado na ponte falhou (${motivoDoErro(erro)})`, em: agora, daLeitura: false }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
naoCabem = registradas
|
|
138
|
+
await $.state.set(NAO_CABEM, naoCabem, { ifVersion: versao })
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch (erro) {
|
|
142
|
+
problema = { motivo: `a medição falhou (${motivoDoErro(erro)})`, em: agora, daLeitura: false }
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const guardado = await $.state.get(PROBLEMA)
|
|
146
|
+
const antes = guardado.value ?? null
|
|
147
|
+
const depois = problema ?? (semLeituraNova && antes !== null && antes.daLeitura ? antes : null)
|
|
148
|
+
if (JSON.stringify(depois) !== JSON.stringify(antes)) {
|
|
149
|
+
await $.state.set(PROBLEMA, depois, { ifVersion: guardado.version })
|
|
150
|
+
// So o motivo novo vai ao transcript: a falha que se repete a cada medicao nao enche a tela.
|
|
151
|
+
if (depois !== null && depois.motivo !== antes?.motivo) $.ui.log(`Clima de Cota: ${depois.motivo}. O /clima mostra o que ficou guardado.`)
|
|
152
|
+
}
|
|
153
|
+
} catch (erro) {
|
|
154
|
+
$.ui.log(`Clima de Cota: o problema da medição não foi guardado (${motivoDoErro(erro)}).`, { to: 'debug' })
|
|
155
|
+
}
|
|
156
|
+
return next(e)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// O `/clima` le a serie guardada e nao grava ponto: o uso pedido na hora repete a ultima resposta, e um ponto
|
|
160
|
+
// com a hora de agora achataria o ritmo (D2). Le a fila, o retrato, as threads abertas e o registro de contas
|
|
161
|
+
// ao mesmo tempo (D3, D13); leitura que falha vira aviso no texto, o problema da ultima medicao aparece (D15), e
|
|
162
|
+
// o que foi mostrado vai ao registro da ponte (D8).
|
|
163
|
+
on('command.run', { command: 'clima' }, async $ => {
|
|
164
|
+
const agora = await $.clock.now()
|
|
165
|
+
const fuso = fusoDaSessao()
|
|
166
|
+
let papel: string
|
|
167
|
+
try {
|
|
168
|
+
papel = await $.ork.papel()
|
|
169
|
+
} catch {
|
|
170
|
+
return { text: TEXTO_SEM_PONTE }
|
|
171
|
+
}
|
|
172
|
+
if (papel !== 'dono') return { text: textoForaDoDono(papel) }
|
|
173
|
+
const previsoes = prever(await $.store.get(CHAVE_DA_SERIE), agora)
|
|
174
|
+
const problema = (await $.state.get(PROBLEMA)).value ?? null
|
|
175
|
+
const ler = (p: Promise<unknown>) => p.then(valor => ({ ok: true as const, valor }), erro => ({ ok: false as const, erro: motivoDoErro(erro) }))
|
|
176
|
+
const [fila, retrato, abertas, registro] = await Promise.all([
|
|
177
|
+
ler($.ork.ler({ consulta: 'retry.list' })),
|
|
178
|
+
ler($.ork.retrato().then(r => r.retrato)),
|
|
179
|
+
ler($.ork.ler({ consulta: 'thread.list' })),
|
|
180
|
+
ler($.ork.ler({ consulta: 'accounts.esgotamentos' })),
|
|
181
|
+
])
|
|
182
|
+
const retomadas = fila.ok ? retomadasDaFila(fila.valor) : null
|
|
183
|
+
const despachos = retomadas === null ? null : cruzar(previsoes, retomadas, agora)
|
|
184
|
+
// Sem a lista de threads abertas, as paradas nao sao conferidas: o retrato tambem traz thread fechada (D14).
|
|
185
|
+
const listaDeAbertas = abertas.ok ? threadsAbertas(abertas.valor) : null
|
|
186
|
+
const linhas = linhasDoCruzamento({
|
|
187
|
+
despachos, erroDaFila: fila.ok ? null : fila.erro,
|
|
188
|
+
paradas: retrato.ok && listaDeAbertas !== null ? paradasDoRetrato(retrato.valor, listaDeAbertas) : null,
|
|
189
|
+
erroDoRetrato: !retrato.ok ? retrato.erro
|
|
190
|
+
: listaDeAbertas === null ? `sem a lista de threads abertas: ${abertas.ok ? 'fora do formato' : abertas.erro}` : null,
|
|
191
|
+
marcas: registro.ok ? marcasDoRelatorio(registro.valor) : null, erroDoRegistro: registro.ok ? null : registro.erro,
|
|
192
|
+
}, previsoes, fuso, agora)
|
|
193
|
+
const naoCabem = (despachos ?? []).filter(d => d.classe === 'nao-cabe')
|
|
194
|
+
try {
|
|
195
|
+
await $.ork.mostrado({ mod: MOD, canal: 'transcript', chave: 'clima:consulta', texto: resumoDoClima(previsoes, threadsQueNaoCabem(naoCabem), fuso, agora) })
|
|
196
|
+
for (const d of naoCabem) {
|
|
197
|
+
await $.ork.mostrado({ mod: MOD, canal: 'transcript', chave: `clima:nao-cabe:${d.retomada.id}`, thread: d.retomada.thread,
|
|
198
|
+
texto: textoDoNaoCabe(d, fuso, agora) })
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
// O registro do mostrado e melhor esforco: o texto chega ao dono de qualquer jeito, e cada `/clima` registra de novo.
|
|
202
|
+
}
|
|
203
|
+
return { text: textoDoClima(previsoes, fuso, agora, linhas, problema) }
|
|
204
|
+
})
|
|
205
|
+
}
|