Classificação para as próximas fases (Final Brasileira → PDA → Mundial) — MOJ docs

Classificação para as próximas fases (Final Brasileira → PDA → Mundial)

O contest pode marcar times CLASSIFICADOS para as etapas seguintes. O dado mora em contests/<c>/classification.json, com stages[]: um estágio por etapa — final-br e pda no contest da 1ª fase/regional, mundial no do Campeonato. Cada estágio tem o seu motor, status: draft|published e teams{login→{via, sede,place,…}}. SÓ o estágio published aparece fora do painel:

O motor (server/score/classify-br.sh <contest> <config.json> [out])

Preview PURO (nunca grava) sobre var/placar-full.txt + regions.json.

O placar é lido pelo CABEÇALHO, com o parser único sc_board_rows (score/score-common.sh), pelo comum aos motores (score/classify-common.sh). Até 30/09/2026 o motor contava as colunas a partir do FIM, com $NF = guest. A coluna guest só existe com coorte unranked: sem ela, o motor lia o total de uma célula de problema e tomava por convidado quem tinha LastAC=1. Num contest sem coorte, a Final saía VAZIA. Em 2026 escapou porque havia a coorte CCL. A regressão está no smoke-classify-br.sh, e o motor novo deu os mesmos 61 automáticos publicados na LATAM 2026.

Quem disputa = quem ESTÁ no nó da região (config.region, padrão "Brasil") e a sede de cada time = a sede pela regra única de sedes (lib/regions.sh: a gravada vence, senão a regex mais funda; quem "parou no pai" fica sem sede). Até 28/09/2026 eram a regex do nó da região e a 1ª folha pela regex, ignorando a sede gravada. config.region que não é nó de 1º nível do regions.json = erro de config (rc 2, com os nós que existem); antes ninguém entrava e a classificação saía vazia, calada (03/10/2026). Sede ou supersede com vaga na config que não existe na árvore = aviso sede_missing/supersede_missing (a vaga não vai a ninguém). Regras da 1ª fase, aplicadas EM ORDEM (config define vagas; tudo editável no painel):

Motor manual (toda promoção à mão)

server/score/classify-manual.sh, para contests menores (a seletiva de uma universidade). Não há regra: o admin escolhe quem sobe.

Motor latam-pda (regional LATAM → Campeonato Latino-Americano, a "PDA")

server/score/classify-pda.sh, estágio pda, chip "PDA". A regra é o PDF "2026-2027 ICPC Latin America – Promotion Rules". A config oficial está em server/score/classify-seeds/latam-pda-2027.json, a semente do editor JSON do painel.

Dados (todos pelo classify-common.sh):

Regra geral: a escola ainda não tem time promovido. O 2º time de uma escola só entra no passo 1 (até max_per_school, padrão 2). Os blocos sem a regra geral só exigem que o time não esteja promovido.

Passos padrão, nesta ordem (cada um vê os promovidos dos anteriores):

  1. P1 desempenho: N/2 vagas aos melhores, com até max_per_school por escola.
  2. P2 países: o teto é calculado UMA vez, antes do passo: min(N/4, X) − 1, com X = países participantes ainda sem time (countries.cap: "min_minus_1", decisão do Ribas; a variante "min_of_x_minus_1" = min(N/4, X − 1)). Vaga a vaga, o melhor time de país sem time, com ≥ countries.min_solved (1) resolvido. País só com times de 0 resolvidos sai em countries.zero_solved. Vaga que sobra fica para o P4.
  3. P3 instituição-sede: se nenhuma escola de host_schools tem time, 1 vaga ao melhor time de QUALQUER escola sem time (decisão do Ribas: a condição é sobre as escolas-sede, a vaga não). Sem host_schools, o passo é pulado com aviso.
  4. P4 representação geográfica: o algoritmo do PDF, em INTEIROS (o PDF divide duas vezes; em ponto flutuante, 9 × 21 / 27 dá 6,999… e o trunc dá 6 em vez de 7):
  5. Femininas padrão (female[], sem a regra geral): a melhor só-mulheres do país-sede (host_country), depois a melhor só-mulheres da LATAM.

Blocos da edição 2027 (edition_blocks[], na ordem; cada um com label pt/en/es):

Tipo O que faz
fixed Times de fora do placar (os pendentes de 2026), chave ext:<id>, sem posição. Por padrão não contam como time da escola/país/região (counts_for_school: false, decisão do Ribas de 01/10/2026).
female scope: host_school (a instituição-sede), host_country, latam ou per_region (slots POR região); min_women 3/2/1. Sem a regra geral.
country_participation Cotas quotas (5, 4, 3, 2, 1) aos países com mais times no CICLO, desempate por instituições: a tabela do RCD em cycle_teams/cycle_institutions. Vazia, a prévia usa as contagens DESTE contest e avisa (cycle_table_empty). Empate nas duas = cycle_tie. As vagas vão aos melhores times de instituições ainda sem time (1 por instituição).
host_school Vagas extras da instituição-sede; general_rule: false na semente (decisão do Ribas).
reserve Só reportada (reserve.slots). O comitê usa as vagas com "➕ Promover à mão" via reserva; passar do número = 409 reserve_full.

Mínimo de resolvidos: só onde o PDF diz (countries.min_solved: 1). Os outros blocos aceitam min_solved, padrão 0.

Lista de espera (waitlist): faixas em ordem (tiers, cada uma com schools e/ou countries): na semente, instituições de Monterrey, de Nuevo León e o resto do México. Cada time cai na 1ª faixa que casa; dentro dela, pela posição. Com general_rule: true, a escola não pode ter time promovido nem um time antes na lista. A ação promote_next do handler recalcula a lista contra o estágio ATUAL (--waitlist: os times compostos contam como promovidos, os retirados ficam de fora) e promove o 1º, como override add via lista.

O RCD preenche antes da rodada real (a semente os deixa vazios e o motor avisa onde faz diferença): host_schools (e o school_alias se os campi do ITESM são uma escola só), a tabela do ciclo, as escolas das faixas da lista de espera e as fractions_prev que sobraram do ano anterior.

Saída: além de classified[] (na ordem de promoção, seq), pre[] e warnings[]: blocks[] (vagas e usadas), unused{}, countries, host, geo (por região: escolas, q, inteiras, frações, extra, vagas, preenchidas), fractions_out, country_participation, reserve, waitlist[], labels e via_order.

Os logins da LATAM 2026 (teambrspso…) não seguem o padrão team<RR><PP>; o motor foi validado pelo server/test/smoke-classify-pda.sh (N=12, cada caso conferido à mão).

Motor latam-mundial (Campeonato Latino-Americano → Mundial)

server/score/classify-mundial.sh, estágio mundial, chip "Mundial". Roda no contest do CAMPEONATO. A semente é server/score/classify-seeds/latam-mundial-2027.json; o RCD informa N_WF (as vagas da LATAM no Mundial). Sem ele, o motor avisa (n_wf_missing) e não classifica ninguém.

  1. Campeão de cada região (wf-region): o melhor time da região com ≥ min_solved (1) resolvido.
  2. Geral (wf-overall): as N_WF − campeões vagas restantes aos melhores do placar. A vaga de região sem time elegível vai ao geral (region_slot_unfilled: "overall", decisão do Ribas; "none" a deixa sem uso), com o aviso wf_region_unfilled.

Só UM time por instituição (max_per_school: 1) vai ao Mundial, nos dois passos. Região, país e escola saem como no latam-pda (login + school_alias), com a mesma recusa.

Prêmios (awards, INFORMATIVOS: não mudam a classificação): o campeão LATAM (1º lugar), as medalhas pela posição (awards: {gold:4, silver:4, bronze:4} = ouro 1–4, prata 5–8, bronze 9–12) e o campeão de cada região com o título dela (regions[].title, pt/en/es). Empate divide a posição; se ele atravessa a faixa, a medalha vai a mais times e o motor avisa (award_tie). O painel mostra os prêmios em "📊 Detalhes do cálculo".

config.algorithm diz qual motor roda. O handler (admin/classify.sh) só executa um script da allowlist CL_ENGINES (lib/classify.sh). O catálogo server/score/classify-catalog.json descreve cada motor: estágio padrão, próximo estágio, formulário do painel (form), padrões (nome, local, quando, chip), vias e semente. Ele também traz os rótulos pt/en/es de toda via. O smoke-contest-modules.sh confere que catálogo e allowlist têm os mesmos ids. Hoje: sbc-fase1 (estágio final-br, chip "Final BR"), latam-pda (estágio pda, chip "PDA"), latam-mundial (estágio mundial, chip "Mundial") e manual (estágio proxima-fase, chip = o nome da próxima fase).

Motor novo = score/classify-<x>.sh + uma linha em CL_ENGINES + uma entrada no catálogo + smoke. O placar, o relatório e a rota pública leem o ESTÁGIO, nunca o motor. Id fora da lista = 422 algorithm_invalid.

Contrato de um motor (score/classify-common.sh tem o comum: placar pelo cabeçalho, femininas, avisos):

Override manual (salvaguarda contra erro de execução e caso de borda)

Vale para TODO estágio. O cálculo do motor e as decisões manuais ficam SEPARADOS: o estágio guarda config, result (a saída PURA do motor) e overrides[]. O teams é a COMPOSIÇÃO: motor − retirados + manuais. A regra da composição é ÚNICA, o CL_JQ em lib/classify.sh, usada pelo GET, pela prévia, pelo apply e pelos overrides. Cada override é {id, op, login|ext, reason, by, at}, com motivo OBRIGATÓRIO. O motivo é interno: só o painel o mostra.

Ação Efeito Quando usar
exclude {login} O time sai do CÁLCULO (exclude[] do motor) e o motor roda de novo: o próximo pela regra herda a vaga. Time inelegível, dado errado (escola ou região trocada).
withdraw {login|ext} O time sai da lista SEM recalcular: a vaga fica vaga (o comitê ou a lista de espera a preenche). Time que desistiu depois da classificação.
add {login | ext+team, via} O time entra à mão, com a via manual, lista ou reserva. Para o motor ele já está promovido (preassigned[]): o recálculo não lhe dá outra vaga. Time de fora do placar entra como ext:<id>. Decisão do comitê (a regra 3 da SBC), caso de borda.
override_undo {id} Desfaz o override e volta ao cálculo. Engano.
promote_next {reason} O 1º da lista de espera (recalculada agora) entra como add via lista. Só motor com lista de espera; lista vazia = 409 waitlist_empty. Vaga aberta por desistência.

Fluxo no painel (Evento › Classificação — módulo classificacao)

A criação de contest leva modules.classificacao{algorithm, config} (um estágio, o padrão do motor) ou {stages:[{id?, algorithm, config, name?, venue?, when?, chip?}]}. O motor é conferido na allowlist e a config pelo --check do motor. O export devolve stages[] quando há mais de um estágio. Resultado e overrides não entram: são dados da prova.

Testes: server/test/smoke-classify-br.sh (motor, relatório com dois estágios, gate de rascunho, handler e overrides), smoke-classify-pda.sh (o motor da PDA passo a passo, --check, --geo, lista de espera, promote_next, reserva, trava com 8 escritas em paralelo), smoke-classify-mundial.sh (campeões, 1 por instituição, região sem time → geral, medalhas com empate), smoke-classify-manual.sh (placar, promoção sem motivo, slots_full, desfazer), smoke-score-classified.gjs.sh (chips do placar), smoke-classify-tab.gjs.sh (o painel: grupos, ações no estágio certo, nova etapa, editor JSON), smoke-contest-modules.sh (catálogo × allowlist, spec com stages[]).