MOJ — Visão geral (comece por aqui) — MOJ docs

MOJ — Visão geral (comece por aqui)

O MOJ (Melhor/Meta Online Judge, moj.naquadah.com.br) é um juiz online escrito em bash. Este repositório é a v2 API-first: nginx + backend bash (fcgiwrap) + frontend estático modular, lendo o mesmo contests/<id>/ de sempre (sem migração de dados).

Fluxo de submissão/julgamento e como os daemons conversam: ver FLOW.md. Contrato de rotas: API.md (+ web/api/openapi.json). Formato do pacote de problema (orgs, coleções, metadados): PACOTE.md. Placar: SCOREBOARD.md. Deploy: DEPLOY.md. Plano original: PLAN.md.

Convenção de commit: mensagens em português, no presente, prefixadas pelo componente (ex.: problemas: …). O rodapé leva apenas Co-Authored-By:nunca uma linha Claude-Session: (é ruído no histórico). Vale também p/ assistentes de IA.

Documentação junto com o código (doc atrasada = bug): ao mudar comportamento ou contrato, atualize a doc no mesmo commit — rotas/campos em API.md e em ../web/api/openapi.json (mantenha os dois em sincronia); arquitetura/fluxo aqui e em FLOW.md. bash docs/build-html.sh refaz o HTML.

Estrutura

moj/
  server/          backend bash sob nginx + fcgiwrap
    api/v1/
      router.sh    front-controller único: PATH_INFO -> handlers/<segmentos>.sh
      lib/         ~30 libs: common.sh (resposta/JSON/validação/audit), auth.sh, users.sh, problems.sh,
                   orgs.sh, langs.sh (whitelist+CHÃO), tl-store.sh (TL+TLOVERRIDE), verdict.sh,
                   review.sh (veredicto manual), print.sh (fila/balões/escopo de sede),
                   cohorts.sh, registration.sh, contest-{create,docs,rounds,gate,offline}.sh,
                   webcast.sh, team-{photo,music}.sh, ua-gate.sh, telegram.sh, alerts.sh, …
      handlers/    auth/ index/ treino/ contest/ problems/ orgs/ judge/ submission/ admin/ ops/
                   + submit.sh   (1 arquivo por rota)
    daemons/       judged.sh (consumidor do spool, inotify)
    judge-gw/      sched-lib.sh (escalonador pull: registro+fila+claim) + judge.sh (mock/local, dev) + PULL.md
    score/         build.sh (recalcula placar), updatescore-*.sh (um por modo), stats-gen.sh,
                   report-gen.sh + relatorio-gen.sh (relatório offline), webcast-gen.sh (pacote do
                   telão), treino-list-gen.sh, problem-panorama-gen.sh, jplag-run.sh
    etc/           common.conf, nginx/, systemd/
  web/             frontend vanilla (ES modules, sem build), servido estático
    shared/        api.js auth.js ui.js editor.js charts(/lib) flags.js sonic.js contest-host/guard/shell.js contest-config/
    index/ contests/ status/ treino/ contest/  (home, arquivo de encerrados, status público, treino, contest)
  judge/           agente pull (moj-agent@pos/gpu/cm/hu, puxa job no heartbeat) — repo próprio
  mojtools/        sandbox bubblewrap + calibração + enunciados — repo IRMÃO (não vive sob judge/)
  mojinho-bot/     bot do Telegram (vira cliente da API)
  contests/<id>/   DADOS (conf, users/<login>/ (account.json+history+metrics+submissions), var/placar.txt, …) — fonte da verdade
  run/             estado de runtime (sessions/, spool/, results/, registry/, sockets)  [não versionado]
  docs/            esta documentação

Camada de API

Frontend

Vanilla ES modules, sem build, servido estático. shared/ concentra o cliente de API (fetch + Bearer + envelope), auth/token (localStorage), ui.js (el(), avatares, i18n pt/en — idioma pelo LOCALE do contest › ?lang= na URL › seletor do header › navegador; o ?lang= é o que faz um link mandado por e-mail abrir na versão certa), o editor CodeMirror 6 (bundle vendorizado em shared/vendor/codemirror/ — sem CDN, contest roda em LAN isolada; fallback textarea), os gráficos SVG build-free (/lib/charts.js), e os assets offline: bandeiras locais (shared/flags/, 271 países + 27 estados do BR) e GIFs do Sonic (shared/assets/sonic/). Editores de configuração de contest reaproveitáveis em shared/contest-config/ (cores, países/escolas, regiões, básico, settings/toggles (settings-editor.js), seletor de linguagens (lang-picker.js) e o painel de busca+sorteio do banco por coleção/tag/dificuldade (bank-panel.js)) — os mesmos na criação e no admin do contest.

O que existe (funcional)

Home & treino livre

Home com notícias, contests (abertos/por vir/encerrados; abre cada um pelo subdomínio), top10 e destaques; página pública /status/ (health: fila por lista, máquinas julgando, daemons). Treino livre (/treino/, UMA página com DOIS estados client-side após 1 fetch): hub (default) com omnibusca central de sugestões agrupadas (coleções/tags/problemas, com status pessoal), atalhos (aleatório/fáceis/não resolvidos), "Para você" (continue de onde parou + sugestão, do /treino/history-full), carrossel de coleções em destaque com barra de progresso pessoal (dedup por sobreposição de conjunto — "obi" × "Olimpíada Brasileira…") e "mais enviados na semana" (/treino/trending); busca avançada (?browse=1 ou qualquer filtro, deep-links ?searchcol= antigos continuam valendo) com trilho de facetas — status, dificuldade (derivada da taxa), árvore de coleções com multi-seleção OR (checkbox; searchcol aceita CSV) e progresso por nó, tags AND com contagens vivas — chips removíveis, ordenação (mais resolvidos/A–Z/dificuldade) e tabela sempre visível paginada; página do problema com enunciado + editor CodeMirror + upload + histórico com polling (o veredicto exibido é sempre o canônico; cada submissão julgada mostra um resumo abaixo — "Passou em X/Y testes (Z%)", pontos + grupos, ou Score heurístico — via /submission/summary, redigido por modo em contests: lib/verdict.sh), perfil público por usuário (/treino/stat/?user= — cabeçalho com membro desde/editor/último envio, cartões, curva de resolvidos, heatmap 26 semanas + punchcard dia×hora, veredictos/linguagens/dificuldade/tags em barras, progresso por coleção, problemas em aberto, conquistas derivadas de um REGISTRO gerível (aba 🏅 do admin → var/achievements.json; default embarcado; ver docs/PERFIL.md) e histórico paginado com filtros; tudo client-side de poucos fetches paralelos; perfil privado = cadeado, e o Bearer vai sempre que logado — admin vê o que a API já lhe permitia), stats por problema (cache, linguagens, editores, nuvem de avatares), e painel admin do treino (sessões/logs com UA+IP, busca/regex, bulk logout/lock, notícias, auditoria, máquinas, e — em abas com índice/TOCFila & tempo de resposta (contadores de submissão + calibração, o que cada máquina roda agora (calibração vs submissão), tempo de veredito e mapas de calor de volume de submissões e calibrações) e Estatísticas (usuários, sessões, problemas: total/públicos/privados, quebra por autor, mapa de calor de entrada de públicos e atividade diária). Fontes: /treino/admin/{queue,judges,response-stats,calib-activity,stats}.

Gestão de problemas (MOJ-nativo por org, keyless) & painel de status

Formato do pacote, .moj-meta.json, orgs e coleções: ver PACOTE.md (fonte única). Roteiro de montar um pacote e o que faz cada script: mojtools/README.md.

A fronteira: quem pode tocar moj-problems/

A árvore de pacotes (23 GB, 1402 problemas) é território da gestão de problemas. Só chegam nela: as rotas de autoria (handlers/problems/**, handlers/orgs/**), as de conversa com os juízes (handlers/judge/** — o juiz baixa o pacote e devolve TL/veredicto para run/), as de ops de admin (handlers/ops/**) e os utilitários de server/bin/.

Contest e treino ficam do lado de fora: respondem do que o servidor já materializou — o índice de donos (contests/treino/var/problem-owners.json), o json servível (var/jsons{,-private}/<id>.json, que já traz enunciado renderizado, autor completo, tags, linguagens e o TL com TLOVERRIDE aplicado), o run/tl/, e os enunciados do próprio contest (contests/<c>/enunciados/). Rota de prova não abre pacote por três motivos: latência (é I/O de gigabytes no caminho mais polado do dia), contenção (autores commitam durante a prova) e acoplamento ao formato interno, que é da gestão e muda por conta dela.

O que acontece quando a fronteira é atravessada tem nome e número: o /contest/problems chamava tl-checksum.sh por problema, que lê o conteúdo de tests/ — 112,8 MB hasheados a cada regeração do cache, e 2,0 s de carga a frio, só para exibir tempo-limite. A regra e o inventário dos resíduos estão em cdmoj/CLAUDE.md; o inventário é executável: bash server/test/sem-pacote.sh.

Autoria/edição em /problemas/ (storage: repo git LOCAL por problema em <org>/<prob>; acesso por ORG — membros escrevem, a trava public_allowed barra vazamento; só o login do MOJ). A aba Painel (GET /problems/status) dá a visão agregada dos problemas de que o login é dono, colaborador ou membro da org: quantos/quais calibrando, validados, calibrados, precisam recalibrar (time-limit desatualizado após mudança no pacote) e com erro, mais a planilha de time-limits. O acesso é cortado na API (owners_visible): problema privado de terceiro não aparece. Staleness vem do checksum do pacote carimbado no índice de donos (barato; ≤30 min de atraso); /problems/tl recomputa na hora p/ 1 problema. A aba Análise (GET /problems/my-stats) dá o panorama de submissões dos seus problemas agregado em toda a plataforma (treino + as turmas): tentativas, acertos, erros mais comuns, linguagens, nº de contests e o mais popular — cache precomputado que reconcilia o namespace do history (problemas-apc#…) com o índice de donos (apc#…) via collections; só agregados (sem logins, sem nomes de contests) — não vaza prova privada.

Três mecanismos de autoria em volta da calibração/TL (2026-08-19):

A aba 🕘 Histórico do editor é o git de cada problema (/problems/history, download?sha=, restore — que grava um commit NOVO, nunca reescreve história).

Submissão OFFLINE (LAN sem Internet)

Para prova em rede isolada, o moj-comp (CLI do competidor) assina pacotes sem rede (RSA-OAEP+AES, com um beacon assinado que carimba o horário) e o organizador os injeta depois por POST /contest/offline-submit (máx 50 por vez). O servidor valida envelope, login, contest, monotonicidade do horário reivindicado, dedup por sha256 e a whitelist de linguagem — e contabiliza no horário reivindicado, não no da chegada. Ver lib/contest-offline.sh, handlers/contest/{offline-submit,beacon}.sh.

Store por-usuário, cadastro por Telegram e alertas

Todo contest guarda um diretório por conta (contests/<c>/users/<login>/: account.json + history/metrics.json/submissões/logs/results/photo.png próprios). Não existe passwd: auth (verify_password), placar (sc_users), perfis e listagens leem os account.json direto (USERS_FROM=<src> cai para o users/ do contest-fonte — participantes compartilhados têm dir local sem account.json). Perfil (universidade/editor/privacidade) e metadados de time (.team{name,univ_short,univ_full,flag}) vivem no próprio account.json. Ganhos: trocar de username = mv do diretório e a maioria dos scripts de conta/julgamento só muda o caminho (lib/users.sh, emit_history_stream). Os handlers de usuário do admin do contest (user-add/user-disable/user-remove/users-set-password) escrevem no account.json (fonte da verdade); remover = mv do diretório p/ .removed-users/ (submissões preservadas).

Inscrição em contest (roster, janela e TIMES de contas do treino)

Contest que usa as contas do treino (USERS_FROM=treino) pode exigir inscrição prévia: contests/<c>/registrations.jsonexistir = ligado (mesma doutrina do cohorts.json). Motor em lib/registration.sh; a pessoa se inscreve pela página /contests/inscricao/?c=<id> do site principal (o token é por ORIGEM: o subdomínio do contest não enxerga a sessão do treino), individual ou em time de até 3 contas existentes (convite + aceite do convidado). A janela vem do conf (REG_OPEN/REG_CLOSE e REG_LATE_MINUTES p/ a entrada atrasada, que cai numa coorte unranked — a extra registration do Codeforces) e ancora na PROVA OFICIAL, não na rodada corrente (reg_official_window): o AQUECIMENTO pode ficar dias no ar, mas por DEFAULT só inscrito entra em QUALQUER rodada, aquecimento incluso (decisão 2026-08-04) — REG_WARMUP_OPEN=y no conf é o opt-in que reabre a porta no warmup a qualquer conta da fonte (reg_gate_active). Só nesse caso a promoção faz sentido varrer: reg_sweep_unregistered derruba a sessão de quem não se inscreveu (sessão não expira sozinha) e apaga o diretório vazio dele, p/ o placar da prova não nascer com linhas zeradas de quem só passeou no esquenta. A porta é a API: handlers/auth/login.sh recusa quem não está no roster (403 not_registered) e passou a valer LOGIN_ENABLED/LOGIN_START_TIME, que antes só existiam no desenho da tela.

O time é uma conta local do contest (users/time-<slug>/, senha desativada) e o membro entra com a própria credencial do treino: o login faz o alias (sessão = time, ACTOR = a pessoa), então placar, balões, impressão e clarifications continuam vendo uma linha só, sem nenhuma mudança neles. Cada mudança no roster materializa o store (overlay account.json sem senha p/ o inscrito — a credencial segue sendo a da fonte) e semeia as coortes individual/times, que têm placar próprio (ranking:true em cohorts.jsonvar/placar-view-<id>.txt, seletor no /contest/score/). Admin: painel Pessoas › Inscrições + item no checklist pré-prova.

Migração de contest pré-reforma (arquivado em contests-legado/): server/bin/store-migrate.sh <c> [--apply] [--from <dir>] — dry-run por padrão; canonicaliza probids (offset/a.borg#prob), leva team/perfil ao account.json, roteia os flat files, gera metrics, move os resíduos p/ .legacy-store/ e só publica em contests/ após verificação (contas, spot-check de senhas, soma do history, metrics, placar). server/bin/store-cleanup.sh <c> limpa resíduos de contest já migrado. server/bin/user-merge.sh <c> <de> <para> [--apply] funde um dir de usuário órfão (sem account.json) na conta viva — history ordenado por epoch, arquivos de submissão, metrics_recompute, .score-dirty e o login do var/editor-log; o resíduo vai p/ var/merged/ (nada é apagado) e os logs de auditoria (access.log, activity-*.log) ficam como estão. Órfão desses só nascia pelo furo da sessão que sobrevivia ao rename (ver API.md, "Invariante da sessão"); a ferramenta segue valendo p/ qualquer resíduo. Migração vinda do MOJ ANTIGO (backup em contests-backup/, probid já em <repo>#<slug>) é outro caminho — o store-migrate.sh não serve porque aborta se o destino existe e faz mv -T do contest: server/bin/treino-map-gen.sh decide <repo legado>#<slug><org>#<prob> (alias repo/collections, casefold, slug, título e, por último, o TEXTO do enunciado) e emite um TSV auditável; server/bin/treino-migrate.sh {stage|verify|install|audit} consome o TSV (recusa qualquer linha ?) e funde num contest vivo — conta nova entra por mv do diretório, conta que já existe só ganha history (dedup por subid) e submissões, senha do prod prevalece, telegram via tg_link. stage/verify não tocam o destino; audit confere o instalado contra o legado. Contas GERIDAS (menores, SEM Telegram — docs/CONTAS-GERIDAS.md): criadas pelo admin (aba 🧒; rotas /treino/admin/managed-*), marca .managed{by,note,birthdate,expires_at} no account.json; menor de 18 = perfil sempre privado (gate no profile_is_public) + Telegram bloqueado (link-start 403) — caem sozinhos aos 18; expiração opcional corta o login; senhas geradas mostradas UMA vez ao admin. O treino ganha um overlay de Telegram (lib/telegram.sh): cadastro web-first (/treino/cadastro/) confirmado por deep-link no bot, 1 Telegram = 1 conta (anti-duplicata), recuperação de senha pelo vínculo, e senha entregue só por DM. O mojinho-bot virou transporte fino (bot-token mojb_, sem .admin/GODS) e entrega alertas de incidente que a API decide (lib/alerts.sh + GET /ops/alerts: juiz offline+fila, fila grande, daemon caído, com histerese/cooldown) aos .admin com Telegram vinculado + grupo. O mesmo outbox carrega DM dirigida a UMA pessoa (alert_dm: o produtor resolve o chat, o item vai com group:false p/ não vazar no grupo e loud:true p/ notificar) — é assim que o convite de time avisa o convidado na hora e cutuca quem não respondeu na véspera (lib/invite-notify.sh; o poll do bot continua sendo o relógio, agora com stamp próprio). E carrega também a mensagem só para o grupo (alert_group: chats:[] + group:true) — é por ela que sai o relatório de quartil (lib/relatorio.sh + POST /ops/relatorio, comando /relatorio do bot, gate .admin pelo telegram_id): top-10 de contests por submissões no semestre + treino + comparações com o ano anterior, automático ao vencer cada quartil.

Criação de contest (/treino/criar/) — wizard multi-etapa

Permissão por lista do admin OU threshold de problemas resolvidos. Wizard em 8 passos (shell criar.js + steps/*.js; um objeto draft único — ir-e-voltar não perde nada): 0 Começar (em branco / template salvo / duplicar contest meu / importar .tar.gz / baixar template JSON / salvar template de contest existente), 1 Dados (nome/id/modo/datas), 2 Problemas (painel compartilhado de busca+sorteio por coleção/tag/dificuldade, add por ID, enunciado custom HTML e PDF por problema), 3 Usuários (compartilhados do treino ou próprios, com colagem fluida + senhas legíveis + CSV), 4 Admin (obrigatório), 5 Opções (o MESMO settings-editor da aba Configurações do admin — paridade total, + prioridade de julgamento), 6 Visual (cores/Sonic, países/escolas, regiões), 7 Revisão (resumo + validações + Criar/Criar vazio + salvar como template). Templates nomeados ficam no servidor por criador (/treino/contest-create/templates); duplicar/exportar usam /treino/contest-create/{mine,export,duplicate} (só dono/admin — 404 p/ terceiros).

Ambiente de contest (<id>.moj.<base>)

Login (com gate opcional por substring de User-Agent), página principal (problemas + submissão + editor que o admin pode desligar), placar multi-modo (icpc/obi/treino/ heurístico/outro) com bandeiras locais, filtro por país/escola, modo anônimo (agregado/ quartis), freeze (esconde resultados após o horário; build.sh gera placar.txt público congelado e placar-full.txt completo — .admin/.judge/.cjudge + allowlist SCORE_FULL_USERS veem o completo) com cerimônia de revelação nativa (/contest/score/reveal.html, estilo ICPC resolver: delta frozen→full revelado de baixo p/ cima, passo/auto, botão "descongelar tudo" = settings freeze:0, só admin; o placar aceita &view=public p/ o privilegiado obter a visão congelada; link na aba Situação do admin). O .cstaff conduz a cerimônia POR SEDE: a mesma página com &scope=mine — a API recorta frozen+full aos usuários do escopo dele (staff-filters) e só libera o full quando o contest terminou p/ todas as sedes (contest_over_for_all: fim do conf + o maior end de time-overrides.json; admin pode antecipar via allowlist SCORE_FULL_USERS), tempo de solução relativo ao início (não EPOCH), e nav por papel. Contest 🕵️ SUPER SECRETO (conf SECRET=1, marcável na criação e no admin): fora das listagens públicas (home, arquivo /contests/, /status/) e o placar deixa de ser públicoscore/balloons/regions/ teams-meta exigem sessão daquele contest (401 secret_login_required). A tela de login/ countdown continua funcionando p/ quem tem o link (/contest/basic segue público). Desmarcar exige digitar o id. Usuários comuns têm no menu uma página própria de Backup de arquivos (/contest/backup/) p/ guardar versões de solução (não polui a home); o admin vê/baixa todos na aba Backups (zip por usuário). Quando há usuário .staff no contest, os alunos ganham também a página Impressão (/contest/print/): enviam um arquivo (PDF/imagem/texto/código) e acompanham o status (pendente→processada→entregue) — ver Impressão (.staff) abaixo. Os problemas usam o id canônico coleção#problema (igual ao treino — é o que o juiz usa p/ achar o pacote); o editor é o CodeMirror compartilhado (shared/editor.js, com tela cheia e nova janela) e a seleção de linguagens tem fonte única em lib/langs.sh (effective_problem_langs), com quatro níveis (mais-específico-vence): problem-langs.json do contest › LANGUAGES do conf › languages do PACOTE › PLATFORM_LANGS (as 17 de mojtools/lang/). É FORÇADA no /submit (400 lang_not_allowed) — lista vazia = o CHÃO da plataforma, nunca "qualquer extensão"; o dropdown (shared/languages.js) é só conveniência. O placar é gerado de users/*/metrics.json (mantidos incrementais pelo daemon; score/build.sh + sc_cells — ver SCOREBOARD.md), sem varrer history. O aluno recebe aviso de novidades (notícias + clarifications respondidas, com badge de não lidas — poll de /contest/updates) e vê o tempo-limite por linguagem no detalhe do problema (ocultável pelo admin). Acesso por fase+papel (forçado pela API, não só no front): .admin/.judge veem os problemas e submetem a qualquer momento (antes/durante/depois); o usuário normal só vê os problemas após o início (antes disso, ao logar, recebe uma tela de contagem regressiva) e só submete durante a janela (/contest/problems devolve locked:"not_started" e /submit recusa com 403 fora da janela — contest_not_started/contest_ended); .staff/.cstaff não veem problemas nem submetem; .mon submete só na janela (como o normal) mas fica fora do placar. A janela tem prorrogação por sede/grupo (time-overrides.json, regras regex no login — 1ª que casa estende o fim SÓ daquele grupo, ex.: queda de energia numa sede; contest_end_effective em lib/contest-gate.sh vale no /submit e no countdown do /contest/basic autenticado; editável na aba Configurações do admin e por moj-contest extend --group, auditado). Telas internas:

Juiz .judge, juiz-chefe .cjudge & veredicto manual

Papéis (sufixo no login; ver lib/auth.sh): .judge submete a qualquer hora (fora do placar/estatísticas), responde clarifications e cria avisos; .cjudge (juiz-chefe) herda o juiz (is_judge vale p/ ele) + extras escopados (is_chief): editar notícias/respostas já dadas, ver Situação e Todas Submissões (mesmas ops do admin), resolver conflitos e editar a config de auto-veredicto — não é admin pleno. .cjudge está nas quatro listas de sufixo (auth/score-common/stats-gen/login) p/ ficar fora do placar e isento da janela de login.

Veredicto manual (opt-in por contest, MANUAL_VERDICT): quando ligado, o daemon segura o veredicto computado p/ revisão humana — grava contests/<c>/review/<id>.json e deixa o history provisório (o aluno segue vendo "julgando"); a exceção é a matriz auto-verdicts.json (problema × linguagem × veredicto, editável por admin/chief) que libera combinações automáticas. O casamento da matriz é pelo veredicto canônico (verdict_canon, sem o sufixo de score ,Np que o juiz embute), e erros de juiz (Judge Error/No_Servers) também são segurados — o competidor vê só Not Answered Yet (nenhuma mensagem de erro vaza); o juiz vê o erro no painel e re-julga. Dois .judge pegam a submissão (máx 2, 1 ativa por juiz, TTL 5 min com +5, ou desistir), veem log + fonte + veredicto computado (a tela não recarrega enquanto se avalia) e escolhem um veredicto de uma lista configurável (final-verdicts.json, {label,verdict}; default = as 6: 1-YES…6-Contact staff). O voto é permanente e libera o juiz na hora (ele já pode pegar outra submissão). N votos unânimes → vai ao aluno; diferentes → conflito, que só o juiz-chefe resolve (avisado pelo alerta global de conflito em qualquer página). A liberação enfileira setverdict, consumido pelo daemon e finalizado pelo escritor único (update_history + results/<id>.json), então o veredicto manual entra no timeline de auditoria como qualquer outro. TUDO é auditado (clar-*, news-edit, final-/ auto-verdicts-set, review-claim/extend/giveup/vote/agree/conflict/resolve, verdict-held/released). Mudou o daemon → reinicie-o (mantendo INTAKE_MODE/JUDGE_BACKEND).

Auditoria: ações administrativas são logadas em contests/<c>/var/admin-audit.log (e treino/var/admin-audit.log no treino) — o contest fica auto-contido.

Juiz & daemons

Submissão assíncrona (spool + inotify), judged.sh (particionável em K workers por hash(login) — JUDGED_SHARDS, ver FLOW.md §2), e julgamento pull: o daemon enfileira por prioridade (judge-gw/sched-lib.sh) e os juízes (moj-agent@) puxam o job no heartbeat, baixam o pacote sob demanda, calibram e reportam veredicto/TL por HTTP — sem master, sem push de entrada. Backends mock/local do judge.sh ficam só p/ dev/legado. Detalhes em FLOW.md e judge-gw/PULL.md.

Testes

Suítes de smoke em server/test/smoke-*.sh (cada uma sobe o router.sh com CONTESTSDIR/ SESSIONDIR de fixture e exercita os handlers de ponta a ponta). Rode todas:

cd server/test && for t in smoke*.sh; do bash "$t"; done

Compilar esta documentação em HTML

bash docs/build-html.sh     # gera docs/html/*.html + index (usa pandoc)