@aksp/opencrew 1.6.3 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +50 -5
  3. package/package.json +1 -1
  4. package/templates/AGENTS.md +20 -6
  5. package/templates/_opencrew/.opencrew-version +1 -1
  6. package/templates/_opencrew/core/architect.agent.yaml +1 -1
  7. package/templates/_opencrew/core/escritorio/animacao.js +64 -0
  8. package/templates/_opencrew/core/escritorio/app.js +137 -0
  9. package/templates/_opencrew/core/escritorio/cena.js +132 -0
  10. package/templates/_opencrew/core/escritorio/demo.js +79 -0
  11. package/templates/_opencrew/core/escritorio/escala.js +27 -0
  12. package/templates/_opencrew/core/escritorio/index.html +166 -0
  13. package/templates/_opencrew/core/escritorio/modelo-agentes.js +93 -0
  14. package/templates/_opencrew/core/escritorio/modelo-estado.js +71 -0
  15. package/templates/_opencrew/core/escritorio/modelo-mesas.js +81 -0
  16. package/templates/_opencrew/core/escritorio/modelo-pagina.js +95 -0
  17. package/templates/_opencrew/core/escritorio/modelo-textos.js +65 -0
  18. package/templates/_opencrew/core/escritorio/modelo-visao.js +91 -0
  19. package/templates/_opencrew/core/escritorio/modelo.js +29 -0
  20. package/templates/_opencrew/core/escritorio/painel.js +120 -0
  21. package/templates/_opencrew/core/escritorio/quadro.js +106 -0
  22. package/templates/_opencrew/core/escritorio/rota.js +62 -0
  23. package/templates/_opencrew/core/escritorio/rotulos.js +78 -0
  24. package/templates/_opencrew/core/escritorio/sprites-mesa.js +122 -0
  25. package/templates/_opencrew/core/escritorio/sprites-sala.js +92 -0
  26. package/templates/_opencrew/core/escritorio/sprites.js +187 -0
  27. package/templates/_opencrew/core/prompts/build.prompt.md +3 -3
  28. package/templates/_opencrew/core/prompts/discovery.prompt.md +1 -1
  29. package/templates/_opencrew/core/prompts/repair.prompt.md +7 -12
  30. package/templates/_opencrew/core/runner.pipeline.md +124 -223
  31. package/templates/_opencrew/core/scripts/caminho/argumentos.mjs +49 -0
  32. package/templates/_opencrew/core/scripts/caminho/disco.mjs +36 -0
  33. package/templates/_opencrew/core/scripts/caminho/nucleo.mjs +61 -0
  34. package/templates/_opencrew/core/scripts/caminho.mjs +124 -0
  35. package/templates/_opencrew/core/scripts/escritorio/leitura.mjs +31 -0
  36. package/templates/_opencrew/core/scripts/escritorio/porta.mjs +98 -0
  37. package/templates/_opencrew/core/scripts/escritorio/projeto.mjs +29 -0
  38. package/templates/_opencrew/core/scripts/escritorio/servidor.mjs +78 -0
  39. package/templates/_opencrew/core/scripts/escritorio.mjs +117 -0
  40. package/templates/_opencrew/core/scripts/estado/argumentos.mjs +61 -0
  41. package/templates/_opencrew/core/scripts/estado/arquivo.mjs +53 -0
  42. package/templates/_opencrew/core/scripts/estado/decisao.mjs +56 -0
  43. package/templates/_opencrew/core/scripts/estado/elenco.mjs +58 -0
  44. package/templates/_opencrew/core/scripts/estado/nucleo.mjs +113 -0
  45. package/templates/_opencrew/core/scripts/estado/preferencia.mjs +24 -0
  46. package/templates/_opencrew/core/scripts/estado.mjs +96 -0
@@ -0,0 +1,65 @@
1
+ // Textos que a página mostra, em português do Brasil: os da §6 da spec, o status por extenso de
2
+ // cada agente e o título da aba. A página os grava só por `textContent`.
3
+ // Puro: sem DOM, sem relógio, sem rede.
4
+ // Spec: fase-e1-escritorio-ao-vivo.md, §6 e regras 18, 21, 24 e 29 (repositório do OpenCrew).
5
+
6
+ export const TEXTOS = Object.freeze({
7
+ nome: 'Escritório',
8
+ aguardando: 'Aguardando você',
9
+ abrindo: 'Abrindo o escritório… Se esta mensagem não sumir, abra pelo endereço que o comando /opencrew dashboard mostrou, e não pelo arquivo.',
10
+ semServidor: 'Sem conexão com o escritório. Tentando de novo… Para reabrir, rode /opencrew dashboard.',
11
+ demonstracao: 'Demonstração — nenhuma execução ainda. Rode uma crew e ela aparece aqui.',
12
+ demoForcada: 'Demonstração. Tire ?demo do endereço para ver a sua crew.',
13
+ });
14
+
15
+ /** Status do agente por extenso, para a lista ao lado do desenho (regra 24). */
16
+ export const STATUS_TEXTO = Object.freeze({
17
+ idle: 'Em espera',
18
+ working: 'Trabalhando',
19
+ checkpoint: TEXTOS.aguardando,
20
+ done: 'Concluído',
21
+ skipped: 'Pulado',
22
+ failed: 'Falhou',
23
+ });
24
+
25
+ /** "Passo 2 de 5"; sem total, "Passo 2". */
26
+ export function textoPasso(atual, total) {
27
+ return total ? `Passo ${atual} de ${total}` : `Passo ${atual}`;
28
+ }
29
+
30
+ /** Faixa do checkpoint: "Aguardando você: <rótulo>"; sem rótulo, "Aguardando você". */
31
+ export function textoFaixa(rotulo) {
32
+ return rotulo ? `${TEXTOS.aguardando}: ${rotulo}` : TEXTOS.aguardando;
33
+ }
34
+
35
+ /** "há 19 min" até 59 minutos; depois, em horas inteiras: "há 1 h". */
36
+ function textoIdade(minutos) {
37
+ return minutos <= 59 ? `há ${minutos} min` : `há ${Math.floor(minutos / 60)} h`;
38
+ }
39
+
40
+ export const textoParada = (minutos) => `Última atualização ${textoIdade(minutos)}`;
41
+ export const textoSemSinal = (minutos) => `Sem sinal ${textoIdade(minutos)}`;
42
+
43
+ const TITULO_FIXO = Object.freeze({
44
+ checkpoint: `(!) ${TEXTOS.aguardando}`,
45
+ completed: '✓ Concluída',
46
+ failed: '✗ Falhou',
47
+ });
48
+
49
+ /** "2/5 Escrever"; sem rótulo, "Passo 2 de 5"; sem total saem o "/5" e o " de 5". */
50
+ function tituloDoPasso({ current, total, label }) {
51
+ if (!label) return textoPasso(current, total);
52
+ return `${current}${total ? `/${total}` : ''} ${label}`;
53
+ }
54
+
55
+ /**
56
+ * Título da aba (regra 29), para a página gravar quando mudar.
57
+ * @param {object|null} estado o estado normalizado da crew em exibição; `null`: sem estado
58
+ * @param {{ demo?: boolean, semConexao?: boolean }} [opcoes]
59
+ * @returns {string} "Escritório" em demonstração, sem estado ou sem conexão
60
+ */
61
+ export function titulo(estado, { demo = false, semConexao = false } = {}) {
62
+ if (!estado || demo || semConexao) return TEXTOS.nome;
63
+ const inicio = TITULO_FIXO[estado.status] ?? tituloDoPasso(estado.step);
64
+ return estado.crew ? `${inicio} — ${estado.crew}` : inicio;
65
+ }
@@ -0,0 +1,91 @@
1
+ // O modelo de uma leitura: do estado bruto de uma crew ao que a página desenha e escreve.
2
+ // A hora atual e o "reduzir movimento" entram por parâmetro; nada aqui lê relógio.
3
+ // Puro: sem DOM, sem relógio, sem rede.
4
+ // Spec: fase-e1-escritorio-ao-vivo.md, regras 18, 21, 24 e 29 (repositório do OpenCrew).
5
+ import { normalizar } from './modelo-estado.js';
6
+ import { verAgente } from './modelo-agentes.js';
7
+ import { textoFaixa, textoParada, textoPasso, textoSemSinal, titulo } from './modelo-textos.js';
8
+
9
+ const MINUTO = 60000;
10
+ /** Execução `running` sem atualização há mais que isto: a página diz há quanto tempo. */
11
+ export const PARADA_MS = 2 * MINUTO;
12
+ /** Há mais que isto: quem trabalhava recebe a ação `sem-sinal`. */
13
+ export const SEM_SINAL_MS = 20 * MINUTO;
14
+
15
+ /**
16
+ * Regra 21. Só vale para execução `running`, fora da demonstração e com `updatedAt` válido.
17
+ * Nada é regravado: são textos e uma ação derivados da idade, refeitos a cada leitura.
18
+ */
19
+ function sinais(estado, { agoraMs, demo }) {
20
+ const vale = !demo && estado.status === 'running' && estado.updatedAtMs !== null && Number.isFinite(agoraMs);
21
+ const idade = vale ? agoraMs - estado.updatedAtMs : 0;
22
+ const minutos = Math.floor(idade / MINUTO);
23
+ return {
24
+ parada: idade > PARADA_MS ? textoParada(minutos) : null,
25
+ semSinal: idade > SEM_SINAL_MS ? textoSemSinal(minutos) : null,
26
+ };
27
+ }
28
+
29
+ /** A última passagem de bastão, com o nome de quem entregou e de quem recebeu. */
30
+ function verPassagem({ handoff, agents }) {
31
+ if (!handoff) return null;
32
+ const nome = (id) => agents.find((a) => a.id === id)?.name ?? id;
33
+ return {
34
+ de: nome(handoff.from),
35
+ para: nome(handoff.to),
36
+ mensagem: handoff.message,
37
+ deId: handoff.from,
38
+ paraId: handoff.to,
39
+ completedAt: handoff.completedAt,
40
+ };
41
+ }
42
+
43
+ const SEM_ESTADO = Object.freeze({ semEstado: true, crew: '', execucao: null, passo: null, faixa: null, parada: null, passagem: null, motivo: null });
44
+
45
+ function verEstado(estado, { parada, semSinal }, reduzirMovimento) {
46
+ const { current, total, label } = estado.step;
47
+ const contagem = textoPasso(current, total);
48
+ const contexto = { execucao: estado.status, semSinal, reduzirMovimento, balaoDoPasso: label || contagem };
49
+ return {
50
+ semEstado: false,
51
+ crew: estado.crew,
52
+ execucao: estado.status,
53
+ passo: { atual: current, total, rotulo: label, contagem },
54
+ faixa: estado.status === 'checkpoint' ? textoFaixa(label) : null,
55
+ parada,
56
+ agentes: estado.agents.map((agente, i) => verAgente(agente, i, contexto)),
57
+ passagem: verPassagem(estado),
58
+ motivo: estado.status === 'failed' && estado.motivo ? estado.motivo : null,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * O que a página mostra de uma crew. Nunca lança erro e não altera o objeto recebido.
64
+ * @param {unknown} bruto o estado como veio do servidor (ou da demonstração)
65
+ * @param {object} [opcoes]
66
+ * @param {number} [opcoes.agoraMs] a hora atual, em milissegundos; sem ela não há "sem sinal"
67
+ * @param {boolean} [opcoes.reduzirMovimento] o sistema pede menos movimento
68
+ * @param {boolean} [opcoes.demo] o estado é o da demonstração
69
+ * @param {boolean} [opcoes.semConexao] a última consulta ao servidor falhou
70
+ * @param {string} [opcoes.crew] o nome com que o servidor lista a crew; vale mais que o do estado
71
+ * @returns {object} `{ semEstado, demo, semConexao, reduzirMovimento, titulo, crew, execucao,
72
+ * passo, faixa, parada, agentes, passagem, motivo }` · `semEstado`: a entrada não era um
73
+ * estado (aí `agentes` é vazio e o resto, `null`) · `execucao`: `running`, `checkpoint`,
74
+ * `completed` ou `failed` · `passo`: `{ atual, total, rotulo, contagem }` · `faixa`: o texto do
75
+ * checkpoint ou `null` · `parada`: "Última atualização há …" ou `null` · `agentes`: todos, na
76
+ * ordem do elenco (ver `verAgente`); é a lista ao lado do desenho, e quem tem `mesa` é
77
+ * desenhado · `passagem`: `{ de, para, mensagem, deId, paraId, completedAt }` ou `null` ·
78
+ * `motivo`: só em execução `failed`
79
+ */
80
+ export function montarModelo(bruto, opcoes = {}) {
81
+ const { reduzirMovimento = false, demo = false, semConexao = false } = opcoes;
82
+ const base = { demo, semConexao, reduzirMovimento };
83
+ const lido = normalizar(bruto);
84
+ if (!lido) return { ...SEM_ESTADO, ...base, agentes: [], titulo: titulo(null) };
85
+ const estado = { ...lido, crew: opcoes.crew || lido.crew };
86
+ return {
87
+ ...verEstado(estado, sinais(estado, opcoes), reduzirMovimento),
88
+ ...base,
89
+ titulo: titulo(estado, { demo, semConexao }),
90
+ };
91
+ }
@@ -0,0 +1,29 @@
1
+ // Núcleo puro da página do escritório: tudo o que ela desenha e escreve sai daqui, calculado a
2
+ // partir do estado que o servidor devolve. Nenhum módulo deste núcleo toca no DOM, lê relógio ou
3
+ // usa a rede: a hora atual, o "reduzir movimento" e o `?demo` entram por parâmetro, e por isso
4
+ // os mesmos módulos são testados direto no Node.
5
+ // Este arquivo é só a fachada: cada módulo irmão cuida de uma parte.
6
+ // Spec: fase-e1-escritorio-ao-vivo.md, regras 16 a 24, 28 e 29 (repositório do OpenCrew).
7
+
8
+ // Geometria: a tela de 320×180, o boneco de 16 e as 12 mesas.
9
+ export * from './modelo-mesas.js';
10
+ // Textos da página em PT-BR e o título da aba.
11
+ export * from './modelo-textos.js';
12
+ // Leitura do `state.json`, inclusive o da 1.6.x.
13
+ export * from './modelo-estado.js';
14
+ // Ação, monitor, balão e aparência de cada agente.
15
+ export * from './modelo-agentes.js';
16
+ // O modelo de uma leitura: `montarModelo`.
17
+ export * from './modelo-visao.js';
18
+ // Qual crew está na tela e a página inteira: `consultar`, `escolher`, `montarPagina`.
19
+ export * from './modelo-pagina.js';
20
+ // O roteiro da demonstração.
21
+ export * from './demo.js';
22
+ // A escala inteira do canvas, em pixels do dispositivo: `escala`.
23
+ export * from './escala.js';
24
+ // O caminho de uma entrega, pelos vãos entre as mesas: `rota`.
25
+ export * from './rota.js';
26
+ // O que anima, pela diferença entre duas leituras da mesma crew: `transicao`.
27
+ export * from './animacao.js';
28
+ // Pose, posição e monitor de cada boneco num instante: `quadro`.
29
+ export * from './quadro.js';
@@ -0,0 +1,120 @@
1
+ // O painel em volta do desenho: o nome da crew, o passo com a barra de progresso, os avisos, a
2
+ // faixa do checkpoint, a última passagem de bastão, o seletor de crews e a lista dos agentes
3
+ // (regras 22 a 24 e 29). Tudo o que vem do estado — nome, rótulo, mensagem, motivo — entra por
4
+ // `textContent`: é texto, nunca marcação. Cada parte só é reescrita quando muda.
5
+ // Spec: fase-e1-escritorio-ao-vivo.md, regras 18 e 20 a 24 (repositório do OpenCrew).
6
+ import { TEXTOS } from './modelo.js';
7
+
8
+ /** Os elementos de `index.html` que o painel escreve. */
9
+ const IDS = [
10
+ 'pagina', 'crew', 'situacao', 'passo', 'barra', 'barra-feito', 'barra-atual', 'barra-resto', 'aviso', 'frase',
11
+ 'faixa', 'etiqueta', 'parada', 'motivo', 'passagem', 'lista', 'seletor', 'seletor-caixa',
12
+ ];
13
+ /** A situação da execução, por extenso. */
14
+ const SITUACAO = Object.freeze({ running: 'Em andamento', checkpoint: TEXTOS.aguardando, completed: 'Concluída', failed: 'Falhou' });
15
+ /** A marca de cada ação na lista: a forma muda junto com a cor. */
16
+ const MARCAS = Object.freeze({ idle: '○', working: '▶', checkpoint: '▲', done: '✓', skipped: '–', failed: '✕', 'sem-sinal': '?' });
17
+
18
+ /** Escreve o texto no elemento e o esconde quando não há o que mostrar. */
19
+ function escrever(elemento, texto) {
20
+ const novo = texto || '';
21
+ if (elemento.textContent !== novo) elemento.textContent = novo;
22
+ elemento.hidden = novo === '';
23
+ }
24
+
25
+ /** Um elemento novo com classe e texto; `enfeite`: fora do alcance do leitor de tela. */
26
+ function criar(documento, marca, { classe, texto = '', enfeite = false }) {
27
+ const elemento = documento.createElement(marca);
28
+ elemento.className = classe;
29
+ elemento.textContent = texto;
30
+ if (enfeite) elemento.setAttribute('aria-hidden', 'true');
31
+ return elemento;
32
+ }
33
+
34
+ /**
35
+ * A linha de um agente: a cor da camisa (a mesma do boneco), o ícone, o nome, o status e o que fez.
36
+ * A marca segue o status do agente, não a ação do boneco (regra 18: no checkpoint a lista não
37
+ * muda); só "sem sinal" a troca.
38
+ */
39
+ function criarLinha(documento, agente) {
40
+ const novo = (marca, opcoes) => criar(documento, marca, opcoes);
41
+ const item = novo('li', { classe: 'agente' });
42
+ const cor = novo('span', { classe: 'cor', enfeite: true });
43
+ const quem = novo('div', { classe: 'quem' });
44
+ const status = novo('span', { classe: 'status' });
45
+ const marca = agente.acao === 'sem-sinal' ? 'sem-sinal' : agente.status;
46
+ item.dataset.acao = marca;
47
+ cor.style.background = agente.aparencia.camisa ?? 'transparent';
48
+ status.append(novo('span', { classe: 'sinal', texto: MARCAS[marca], enfeite: true }), novo('span', { classe: 'texto', texto: agente.statusTexto }));
49
+ quem.append(novo('strong', { classe: 'nome', texto: agente.nome }), status);
50
+ if (agente.label) quem.append(novo('span', { classe: 'fez', texto: agente.label }));
51
+ item.append(cor, novo('span', { classe: 'icone', texto: agente.icone, enfeite: true }), quem);
52
+ return item;
53
+ }
54
+
55
+ function criarOpcao(documento, crew) {
56
+ return Object.assign(criar(documento, 'option', { classe: '', texto: crew }), { value: crew });
57
+ }
58
+
59
+ const textoDoPasso = ({ contagem, rotulo }) => (rotulo ? `${contagem} — ${rotulo}` : contagem);
60
+ const textoDaPassagem = ({ de, para, mensagem }) => `Última passagem de bastão: ${de} → ${para}${mensagem ? ` — ${mensagem}` : ''}`;
61
+
62
+ /**
63
+ * O painel da página.
64
+ * @param {object} documento a página (`getElementById`, `createElement`, `title`)
65
+ * @param {(crew: string) => void} aoEscolher chamada quando o usuário troca de crew no seletor
66
+ * @returns {{ atualizar: (modelo: object) => void }} `atualizar` recebe o que `montarPagina` devolveu
67
+ */
68
+ export function criarPainel(documento, aoEscolher) {
69
+ const el = Object.fromEntries(IDS.map((id) => [id, documento.getElementById(id)]));
70
+ const escrito = { crews: '', agentes: '' };
71
+ el.seletor.addEventListener('change', () => aoEscolher(el.seletor.value));
72
+
73
+ /** A barra em três partes: passos feitos, o passo atual e o que falta. */
74
+ function progresso({ passo, execucao }) {
75
+ const total = passo?.total ?? 0;
76
+ el.barra.hidden = total === 0;
77
+ if (!total) return;
78
+ const feitos = execucao === 'completed' ? total : Math.min(total, Math.max(0, passo.atual - 1));
79
+ const atual = feitos < total && passo.atual >= 1 ? 1 : 0;
80
+ el.barra.style.setProperty('--total', String(total));
81
+ [feitos, atual, total - feitos - atual].forEach((parte, i) => {
82
+ el[['barra-feito', 'barra-atual', 'barra-resto'][i]].style.flexGrow = String(parte);
83
+ });
84
+ }
85
+
86
+ /** O seletor só aparece com mais de uma crew (regra 22). */
87
+ function seletor({ crews, crew }) {
88
+ const novo = JSON.stringify(crews);
89
+ el['seletor-caixa'].hidden = crews.length < 2;
90
+ if (novo !== escrito.crews) el.seletor.replaceChildren(...crews.map((nome) => criarOpcao(documento, nome)));
91
+ escrito.crews = novo;
92
+ if (crews.includes(crew) && el.seletor.value !== crew) el.seletor.value = crew;
93
+ }
94
+
95
+ /** A lista traz todo agente, com ou sem mesa: é ela que vale para o leitor de tela (regra 24). */
96
+ function lista({ agentes }) {
97
+ const novo = JSON.stringify(agentes.map((a) => [a.id, a.nome, a.icone, a.acao, a.statusTexto, a.label, a.aparencia.camisa]));
98
+ if (novo !== escrito.agentes) el.lista.replaceChildren(...agentes.map((agente) => criarLinha(documento, agente)));
99
+ escrito.agentes = novo;
100
+ }
101
+
102
+ function atualizar(modelo) {
103
+ if (documento.title !== modelo.titulo) documento.title = modelo.titulo;
104
+ el.pagina.dataset.execucao = modelo.execucao ?? '';
105
+ el.etiqueta.hidden = modelo.modo !== 'demo';
106
+ escrever(el.crew, modelo.crew);
107
+ escrever(el.situacao, SITUACAO[modelo.execucao]);
108
+ escrever(el.passo, modelo.passo && textoDoPasso(modelo.passo));
109
+ escrever(el.aviso, modelo.aviso);
110
+ escrever(el.frase, modelo.frase);
111
+ escrever(el.faixa, modelo.faixa);
112
+ escrever(el.parada, modelo.parada);
113
+ escrever(el.motivo, modelo.motivo && `Motivo da falha: ${modelo.motivo}`);
114
+ escrever(el.passagem, modelo.passagem && textoDaPassagem(modelo.passagem));
115
+ progresso(modelo);
116
+ seletor(modelo);
117
+ lista(modelo);
118
+ }
119
+ return { atualizar };
120
+ }
@@ -0,0 +1,106 @@
1
+ // O quadro de um instante (regra 28): a pose e a posição inteira de cada boneco, e o monitor de
2
+ // cada mesa, calculados pelo tempo decorrido. Nada aqui conta quadros: o mesmo modelo no mesmo
3
+ // instante dá sempre o mesmo quadro, e o percurso cujo tempo já passou aparece terminado.
4
+ // Puro: sem DOM, sem relógio, sem rede. A hora entra por parâmetro.
5
+ // Spec: fase-e1-escritorio-ao-vivo.md, regras 16, 18, 24 e 28 (repositório do OpenCrew).
6
+ import { ANDAR_MS, COMEMORACAO_MS, DIGITAR_MS, IDA_MS, VOLTA_MS } from './animacao.js';
7
+ import { pontoDaRota, rota } from './rota.js';
8
+
9
+ /**
10
+ * As poses que `quadro` devolve. Cada uma é o molde do boneco com linhas trocadas (regra 16):
11
+ * `parado` é o molde; `digitar-a` e `digitar-b`, os braços no teclado; `mao`, o braço
12
+ * levantado; `andar-a` e `andar-b`, as pernas. O braço com papel vem à parte (`papel`), somado
13
+ * às pernas de quem anda.
14
+ */
15
+ export const POSES = Object.freeze(['parado', 'digitar-a', 'digitar-b', 'mao', 'andar-a', 'andar-b']);
16
+ /** O monitor animado avança um passo a cada 250 ms: os 4 passos dão a volta em 1 segundo. */
17
+ export const MONITOR_MS = 250;
18
+ const PASSOS_DO_MONITOR = 4;
19
+ /** Quanto sobe, em pixels lógicos, quem comemora com o braço levantado. */
20
+ export const PULO = 2;
21
+
22
+ /** Em que passo de um ciclo de `total` cai o instante, quando o passo troca a cada `periodo`. */
23
+ function fase(ms, periodo, total = 2) {
24
+ return Number.isFinite(ms) && ms > 0 ? Math.floor(ms / periodo) % total : 0;
25
+ }
26
+
27
+ /** Tempo desde o início de uma animação; `null` se não há, não começou ou já terminou. */
28
+ function decorrido(marca, duracao, agoraMs) {
29
+ const ms = marca ? agoraMs - marca.inicioMs : Number.NaN;
30
+ return ms >= 0 && ms < duracao ? ms : null;
31
+ }
32
+
33
+ /** O âmbar pisca 1 vez por segundo (apaga na segunda metade); o aceso corre as linhas pelo passo. */
34
+ function monitorDe(agente, agoraMs) {
35
+ const monitorFase = agente.monitorAnimado ? fase(agoraMs, MONITOR_MS, PASSOS_DO_MONITOR) : 0;
36
+ const apagou = agente.monitor === 'ambar' && monitorFase >= PASSOS_DO_MONITOR / 2;
37
+ return { monitor: apagou ? 'apagado' : agente.monitor, monitorFase };
38
+ }
39
+
40
+ function poseNaMesa(acao, agoraMs, semMovimento) {
41
+ if (acao === 'checkpoint') return 'mao';
42
+ if (acao !== 'working') return 'parado';
43
+ return semMovimento || fase(agoraMs, DIGITAR_MS) === 0 ? 'digitar-a' : 'digitar-b';
44
+ }
45
+
46
+ /** O boneco sentado na própria mesa, como o status manda. */
47
+ function naMesa(agente, agoraMs, semMovimento) {
48
+ const { id, indice, mesa, aparencia, acao } = agente;
49
+ return {
50
+ id, indice, mesa, aparencia, acao,
51
+ pose: poseNaMesa(acao, agoraMs, semMovimento),
52
+ papel: false,
53
+ espelhado: false,
54
+ sentado: true,
55
+ x: mesa.boneco.x,
56
+ y: mesa.boneco.y,
57
+ destino: null,
58
+ ...monitorDe(agente, agoraMs),
59
+ };
60
+ }
61
+
62
+ /** Quem está no meio de uma entrega e o que muda no boneco dele; `null` se ninguém anda. */
63
+ function caminhada(entrega, agentes, agoraMs) {
64
+ const ms = decorrido(entrega, IDA_MS + VOLTA_MS, agoraMs);
65
+ if (ms === null) return null;
66
+ const [de, para] = [entrega.deId, entrega.paraId].map((id) => agentes.find((a) => a?.id === id));
67
+ if (!de?.mesa || !para?.mesa) return null;
68
+ const ida = rota(de.mesa.indice, para.mesa.indice);
69
+ const onde = ms <= IDA_MS ? pontoDaRota(ida, ms / IDA_MS) : pontoDaRota([...ida].reverse(), (ms - IDA_MS) / VOLTA_MS);
70
+ const pose = fase(ms, ANDAR_MS) === 0 ? 'andar-a' : 'andar-b';
71
+ return { indice: de.indice, boneco: { ...onde, acao: 'entregando', pose, papel: ms < IDA_MS, sentado: false, destino: para.mesa.indice } };
72
+ }
73
+
74
+ /** Comemora na própria mesa: braço levantado e um pulo, depois o molde, a cada 0,3 s. */
75
+ function comemorar(boneco, ms) {
76
+ const noAlto = fase(ms, DIGITAR_MS) === 0;
77
+ return { ...boneco, acao: 'comemorando', pose: noAlto ? 'mao' : 'parado', y: boneco.y - (noAlto ? PULO : 0) };
78
+ }
79
+
80
+ /**
81
+ * O que a cena pinta num instante: um boneco por agente com mesa, na ordem do elenco.
82
+ * @param {object} modelo o modelo de uma leitura; para haver entrega e comemoração, o que
83
+ * `transicao` devolveu
84
+ * @param {number} agoraMs a hora, em milissegundos, no mesmo relógio passado a `transicao`
85
+ * @returns {object[]} cada boneco: `{ id, indice, mesa, aparencia, acao, pose, papel, espelhado,
86
+ * sentado, x, y, destino, monitor, monitorFase }` · `acao`: a do modelo, ou `entregando`
87
+ * (durante os 4 s da entrega) ou `comemorando` (durante os 4 s da comemoração; quem foi pulado
88
+ * não comemora) · `pose`: uma de `POSES` · `papel`: leva o papel (só na ida) · `espelhado`:
89
+ * desenhar o molde virado para a esquerda · `sentado`: está na própria mesa · `x`, `y`: canto
90
+ * superior esquerdo do boneco, em pixel lógico inteiro · `destino`: índice da mesa de quem
91
+ * recebe, ou `null` · `monitor`: a cor a pintar agora (`apagado`, `aceso`, `ambar` ou
92
+ * `vermelho`) · `monitorFase`: 0 a 3, o passo das linhas do monitor aceso (0 se ele não anima)
93
+ * · com `modelo.reduzirMovimento`: pose fixa e todos sentados, sem entrega nem comemoração
94
+ */
95
+ export function quadro(modelo, agoraMs) {
96
+ const todos = Array.isArray(modelo?.agentes) ? modelo.agentes : [];
97
+ const { entrega = null, comemoracao = null } = modelo?.animacao ?? {};
98
+ const semMovimento = Boolean(modelo?.reduzirMovimento);
99
+ const andando = semMovimento ? null : caminhada(entrega, todos, agoraMs);
100
+ const festa = semMovimento ? null : decorrido(comemoracao, COMEMORACAO_MS, agoraMs);
101
+ return todos.filter((agente) => agente?.mesa).map((agente) => {
102
+ const boneco = naMesa(agente, agoraMs, semMovimento);
103
+ if (andando?.indice === agente.indice) return { ...boneco, ...andando.boneco };
104
+ return festa !== null && agente.acao !== 'skipped' ? comemorar(boneco, festa) : boneco;
105
+ });
106
+ }
@@ -0,0 +1,62 @@
1
+ // O caminho de uma entrega (regra 28): do assento de quem entrega até a frente da mesa de quem
2
+ // recebe, calculado direto da grade, sem busca de caminho. O boneco sai pelo corredor de trás da
3
+ // própria mesa até o vão ao lado, anda pelo vão até o corredor da frente da mesa de destino e
4
+ // segue por ele. Só trechos horizontais e verticais, em pixel lógico e inteiro; nenhum encosta
5
+ // num tampo nem passa por cima de um colega sentado.
6
+ // Puro: sem DOM, sem relógio, sem rede.
7
+ // Spec: fase-e1-escritorio-ao-vivo.md, regra 28 (repositório do OpenCrew).
8
+ import { VAOS_X, mesaDe } from './modelo-mesas.js';
9
+
10
+ /**
11
+ * Índice (em `VAOS_X`) do vão por onde a entrega passa: o vizinho da coluna de origem, do lado
12
+ * do destino. Na mesma coluna, o da direita; na última coluna, o da esquerda.
13
+ */
14
+ function vaoDe(origem, destino) {
15
+ if (destino > origem) return origem;
16
+ if (destino < origem) return origem - 1;
17
+ return origem < VAOS_X.length ? origem : origem - 1;
18
+ }
19
+
20
+ /**
21
+ * A rota entre duas mesas, pelo canto superior esquerdo do boneco.
22
+ * @param {number} de índice da mesa de quem entrega (0 a 11)
23
+ * @param {number} para índice da mesa de quem recebe (0 a 11)
24
+ * @returns {{ x: number, y: number }[] | null} quatro pontos: o assento de quem entrega, a
25
+ * entrada do vão, a saída do vão e a frente da mesa de quem recebe; `null` se uma das mesas
26
+ * não existe
27
+ */
28
+ export function rota(de, para) {
29
+ const origem = mesaDe(de);
30
+ const destino = mesaDe(para);
31
+ if (!origem || !destino) return null;
32
+ const x = VAOS_X[vaoDe(origem.coluna, destino.coluna)];
33
+ return [{ ...origem.boneco }, { x, y: origem.boneco.y }, { x, y: destino.frente.y }, { ...destino.frente }];
34
+ }
35
+
36
+ const trechos = (pontos) => pontos.slice(1).map((fim, i) => [pontos[i], fim]);
37
+ const tamanho = ([a, b]) => Math.abs(b.x - a.x) + Math.abs(b.y - a.y);
38
+
39
+ /** Comprimento de uma rota, em pixels lógicos. */
40
+ export function comprimentoDaRota(pontos) {
41
+ return trechos(pontos).reduce((soma, trecho) => soma + tamanho(trecho), 0);
42
+ }
43
+
44
+ /**
45
+ * Onde está quem já andou uma fração da rota, em velocidade constante.
46
+ * @param {{ x: number, y: number }[]} pontos a rota (ou ela invertida, para a volta)
47
+ * @param {number} fracao de 0 (o primeiro ponto) a 1 (o último); fora disso vale o extremo
48
+ * @returns {{ x: number, y: number, espelhado: boolean }} posição inteira · `espelhado`: o
49
+ * último trecho horizontal andado foi para a esquerda
50
+ */
51
+ export function pontoDaRota(pontos, fracao) {
52
+ let resta = Math.round(comprimentoDaRota(pontos) * Math.max(0, fracao));
53
+ let espelhado = false;
54
+ for (const [a, b] of trechos(pontos)) {
55
+ if (a.x !== b.x) espelhado = b.x < a.x;
56
+ if (resta <= tamanho([a, b])) {
57
+ return { x: a.x + Math.sign(b.x - a.x) * resta, y: a.y + Math.sign(b.y - a.y) * resta, espelhado };
58
+ }
59
+ resta -= tamanho([a, b]);
60
+ }
61
+ return { ...pontos.at(-1), espelhado };
62
+ }
@@ -0,0 +1,78 @@
1
+ // Os rótulos sobre o desenho (regra 19): o nome de cada agente, sob a mesa dele, e o balão, sobre
2
+ // o boneco, são elementos da página numa camada por cima do canvas, escritos só por
3
+ // `textContent`. A posição vem das âncoras do modelo, em pixel lógico, e é aplicada em
4
+ // porcentagem de 320×180; cada rótulo tem a largura de uma coluna, e o CSS corta o texto que não
5
+ // cabe. A camada é `aria-hidden`: o leitor de tela lê a lista ao lado, que tem o texto inteiro.
6
+ // Spec: fase-e1-escritorio-ao-vivo.md, regras 18 e 19 (repositório do OpenCrew).
7
+ import { ALTURA, BONECO, LARGURA, LARGURA_COLUNA } from './modelo.js';
8
+
9
+ /** Altura, em pixels lógicos, da faixa que o nome ocupa sob a mesa. */
10
+ const ALTURA_DO_NOME = 10;
11
+ /** Quanto sobra do nome enquanto um boneco em pé está por cima dele. */
12
+ const QUASE_APAGADO = '0.15';
13
+
14
+ const porcento = (valor, total) => `${(valor / total) * 100}%`;
15
+
16
+ /** Um rótulo com a largura de uma coluna, centrado na âncora; o texto fica num `span`, que o CSS corta. */
17
+ function criarRotulo(documento, { classe, texto, ancora }) {
18
+ const caixa = documento.createElement('div');
19
+ const tinta = documento.createElement('span');
20
+ caixa.className = classe;
21
+ caixa.style.left = porcento(ancora.x - LARGURA_COLUNA / 2, LARGURA);
22
+ caixa.style.width = porcento(LARGURA_COLUNA, LARGURA);
23
+ tinta.textContent = texto;
24
+ caixa.append(tinta);
25
+ return caixa;
26
+ }
27
+
28
+ /** O nome (sempre) e o balão (quando o modelo traz um) de um agente com mesa. */
29
+ function criarItem(documento, { mesa, nome, balao, acao }) {
30
+ const item = { mesa, nome: criarRotulo(documento, { classe: 'nome', texto: nome, ancora: mesa.nome }), balao: null };
31
+ item.nome.style.top = porcento(mesa.nome.y, ALTURA);
32
+ if (balao === null) return item;
33
+ item.balao = criarRotulo(documento, { classe: acao === 'checkpoint' ? 'balao espera' : 'balao', texto: balao, ancora: mesa.balao });
34
+ item.balao.style.bottom = porcento(ALTURA - mesa.balao.y, ALTURA);
35
+ return item;
36
+ }
37
+
38
+ /** O boneco está sobre a faixa do nome desta mesa? */
39
+ function cobre(boneco, { nome }) {
40
+ const esquerda = nome.x - LARGURA_COLUNA / 2;
41
+ const naColuna = boneco.x < esquerda + LARGURA_COLUNA && boneco.x + BONECO > esquerda;
42
+ return naColuna && boneco.y < nome.y + ALTURA_DO_NOME && boneco.y + BONECO > nome.y;
43
+ }
44
+
45
+ function opacidade(elemento, valor) {
46
+ if (elemento.style.opacity !== valor) elemento.style.opacity = valor;
47
+ }
48
+
49
+ /**
50
+ * A camada de nomes e balões.
51
+ * @param {object} camada o elemento que fica sobre o canvas, do mesmo tamanho dele
52
+ * @param {object} documento de quem vem o `createElement`
53
+ * @returns {{ atualizar: (agentes: object[]) => void, acompanhar: (bonecos: object[]) => void }}
54
+ * `atualizar`: recebe os agentes do modelo a cada leitura e só refaz a camada quando um nome,
55
+ * um balão ou uma mesa mudou · `acompanhar`: recebe os bonecos de cada quadro; o nome sob um
56
+ * boneco em pé quase some, para não cobrir quem entrega, e o balão de quem saiu da mesa some
57
+ * até ele voltar
58
+ */
59
+ export function criarRotulos(camada, documento) {
60
+ let itens = [];
61
+ let escrito = '';
62
+ function atualizar(agentes) {
63
+ const comMesa = agentes.filter((agente) => agente.mesa);
64
+ const novo = JSON.stringify(comMesa.map((a) => [a.mesa.indice, a.nome, a.balao, a.acao]));
65
+ if (novo === escrito) return;
66
+ escrito = novo;
67
+ itens = comMesa.map((agente) => criarItem(documento, agente));
68
+ camada.replaceChildren(...itens.flatMap((item) => (item.balao ? [item.nome, item.balao] : [item.nome])));
69
+ }
70
+ function acompanhar(bonecos) {
71
+ const emPe = bonecos.filter((boneco) => !boneco.sentado);
72
+ for (const { mesa, nome, balao } of itens) {
73
+ opacidade(nome, emPe.some((boneco) => cobre(boneco, mesa)) ? QUASE_APAGADO : '');
74
+ if (balao) opacidade(balao, emPe.some((boneco) => boneco.indice === mesa.indice) ? '0' : '');
75
+ }
76
+ }
77
+ return { atualizar, acompanhar };
78
+ }