From 2da22512cc6e1f11d029eaa43f9727f6c758c34a Mon Sep 17 00:00:00 2001 From: usantos Date: Tue, 18 Aug 2026 20:52:35 -0300 Subject: [PATCH 01/16] =?UTF-8?q?Documenta=C3=A7=C3=A3o=20de=20como=20cria?= =?UTF-8?q?r=20mapa=20com=20IA.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/INDICE.md | 7 +- docs/docs/colaborar.md | 4 + docs/docs/criar-mapa-com-ia.md | 212 +++++++++++++++++++++++++++++++++ docs/sidebars.js | 1 + 4 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 docs/docs/criar-mapa-com-ia.md diff --git a/docs/INDICE.md b/docs/INDICE.md index 9672881df..8c19e67c3 100644 --- a/docs/INDICE.md +++ b/docs/INDICE.md @@ -26,14 +26,15 @@ ordem. | 7 | [`../tools/eval/BAR-CONSISTENCIA.md`](../tools/eval/BAR-CONSISTENCIA.md) | A régua vigente: 25 critérios de consistência e flow. **Tem precedência** sobre a `BAR.md`. | | 8 | [`../tools/eval/BAR.md`](../tools/eval/BAR.md) | A régua de fidelidade visual. Consulta, não leitura obrigatória. | | 9 | [`../tools/eval/README.md`](../tools/eval/README.md) | Catálogo do arnês: o que cada script mede, e quais estão obsoletos. | +| 10 | [`docs/criar-mapa-com-ia.md`](docs/criar-mapa-com-ia.md) | Fluxo completo para especificar, implementar, integrar e validar mapas com assistência de IA. | ### 3. Vai mexer no SITE ou no BANCO | # | Arquivo | Por quê | |---|---|---| -| 10 | [`seguranca.md`](seguranca.md) | O que foi fechado no pré-release, onde estava e como testar. Leia antes de mexer em `/api/*` ou em `supabase/`. | -| 11 | [`../supabase/README.md`](../supabase/README.md) | Como aplicar as migrations. | -| 12 | [`../supabase/opcional/OFUSCACAO-README.md`](../supabase/opcional/OFUSCACAO-README.md) | A ofuscação de schema entregue pronta e **não aplicada**. | +| 11 | [`seguranca.md`](seguranca.md) | O que foi fechado no pré-release, onde estava e como testar. Leia antes de mexer em `/api/*` ou em `supabase/`. | +| 12 | [`../supabase/README.md`](../supabase/README.md) | Como aplicar as migrations. | +| 13 | [`../supabase/opcional/OFUSCACAO-README.md`](../supabase/opcional/OFUSCACAO-README.md) | A ofuscação de schema entregue pronta e **não aplicada**. | ### 4. Contexto e direção diff --git a/docs/docs/colaborar.md b/docs/docs/colaborar.md index 7049c17c3..726585243 100644 --- a/docs/docs/colaborar.md +++ b/docs/docs/colaborar.md @@ -236,6 +236,10 @@ O pipeline é data-driven a partir do GLB. Os GLBs de arma vivem em `public/mode ## Como adicionar um mapa +Para um roteiro orientado a agentes — incluindo ficha de design, prompt reutilizável, +mutantes, preview e revisão adversarial — consulte +[Criar mapa usando IA](./criar-mapa-com-ia.md). + Hoje mapas são **código**, não dado: cada `map_*.js` é geometria declarada à mão, e os maiores rivalizam em tamanho com os módulos de sistema. Migrar isso para JSON é a Fase 2 conteúdo como dado do diff --git a/docs/docs/criar-mapa-com-ia.md b/docs/docs/criar-mapa-com-ia.md new file mode 100644 index 000000000..9ab038971 --- /dev/null +++ b/docs/docs/criar-mapa-com-ia.md @@ -0,0 +1,212 @@ +--- +id: criar-mapa-com-ia +title: Criar mapa usando IA +sidebar_label: Criar mapa usando IA +sidebar_position: 7 +description: Fluxo prático para pedir, implementar, integrar e validar um mapa criado com assistência de IA no CORO SOLTO. +--- + +# Criar mapa usando IA + +IA acelera modelagem procedural, texturas, integração e testes, mas não decide sozinha se +o mapa é jogável. Neste projeto, um mapa só está pronto quando o jogo real abre, os dois +times conseguem navegar, obstáculos têm colisão, armas são alcançáveis e uma captura foi +olhada por uma pessoa. + +Este guia complementa a página **Como colaborar**. A página de +colaboração documenta o formato e as réguas vigentes; aqui o foco é **como conduzir um +agente de IA sem deixar contrato, gameplay ou documentação para trás**. + +## Antes de pedir código + +Comece com uma ficha curta. Quanto mais concreta ela for, menos o agente preencherá lacunas +com decisões genéricas. + +| Decisão | O que informar | +|---|---| +| Identidade | id em `snake_case`, nome exibido, tema e categoria do menu | +| Modo | se abre em rounds ou CTF; `ctfMode` define apenas o padrão | +| Espaço | limites aproximados, interior/exterior e níveis de altura | +| Rotas | rota principal, dois flancos e pontos de reencontro | +| Cobertura | objetos baixos, paredes, edifícios e linhas de tiro que devem ser quebradas | +| Spawns | lados dos times, proteção inicial e direção em que olham | +| Objetivos | posições e nomes das bandeiras quando houver CTF | +| Arsenal | quantidade e distribuição; nunca reduza armas existentes para “limpar” a cena | +| Arte | materiais, clima, referências visuais e elementos proibidos | + +Não peça apenas “faça um mapa bonito”. Diga qual decisão o jogador toma nos primeiros +segundos, onde acontece combate curto e onde uma arma de longo alcance pode dominar. A IA +precisa de uma intenção de gameplay, não só de uma lista de objetos. + +## Prompt reutilizável + +Copie, preencha os colchetes e envie ao agente a partir da raiz do repositório: + +```text +Crie a branch map/[id]. Leia AGENTS.md, docs/LICOES.md e tools/eval/ARCH.md antes de editar. + +Crie o mapa [nome], id [id], com tema [tema]. Ele deve abrir em [rounds/CTF], ter limites +aproximados de [x × z], três rotas jogáveis ([descreva]) e estes marcos visuais: [lista]. +Spawns: [descrição]. Coberturas e obstáculos: [lista]. Arsenal: [distribuição]. + +Direção visual: [materiais, clima, paleta e referências]. Não use pessoa real, gore, +marca/obra protegida, CDN ou dependência de runtime. Prefira texturas procedurais ou assets +com licença compatível e procedência registrada. + +Antes de implementar, escreva uma régua específica que reprove sem o mapa e declare a +mutação que prova que ela morde. Depois implemente sem duplicar sistemas existentes. + +Integre o mapa em public/js/maps.js, public/js/main.js, src/data/jogo.ts e gere +public/img/map-previews/[id].jpg sem HUD, personagens ou armas visíveis. + +Valide contrato, colisões, spawns, waypoints, pickups, bots, preview, console, check:fast e +build. Gere uma captura, olhe a imagem e descreva o que foi realmente verificado. +Não faça commit até eu pedir. +``` + +Adapte o prompt em vez de acrescentar correções contraditórias no fim. Se o conceito mudou, +reescreva a ficha e diga explicitamente o que substitui a decisão anterior. + +## Fluxo recomendado + +### 1. Leia as regras e proteja o trabalho atual + +Antes de trocar de branch, rode `git status -sb`. Não leve mudanças de outro mapa para a +branch nova. Leia `AGENTS.md`, as lições 1–5 de `docs/LICOES.md` e a frente **MAPAS / MUNDO** +em `tools/eval/ARCH.md`. + +Mapas vivem em `public/`, que usa módulos ES servidos diretamente. Não adicione bundler, +CDN ou dependência de runtime. + +### 2. Faça a régua nascer vermelha + +Crie `tools/eval/-check.mjs` com invariantes próprias do mapa. Exemplos: + +- marcos obrigatórios existem e têm nomes estáveis; +- cada obstáculo visível possui um colisor coerente; +- os dois times alcançam o centro e os objetivos; +- rampas, escadas ou passagens são atravessáveis pelo movimento real; +- materiais visíveis possuem textura e UV válido; +- o preview esconde HUD, personagens e armas. + +Rode a régua antes da implementação e registre a falha. Para cada cláusula, crie um +`--mutante=` que remova ou corrompa exatamente a propriedade medida. Uma régua que +continua verde com o mutante não protege o mapa. + +### 3. Implemente o contrato do mapa + +Crie `public/js/map_.js` exportando uma função `build*`. Use um mapa registrado de +tamanho parecido como referência; não copie um arquivo inteiro para depois renomear. + +O objeto devolvido pelo builder precisa respeitar o contrato consumido pelo jogo: + +```text +root · colliders · occluders · spawns · bounds +waypoints { nodes, adj } · nearestWaypoint · findPath +``` + +Campos como `groundHeightAt`, `slowAt`, `pickups`, `ctfPoints`, `sun` e `hemi` entram +conforme o mapa precisa. Antes de registrá-lo, confira o contrato: + +```bash +node tools/eval/map-contrato-check.mjs --extra=public/js/map_.js +``` + +Use colisores simples separados da malha visual. Neste projeto eles são AABBs; uma parede +rotacionada pode parecer correta e ainda permitir que o jogador deslize pelo canto. Teste a +travessia pelo movimento real, não apenas contando objetos no array `colliders`. + +### 4. Integre todas as superfícies públicas + +Um arquivo em `public/js/` não aparece sozinho no jogo. Atualize no mesmo PR: + +| Arquivo | Responsabilidade | +|---|---| +| `public/js/maps.js` | import, nome exibido, builder e `ctfMode` quando aplicável | +| `public/js/main.js` | descrição (`MAP_DESC`) e categoria (`MAP_CAT`) do seletor | +| `src/data/jogo.ts` | entrada de `MAPAS` usada pelas páginas públicas e metadados | +| `public/img/map-previews/.jpg` | cartaz quadrado, nomeado exatamente como o id | + +Não edite contagens geradas em README ou documentação. Rode `npm run docs` depois do +registro para que `tools/gen-docs.mjs` atualize os blocos derivados do código. + +### 5. Gere e confira o preview + +Com o servidor em execução, capture vários ângulos: + +```bash +npm run dev +node tools/eval/g2ui-map-previews.mjs -1.2,-0.6,0,0.6,1.2 +``` + +As imagens de calibração ficam em `/tmp/gauntlet/g2ui-maps/`. Escolha uma que explique o +layout e grave o JPG versionado: + +```bash +node tools/eval/g2ui-map-previews.mjs --write =/tmp/gauntlet/g2ui-maps/.png +``` + +Olhe o JPG final. Confirme textura, enquadramento, leitura das rotas e ausência de HUD, +personagens, viewmodel e armas. O script detectar zero erros de console não prova que a +imagem ficou boa. + +### 6. Valide gameplay e integração + +Depois de registrar o mapa, rode: + +```bash +npm run eval:mapcontrato +node tools/eval/map-check.mjs +node tools/eval/pickup-check.mjs +node tools/eval/botsim.mjs 60 +npm run eval:mapid +npm run docs +npm run arch +npm run check:fast +npm run build +``` + +Abra também `http://localhost:4321/?debug=1&auto=P,mst&map=` e exercite pelo menos: + +1. nascimento dos dois times; +2. rota principal e os dois flancos; +3. colisão dos obstáculos e acesso a desníveis; +4. coleta de armas; +5. uma luta com bots; +6. objetivos de CTF, quando existirem; +7. console sem `pageerror` ou erro de recurso do mapa. + +Se a geometria de paredes mudou em um mapa que usa grafite assado, rode também +`npm run grafite`; `eval:grafitelayout` reprova quando o layout fica velho. + +## Como revisar a resposta da IA + +Faça uma segunda leitura como adversário, sem reutilizar a justificativa do agente: + +- remova um colisor e confirme que a régua reprova; +- parta uma ligação do grafo e confirme que os bots perdem a rota; +- retire o preview e confirme que `eval:mapid` detecta o cartaz ausente; +- reorganize waypoints e confirme que caminhos usam índices, não coordenadas por acaso; +- caminhe contra cada obstáculo importante; +- procure pontos abertos demais, gargalos sem saída e linhas de sniper entre spawns; +- confira a captura em 3:2, proporção usada na revisão visual do projeto. + +Peça correção quando o jogo contradizer o relatório, mesmo com o portão verde. A regra da +casa é simples: se uma pessoa encontra o defeito e a régua não encontra, falta uma +invariante — não uma explicação melhor. + +## Definition of done + +O mapa criado com IA está pronto para PR quando: + +- possui identidade visual própria sem material protegido ou pessoa real; +- aparece no seletor, na categoria correta e nas páginas públicas; +- tem preview revisado visualmente; +- spawns, rotas, colisões, pickups e objetivos funcionam no jogo real; +- bots navegam sem ficar presos; +- a régua específica passa e seus mutantes reprovam; +- `check:fast` e `build` passam; +- o PR marca risco alto e pede revisão humana de gameplay. + +IA pode produzir o primeiro rascunho em minutos. O que transforma esse rascunho em mapa do +jogo é a combinação de contrato explícito, mutação, captura olhada e teste jogável. diff --git a/docs/sidebars.js b/docs/sidebars.js index d2e430fa2..804123ea4 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -27,6 +27,7 @@ const sidebars = { 'quality-gates', 'botbrain', 'arquitetura', + 'criar-mapa-com-ia', 'colaborar', 'estado', ], From d024e4c3e1b32f07dc6e6ce33fe54244c4387352 Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Sat, 22 Aug 2026 04:18:01 +0000 Subject: [PATCH 02/16] =?UTF-8?q?Merge=20da=20base=20(conflito=20s=C3=B3?= =?UTF-8?q?=20em=20arquivo=20gerado,=20resolvido=20pelo=20autofix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent: csbrasil-bot (autofix) Signed-off-by: csbrasil-bot --- package-lock.json | 612 +---- package.json | 118 +- scripts/ci/autofix_allowlist.py | 123 + scripts/ci/check_automerge.py | 65 +- scripts/ci/coderabbit_gate.py | 76 +- scripts/ci/crash_dedupe.py | 128 +- scripts/ci/dco_check.py | 50 +- scripts/ci/ensure_labels.py | 44 +- scripts/ci/greptile_gate.py | 62 + scripts/ci/issue_review.py | 137 +- scripts/ci/issue_sweep.py | 56 +- scripts/ci/pr_comment.py | 171 +- scripts/ci/workflow_security_check.py | 122 +- scripts/fetch-audio.sh | 2 +- scripts/fetch-decals.sh | 4 +- scripts/setup.mjs | 2 +- scripts/sync-changelog.mjs | 130 +- tools/eval/ARCH.md | 465 ++-- tools/eval/KNOWN-RED.json | 4 +- tools/eval/README.md | 1 + tools/eval/armas-check.mjs | 91 + tools/eval/autofix-check.mjs | 141 + tools/eval/backend-hints-check.mjs | 92 + tools/eval/base-ref.mjs | 36 + tools/eval/bot-brain-check.mjs | 8 +- tools/eval/bot-record.mjs | 5 +- tools/eval/botbrain-safety-check.mjs | 35 +- tools/eval/cache-baseline.json | 15 + tools/eval/cache-probe.mjs | 104 + tools/eval/char_probe.json | 16 +- tools/eval/comentario-check.mjs | 141 + tools/eval/deploy-gate-check.mjs | 75 + tools/eval/deps-check.mjs | 111 + tools/eval/docs-autoria-check.mjs | 107 + tools/eval/entrada-check.mjs | 102 + tools/eval/error-provenance-check.mjs | 359 ++- tools/eval/fixture-check.mjs | 107 + tools/eval/g2ui-map-previews.mjs | 7 + tools/eval/gl-shots.mjs | 16 +- tools/eval/graffiti-audit.mjs | 7 +- tools/eval/graffiti-census.mjs | 23 +- tools/eval/graffiti-layout-check.mjs | 2 +- tools/eval/invariants.mjs | 12 +- tools/eval/map_check.json | 677 ++++- tools/eval/mat_check.json | 3463 ++++++++++++++++++++++++- tools/eval/mat_scenes.json | 212 +- tools/eval/mat_shade.json | 2042 ++++++++++++++- tools/eval/mutacao-cega-check.mjs | 60 + tools/eval/parque-wheel-check.mjs | 94 + tools/eval/pause-check.mjs | 5 +- tools/eval/penitenciaria-check.mjs | 73 + tools/eval/pickup-arma-check.mjs | 189 ++ tools/eval/pickup_check.json | 1276 ++++++++- tools/eval/portao-inteiro-check.mjs | 68 + tools/eval/preload-check.mjs | 94 + tools/eval/probe-grafite.mjs | 31 + tools/eval/redesign-check.mjs | 188 +- tools/eval/replaycam-check.mjs | 137 + tools/eval/replaycam-probe.mjs | 12 + tools/eval/select-inflate.mjs | 15 +- tools/eval/submit-guard-check.mjs | 43 + tools/eval/telemetry-check.mjs | 20 +- tools/eval/velho-oeste-check.mjs | 124 + tools/eval/vm-kick-sim.mjs | 48 +- tools/eval/vm_kick_sim.json | 2 +- tools/eval/webgl-compat-check.mjs | 2 +- tools/eval/workflow-secret-check.mjs | 74 + tools/eval/workflow-timeout-check.mjs | 77 + tools/gen-arch.mjs | 2 +- tools/gen-docs.mjs | 27 +- 70 files changed, 11819 insertions(+), 1220 deletions(-) create mode 100644 scripts/ci/autofix_allowlist.py create mode 100644 scripts/ci/greptile_gate.py create mode 100644 tools/eval/armas-check.mjs create mode 100644 tools/eval/autofix-check.mjs create mode 100644 tools/eval/backend-hints-check.mjs create mode 100644 tools/eval/base-ref.mjs create mode 100644 tools/eval/cache-baseline.json create mode 100644 tools/eval/cache-probe.mjs create mode 100644 tools/eval/comentario-check.mjs create mode 100644 tools/eval/deploy-gate-check.mjs create mode 100644 tools/eval/deps-check.mjs create mode 100644 tools/eval/docs-autoria-check.mjs create mode 100644 tools/eval/entrada-check.mjs create mode 100644 tools/eval/fixture-check.mjs create mode 100644 tools/eval/mutacao-cega-check.mjs create mode 100644 tools/eval/parque-wheel-check.mjs create mode 100644 tools/eval/penitenciaria-check.mjs create mode 100644 tools/eval/pickup-arma-check.mjs create mode 100644 tools/eval/portao-inteiro-check.mjs create mode 100644 tools/eval/preload-check.mjs create mode 100644 tools/eval/probe-grafite.mjs create mode 100644 tools/eval/replaycam-check.mjs create mode 100644 tools/eval/replaycam-probe.mjs create mode 100644 tools/eval/velho-oeste-check.mjs create mode 100644 tools/eval/workflow-secret-check.mjs create mode 100644 tools/eval/workflow-timeout-check.mjs diff --git a/package-lock.json b/package-lock.json index 0e859713a..e25564558 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,21 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.136", + "version": "2.0.0-alpha.176", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coro-solto", - "version": "2.0.0-alpha.136", + "version": "2.0.0-alpha.176", "license": "AGPL-3.0-only", "dependencies": { - "@astrojs/vercel": "^11.0.3", + "@astrojs/vercel": "^11.0.6", "@resvg/resvg-js": "^2.6.2", "@resvg/resvg-wasm": "^2.6.2", "@supabase/supabase-js": "^2.110.7", "astro": "^7.1.1", "dejavu-fonts-ttf": "^2.37.3", + "ndarray-pixels": "^5.2.0", "sharp": "^0.35.3" }, "devDependencies": { @@ -261,12 +262,12 @@ } }, "node_modules/@astrojs/vercel": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@astrojs/vercel/-/vercel-11.0.3.tgz", - "integrity": "sha512-UMhcZ/lWB0/B2l1L8BJh6QxysjZt8SLS9e40xRfBdVhfi3Y6SIDw+99rY/+OGW/yem/S7sBRkvqvto8neevO2A==", + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/@astrojs/vercel/-/vercel-11.0.6.tgz", + "integrity": "sha512-GW+2Sw0qWnpuAa2dFqid8nmdSPWw3ZhF+GXxLZkyAl//H+bzNsG0N5kYXO9UU88jtDH91igYqQE1oUq1AJrOZA==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.10.1", + "@astrojs/internal-helpers": "0.10.3", "@vercel/analytics": "^1.6.1", "@vercel/functions": "^3.4.3", "@vercel/nft": "^1.3.2", @@ -278,6 +279,22 @@ "astro": "^7.0.0" } }, + "node_modules/@astrojs/vercel/node_modules/@astrojs/internal-helpers": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.3.tgz", + "integrity": "sha512-HIx/t1NywcqSTFaVwZh15n8JsC3YQdCaJZDaTS/aurYTEvNiWDXUGS3Z9uQX3bFB+B1pCgN/nljckkzYTpHZ2w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "js-yaml": "^4.3.0", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "unified": "^11.0.5" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -2341,7 +2358,6 @@ "version": "1.0.14", "resolved": "https://registry.npmjs.org/@types/ndarray/-/ndarray-1.0.14.tgz", "integrity": "sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==", - "dev": true, "license": "MIT" }, "node_modules/@types/nlcst": { @@ -2810,15 +2826,15 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/ccount": { @@ -3056,7 +3072,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz", "integrity": "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==", - "dev": true, "license": "MIT", "dependencies": { "uniq": "^1.0.0" @@ -3658,7 +3673,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", "integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==", - "dev": true, "license": "MIT" }, "node_modules/iron-webcrypto": { @@ -3674,7 +3688,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true, "license": "MIT" }, "node_modules/is-docker": { @@ -3732,9 +3745,9 @@ } }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "funding": [ { "type": "github", @@ -4241,9 +4254,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -4262,7 +4275,6 @@ "version": "1.0.19", "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.0.19.tgz", "integrity": "sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==", - "dev": true, "license": "MIT", "dependencies": { "iota-array": "^1.0.0", @@ -4284,548 +4296,21 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/ndarray-ops/-/ndarray-ops-1.2.2.tgz", "integrity": "sha512-BppWAFRjMYF7N/r6Ie51q6D4fs0iiGmeXIACKY66fLpnwIui3Wc3CXiD/30mgLbDjPpSLrsqcp3Z62+IcHZsDw==", - "dev": true, "license": "MIT", "dependencies": { "cwise-compiler": "^1.0.0" } }, "node_modules/ndarray-pixels": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ndarray-pixels/-/ndarray-pixels-5.0.1.tgz", - "integrity": "sha512-IBtrpefpqlI8SPDCGjXk4v5NV5z7r3JSuCbfuEEXaM0vrOJtNGgYUa4C3Lt5H+qWdYF4BCPVFsnXhNC7QvZwkw==", - "dev": true, + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ndarray-pixels/-/ndarray-pixels-5.2.0.tgz", + "integrity": "sha512-lTh4tFKziAatVTa9crIsidUyn+lqujVOQpzfdBWvdFu2wo9Uo6z261lVX7SgMyP89xGmj3TMTPbbxl9YDnV4SA==", "license": "MIT", "dependencies": { "@types/ndarray": "^1.0.14", "ndarray": "^1.0.19", "ndarray-ops": "^1.2.2", - "sharp": "^0.34.0" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/ndarray-pixels/node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "sharp": "^0.35.0" } }, "node_modules/neotraverse": { @@ -5099,9 +4584,9 @@ } }, "node_modules/path-to-regexp": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz", - "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "license": "MIT" }, "node_modules/path-to-regexp-updated": { @@ -5183,9 +4668,9 @@ } }, "node_modules/postcss": { - "version": "8.5.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", - "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -5202,7 +4687,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5589,9 +5074,9 @@ } }, "node_modules/tar": { - "version": "7.5.20", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.20.tgz", - "integrity": "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -5735,7 +5220,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==", - "dev": true, "license": "MIT" }, "node_modules/unist-util-is": { diff --git a/package.json b/package.json index a7cc5bbbc..29eebdad5 100644 --- a/package.json +++ b/package.json @@ -1,207 +1,159 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.136", + "version": "2.0.0-alpha.176", "license": "AGPL-3.0-only", "description": "CORO SOLTO: Treta Suprema (ex-CS BRASIL) — FPS satírico de navegador com 5 facções brasileiras (site Astro + jogo vanilla em Three.js), gerado em par com agentes de IA (AI generated & AI friendly)", "scripts": { "dev": "npm run copy-wasm && astro dev", "build": "npm run copy-wasm && astro build && node scripts/prune-dist.mjs", - "//prune-dist": "T2. Roda no fim do build, tira as bancadas locais e, salvo KEEP_FPVM=1, models/fpvm do publicado. Poda dist e o espelho .vercel/output/static.", - "//release": "Bump + CHANGELOG + tag num comando. A versão vive em package.json e version.js, e o ?v= do import map sai do package.json — este script mantém os dois em sincronia por construção. Existe porque com commit direto na main o `versao-bumpada` do pr-gates (que só roda em pull_request) passa ao largo, e o código chegou a alpha.35 com a última tag em v2.0.0-alpha.32. NÃO faz push: publicar é ação pra fora. Recusa árvore suja. `--seco` mostra sem escrever.", "release": "node scripts/release.mjs", - "//setup": "T26. UM comando do `git clone` ao jogo inteiro: deps, wasm, pacote de áudio, acervo de decalques, e a asserção do T1 por cima. NÃO é cadeia de && — cada passo roda, o estado de todos é impresso junto, e a mensagem final diz o que fazer. Existe porque quem clona pega jogo mudo e com parede pelada (public/audio, public/img/decals e references ficam fora do git por procedência), e quem a gente quer atrair não debuga setup: fecha a aba.", "setup": "node scripts/setup.mjs", - "//prepare": "Ativa o hook nativo que acrescenta Signed-off-by com user.name/user.email em todo commit. Evita DCO vermelho por esquecimento sem trazer Husky; preserva core.hooksPath preexistente e mantém o CI como portão para clones sem o hook.", "prepare": "node scripts/install-git-hooks.mjs", "copy-wasm": "node scripts/copy-wasm.mjs", - "preview": "python3 -m http.server 4321 -d dist/client", + "preview": "python3 -m http.server ${PORT:-4321} -d dist/client", "fetch-audio": "bash scripts/fetch-audio.sh", "arch": "node tools/gen-arch.mjs", "arch:check": "node tools/gen-arch.mjs --check", - "//docs": "Blocos NUMÉRICOS do README.md e de docs/ gerados do código (contagem de linhas, elenco, mapas, versões da stack, invariantes, skills). Existe porque um SKILL.md afirmava 3.234 linhas de game.js quando o arquivo tinha 6.427 — corrigir à mão dura um commit. `--check` está no check:fast: gerador sem portão desatualiza em uma semana.", "docs": "node tools/gen-docs.mjs", "docs:check": "node tools/gen-docs.mjs --check", + "docs:check:autoria": "node tools/gen-docs.mjs --check --autoria", + "eval:docsautoria": "node tools/eval/docs-autoria-check.mjs", "syntax": "for f in public/js/*.js public/js/botbrain/*.js; do node --input-type=module --check < \"$f\" || exit 1; done", "eval:invariants": "node tools/eval/invariants.mjs", - "//prod:coherence": "Mede o que o EDGE está servindo, não o repo: baixa o HTML de produção, segue o import map e verifica que todo import nomeado existe como export no módulo alvo. Nasceu do apagão de 08/08 (cache split-brain: main.js de um deploy com fparms.js de outro — BUG-39). NÃO entra no check:fast (precisa de rede e de produção no ar); quem roda é o prod-watch.yml a cada 15 min. `--selftest` é a mutação que prova que morde.", "prod:coherence": "node tools/eval/prod-coherence.mjs", - "//eval:release": "Release preserva nome/créditos, DCO, docs e um único deploy automático pela main; o CLI fica manual. Mutantes: nome-antigo|semcreditos|semdco|semdocs|semrollback|deploy-duplo.", "eval:release": "node tools/eval/release-check.mjs", "eval:uuid": "node tools/eval/uuid-compat-check.mjs", - "//eval:drop": "procedência de DROP_TTL/DROP_MAX: acúmulo de armas no chão em regime", "eval:drop": "node tools/eval/drop-check.mjs", - "//eval:escala": "procedência da escala dos veículos do estacionamento: bbox real do glb contra ficha de fábrica", + "eval:deps": "node tools/eval/deps-check.mjs", "eval:escala": "node tools/eval/escala-veiculo-check.mjs", - "//eval:prune": "KEEP_FPVM preserva só models/fpvm; as bancadas continuam podadas dos dois destinos publicados. --mutante=early-exit prova a restrição.", "eval:prune": "node tools/eval/prune-check.mjs", - "//eval:mapjson": "A régua do formato 'mapa como dado' (issue #210): sobe o mapa JSON pelo three do harness e confere grafo de waypoints conexo, aresta bidirecional e spawn na área jogável. --mutante=grafo|aresta|spawn prova que morde.", "eval:mapjson": "node tools/eval/mapjson-check.mjs", - "//eval:mapcontrato": "Todo mapa DO REGISTRO devolve o que o game.js CONSOME. Irma da eval:mapjson, que valida um spec JSON antes do build: esta valida o registro inteiro, inclusive os mapas escritos a mao. A lista de chaves nao e opiniao — obrigatorio = o jogo desreferencia SEM guarda (waypoints game.js:4157, nearestWaypoint :4292, findPath :4298); opcional = ha guarda com fallback (slowAt, groundHeightAt). Mutacao: --mutante=sem-waypoints. Mapa ainda nao registrado: --extra=arquivo.js", "eval:mapcontrato": "node tools/eval/map-contrato-check.mjs", - "//eval:vminspect": "O default do vm-inspect precisa ser uma arma publicada, existente e versionada. Nasceu do #141: o caminho antigo dava 404; a primeira correção apontava para o corpo do doador e mostrava pernas/botas. --mutante=fantasma|corpo prova as duas cláusulas.", + "eval:parquewheel": "node tools/eval/parque-wheel-check.mjs", + "eval:velhooeste": "node tools/eval/velho-oeste-check.mjs", + "eval:penitenciaria": "node tools/eval/penitenciaria-check.mjs", "eval:vminspect": "node tools/eval/vm-inspect-check.mjs", - "//eval:pegada": "Colisor de prop na ALTURA DO CORPO (Brasília): recomputa a pegada dos GLBs (tent/stall/drinkstand/bus) e acusa deriva da tabela PEGADA_CORPO / PEGADA_BUS. Nasceu da reprovação de 05/08 ('box do ônibus e barracas') com o obb-check VERDE — ele compara contra a caixa declarada e não vê caixa mais gorda que a malha.", "eval:pegada": "node tools/eval/pegada-check.mjs", - "//eval:dmgdir": "O arco de dano na borda da tela (_dmgArc) tem que apontar pro atacante, não pro lado oposto. Nasceu do relato do dono (BUG-52): tiro na frente desenhava o arco embaixo (COSTAS). Executa _dmgArc de game.js com o atacante nas 4 direções cardeais em 7 yaws da vítima; --mutante=ordem-trocada reintroduz a troca de operandos do atan2.", "eval:dmgdir": "node tools/eval/dmgdir-check.mjs", - "//eval:ctflabels": "Bandeira com nome do PRÓPRIO mapa, declarada pelo mapa (defeito de 06/08: 'CONGRESSO' jogando na piscina — o fallback do game.js vazava os nomes de Brasília). --mutante=vaza prova que morde.", "eval:ctflabels": "node tools/eval/ctf-labels-check.mjs", - "//eval:faccao": "Cor de facção tem UMA origem (public/js/paleta.js) e ela cobre o elenco. Nasceu do dono jogando em 07/08: 'quando captura bandeira não pinta de vermelho e nem põe o brasão' — o rename Time E (06/08) trocou a letra em BRASAO e no arquivo e esqueceu COR_TIME, então bandeiraTextura('E') devolvia null e o pano saía sem cor e sem brasão. O mesmo rename passou batido em TEAM_RIM (contorno branco) e na faixa do peito (braçadeira azul). Nenhum dos três dá erro no console. Os três espelhos ACABARAM: game.js, brasoes.js e characters.js importam de paleta.js, e F2 impede espelho novo de nascer. Estava fora de TODO portão até agora — régua escrita e não pendurada é régua que não roda; entrou no check:fast, que é o gatilho certo (node puro, milissegundos). `--mutar=sem-e|espelho` prova que morde.", "eval:faccao": "node tools/eval/faccao-paleta-check.mjs", - "//eval:mapid": "Id de mapa é NOSSO, e id antigo não morre calado. Os ids eram herança do CS 1.6 (awp_map e fy_pool_day são nomes literais de lá; fy_ é convenção de lá) enquanto os nomes exibidos já eram brasileiros — só o id carregava o CS, e id VAZA: vai gravado no banco (match.ts manda p_map em toda partida) e viaja em link (?map=fy_quebrada). Como resolveMapId devolve o mapa padrão para id desconhecido, link antigo não daria erro: abriria a Praça no lugar da Quebrada, calado — a mesma falha silenciosa do rename Time E, que apagou a bandeira sem uma linha no console. M1 nenhum id no estilo CS (fy_/de_/aim_/awp_) sobrevive no código vivo; M2 todo id antigo resolve para mapa que existe; M3 todo mapa tem a prévia em disco com o nome do id (renomear id sem renomear a imagem dá 404 no menu, não erro de build). CHANGELOG, KNOWN-BUGS e docs/historico ficam fora: são registro do passado, e o alias é o que os mantém verdadeiros E navegáveis. `--mutar=id-cs|sem-alias|sem-preview` provam que morde.", "eval:mapid": "node tools/eval/mapa-id-check.mjs", - "//eval:maprotate": "Rotação de mapas: link ?map= manda, escolha explícita fica, e sem escolha a rotação percorre o registro. O check:fast citava este nome sem o script existir — portão morto na corrente. A régua real é tools/eval/map-rotation-check.mjs.", "eval:maprotate": "node tools/eval/map-rotation-check.mjs", - "//eval:submitguard": "A trava anti-fraude do banco não pode recusar partida que o jogo produz. Issue #87 (maurodesouza): 'stats não enviados: partida rápida demais pra ser verdade' numa partida de CAPTURA legítima no quebrada. A cláusula exigia 80 s por rodada — número que veio do ABATE, onde a rodada é uma janela de 99 s; no CAPTURA a rodada não tem janela nenhuma. Medido: 6 de 50 partidas de captura jogadas até o fim eram recusadas, mais 4 de 50 abandonos, E a cláusula chamava _flag() antes de recusar (3 strikes escondem o jogador do ranking). FORA DO `check` DE PROPÓSITO, pelo mesmo motivo do eval:boot: o piso é LIDO de ~/db-privado (o banco não mora neste repo, .gitignore:145-148) e sem esse insumo a régua não sabe medir — e não saber custa o mesmo que estar errado, então ela fica VERMELHA em vez de passar calada. É passo de pré-deploy. SG1 (física)/SG4 (sem strike)/SG5 (default brando) rodam em segundos; SG2/SG3 sobem o motor e pedem --amostra (~10 min). `--mutante=piso80|comflag|defaultduro` provam que morde.", "eval:submitguard": "node tools/eval/submit-guard-check.mjs", - "//eval:ctrlw": "Agachar andando pra frente não pode fechar a aba. Relato do Daniel Diniz (07/08): 'quando fica muito tempo com a tecla Control pressionada a página fecha' — no Windows, não no Mac. Não é o Control: agachar é ControlLeft/Right e andar pra frente é W, então agachar avançando É Ctrl+W (no Mac o atalho é Cmd+W, por isso não reproduzia aqui). preventDefault não alcança atalho reservado; quem resolve é a Keyboard Lock API em tela cheia, com a confirmação de saída do beforeunload como segunda camada pra Firefox/Safari. CW3 cobra o SILÊNCIO no menu — confirmação que aparece sempre vira praga e é arrancada inteira depois, levando o conserto junto. EXIGE BROWSER: fora do `check`, passo de pré-deploy. `--mutante=semlock|semprompt|promptsempre` provam que morde, e a régua morre se o mutante não casar o texto.", "eval:ctrlw": "node tools/eval/ctrlw-check.mjs", - "//eval:boot": "O JOGO ABRE E FALHA DE FORMA ACIONÁVEL? No Chrome real, exige zero pageerror, o `onclick` do #btn-jogar ligado e uma falha injetada no início da partida voltando ao menu com modal amigável, telemetria automática/manual, erro preservado no console e detalhe técnico só em ?debug=1. Nasceu do BUG-34 (TDZ deixava JOGAR inerte) e do BUG-42 (erro bruto assustava o jogador sem dar recuperação). EXIGE BROWSER: fica fora do `check` e é passo obrigatório antes de deploy. `--mutante=tdz|sem-amigavel|vaza-detalhe|sem-console-watchdog` prova as cláusulas.", "eval:boot": "node tools/eval/boot-check.mjs", - "//eval:ssr": "PÁGINA SSR ENTREGA CORPO, medido no ARTEFATO do build. Nasceu do dono em 12/08: 'a pagina mapa online (aovivo) esta quebrada'. Eram as TRÊS páginas prerender=false (/mapa, /ranking, /u/) devolvendo 200 com 0 bytes em produção e no preview, desde 3e5b0ea (#194, 11/08 23:23) — que pôs moduleCacheManifest() no escopo do módulo do Layout.astro. Ele faz readdirSync('public/js') relativo ao cwd: no build o diretório existe, dentro da função da Vercel NÃO, e como o Astro faz streaming o 200 já tinha saído quando o ENOENT estourou. 500 teria sido barulhento; 200 com casca vazia passou um dia no ar. O eval:site cobre /ranking e checa corpo, e passou o tempo todo porque sobe um astro dev LOCAL, onde public/js existe — LIÇÃO 3, a régua media outro mundo. Esta aqui entra no diretório da função e chama o handler construído, que é o cwd de produção. EXIGE BUILD, não exige browser nem rede: passo de pré-deploy junto do eval:boot. `--mutante=corpo-vazio|lanca` provam que morde; `--mutante=sem-publicjs` é asserção de que a leitura de disco em tempo de renderização continua morta.", "eval:ssr": "node tools/eval/ssr-render-check.mjs", - "//eval:cena": "CUSTO DE CENA TEM TETO. tools/eval/gl-metrics.mjs já media calls/triângulos reais por frame desde a rodada 3 — e ninguém reprovava: `grep -nE 'calls|tris|draw' tools/eval/invariants.mjs` não devolvia uma linha, e o único teto escrito no repo era prosa no comentário de mapprops.js:15-17 ('300-800 calls e 500 k tris'). Número medido sem consequência envelhece sem avisar: foi assim que loja_h chegou a 4.347 calls e 3,65 M triângulos antes de alguém olhar. A sonda antiga também cobria só 4 mapas — faltava o quinto, que é justamente o pior (1.8k calls, metade do fps dos outros). O teto mora em tools/eval/cena-tetos.mjs, compartilhado com as cláusulas CENA do invariants (LIÇÃO 2: dois limiares para o mesmo conceito é o instrumento discordando de si). EXIGE BROWSER: fica fora do `check` e do `check:fast`, igual ao eval:boot, e é passo de pré-deploy. Backend padrão é o do sistema e não swiftshader — `info.render` é contabilidade do three, não do driver, então trocar backend não move calls/tris (move fps, que é reportado e NÃO tem teto); medido, swiftshader levou 79 s até `live` e derrubou o processo de GPU no meio, contra 8,3 s do padrão. `--gl=swiftshader` para CI sem GPU. `--mutante=estoura` desliga instancing e culling no módulo servido e prova que morde.", "eval:cena": "node tools/eval/cena-check.mjs", "eval:vm": "node tools/eval/vm-mint-audit.mjs", "eval:kick": "node tools/eval/vm-kick-sim.mjs", "eval:bots": "node tools/eval/botsim.mjs 60 all", - "//eval:site": "Contrato das 13 rotas do site SEM Supabase (o estado do CI): status, corpo, XML do sitemap e JSON-LD parseável. `astro build` verde não prova nada disso — /ranking pode dar 500 e o build passa. Sobe o astro dev sozinho; SITE_URL aponta pra um alvo externo. --mutante=jsonld prova que morde.", "eval:site": "node tools/eval/site-smoke.mjs", "eval:telemetry": "node tools/eval/telemetry-check.mjs", - "//eval:identity": "UID é a identidade estável, token é a prova e nick é atributo. Cobra cliente + APIs + fallback temporário e tem mutantes semuid-client|nick-auth|semcanonical.", "eval:identity": "node tools/eval/identity-check.mjs", - "//eval:error-console": "Exceções globais e promises rejeitadas continuam visíveis no console nativo além da telemetria. Mutantes: erro|promise.", "eval:error-console": "node tools/eval/error-console-check.mjs", - "//eval:error-origin": "Erros de extensão e scripts cross-origin continuam brutos, mas não acionam watchdog, dispatch ou issue do jogo. Mutantes: sem-extensao|sem-cross-origin|filtro-amplo|sem-api|sem-early-return|sem-workflow|abre-externo|sem-cliente|cliente-mensagem-url|sem-teto-externo|debug-externo|cache-antes-origem.", "eval:error-origin": "node tools/eval/error-provenance-check.mjs", - "//eval:webgl": "Compatibilidade Linux/WebGL: executa a factory real com contextos controlados, exige degradação sem falso crash, fundo opcional, recuperação, qualidade temporária, zero sondas extras e shader de personagem válido em renderers distintos. Mutantes: alto-primeiro|sem-webgl1|erro-provisorio|contexto-extra|fundo-fatal|sem-context-loss|qualidade-persistida|canvas-reusado|preview-null|texture-lod-ext.", "eval:webgl": "node tools/eval/webgl-compat-check.mjs", - "//eval:webglguard": "Roteamento sem WebGL em main.js: retorno nulo de criaRenderer vira painel avisaSemWebgl() e aborta o boot com throw, sem tocar num renderer nulo. Fecha #105/#115/#215/#217/#104. Mutantes: sem-guard|sem-aviso|sem-throw|uso-antes.", "eval:webglguard": "node tools/eval/webgl-guard-check.mjs", - "//eval:shaderlog": "Logs WebGL nulos viram string vazia antes de trim; framebuffer nulo não derruba o WeakMap de drawBuffers; rotas usam versão, arnêses usam hash do core e addons sem URL própria revalidam na origem/CDN. Mutantes: sem-guardas|sem-cache-bust|addons-immutable|cloudflare-vendor|framebuffer-nulo.", "eval:shaderlog": "node tools/eval/shader-log-check.mjs", - "//eval:shaderbudget": "Todas as primitivas da urna precisam caber nos 8 varying vectors mínimos do WebGL1; deriva materiais, instancing, sombras/cookies, protege fog/triplanar e o grafo publicado completo do cache-bust. Mutantes: fog-separado|tri-separado|tri-varying|tri-helper-varying|sem-install|sem-patch|tri-flat|lam-flat|urna-color|urna-clearcoat|urna-anisotropy|urna-instancing|urna-segunda|sombra-extra|sombra-condicional|sombra-pontual|sombra-reativada|spot-map|cache-antigo|cache-omitido|cache-constante|cache-podado|cache-entry-site.", "eval:shaderbudget": "node tools/eval/shader-budget-check.mjs", - "//eval:og": "Renderiza e MEDE os 3 cards de og:image (1200×630) em node puro. Não dá pra testar pela rota: o dev server não carrega resvg-wasm porque src/lib/font-data.ts (996 KB de base64) estoura o parser de TS — a rota da badge quebra igual em dev. Também confere se a fonte decodificada tem tamanho plausível: extração truncada rendia card SEM TEXTO passando por 1200×630.", "eval:og": "node --experimental-strip-types --import ./tools/eval/ts-resolve.mjs tools/eval/og-check.mjs", - "//eval:sitemap": "Prova o fatiamento do sitemap com totais falsos (0, 5000, 5001, 12000). O modo índice é código que NINGUÉM alcança pedindo a URL — perfis só entram com RANKING_ON e precisariam de 5000+ jogadores. Checa páginas contíguas sem buraco, teto por página, XML bem formado nos dois modos e escape de & no nick.", "eval:sitemap": "node --experimental-strip-types tools/eval/sitemap-check.mjs", "eval:ctfhud": "node tools/eval/ctfhud-check.mjs", - "//eval:ratchet": "O ratchet de dívidas (KNOWN-RED.json) só anda pra frente: entrada NOVA reprova o PR, a menos que o corpo justifique com `ratchet: +ID porque `; entrada REMOVIDA é quitação e passa. Sem régua, quebrar uma invariante nova e pingar o ID na lista era a forma de deixar o portão verde — virou lista de desculpas.", "eval:ratchet": "node tools/eval/ratchet-check.mjs", - "//eval:mutate": "Mutation testing do viewmodel: regenera a auditoria, aplica cada mutante de tools/eval/mutantes.json, roda a régua alvo e restaura. MATOU = régua morde; SOBREVIVEU = invariante cega; PULADO = avaliação inconclusiva. Ctrl-C/SIGTERM restauram na hora.", "eval:mutate": "node tools/eval/mutate.mjs", - "//eval:vmlabhud": "O sidebar 1–5 existe no HUD real, com ou sem ?vmlab=1. Executa _updateWeaponHud com loadout completo, sem granadas, munição infinita e arma repetida; --mutante=escondido|duplicado-ativo prova presença e um único slot ativo.", "eval:vmlabhud": "node tools/eval/vmlab-hud-check.mjs", - "//bot:record": "BOTBRAIN: gera o dataset bootstrap (estado→ação) gravando os bots roteirizados no botsim — o professor da rede enquanto não há dado de jogador. Uso: [segundos] [mapId|all].", "bot:record": "node tools/eval/bot-record.mjs", - "//bot:train": "BOTBRAIN: treina o MLP com o dataset bootstrap e/ou o Supabase (--from-supabase), exportando public/models/bot-brain/. REQUER tfjs-node local (npm i -D @tensorflow/tfjs-node) — é nativa pesada, fora das deps do projeto. Uso: [--epochs=40] [--data=...].", "bot:train": "node tools/eval/bot-train.mjs", - "//bot:brain:check": "BOTBRAIN (régua Fase D): a rede é um controlador funcional (move+atira+mata, não congela) e a régua mede a rede — --mutante=zero zera os pesos e a régua reprova.", "bot:brain:check": "node tools/eval/bot-brain-check.mjs", - "//eval:botbrain": "Contrato de produção do BotBrain: coleta opt-in autenticada por UID, limites confiáveis, objetivo CTF, cache bust dos módulos, usuário não-root e balanceamento do corpus remoto. Mutantes: anonimo|ctf|optout|cache|root|poison.", "eval:botbrain": "node tools/eval/botbrain-safety-check.mjs", - "//eval:posters": "O aspecto declarado de cada cartaz em POSTER_FILES (textures.js) bate com o pixel do arquivo em public/posters/? Erro >6% = arte esticada na parede (issue #79). `--mutate` estraga um número e prova que a régua morde.", + "eval:pickuparma": "node tools/eval/pickup-arma-check.mjs", + "eval:fixture": "node tools/eval/fixture-check.mjs", + "eval:preload": "node tools/eval/preload-check.mjs", + "eval:armas": "node tools/eval/armas-check.mjs", + "eval:comentario": "node tools/eval/comentario-check.mjs", "eval:posters": "node tools/eval/poster-aspect-check.mjs", - "//eval:ui": "As 5 réguas de UI (UI1 contraste WCAG do HUD sobre o pior fundo medido, UI2 poluição, UI3 área morta sobre mira/viewmodel, UI4 ritmo por modo, UI5 paleta contra ref_ui.json). O arquivo existe desde a fase 5 do roadmap mas NÃO tinha script: nenhum `npm run` chamava, então ela só rodava quando alguém lembrava — justo a régua que o redesign AAA mais precisa. NÃO está em check:fast/check:deploy de propósito: o UI4 (ritmo do modo ABATE) está VERMELHO desde antes do redesign, e pendurar isso na cadeia bloqueante reprovaria o build por defeito alheio. Baseline de 13/08/2026: 4 de 5 passam.", "eval:ui": "node tools/eval/ui-check.mjs all", - "//eval:redesign": "Contrato do redesign: resultados estáticos com alpha, loading GLB ao vivo por facção, mapa full-bleed com carrossel visual, HUD 1–5 em máscaras 2D, seleção, i18n, preview 3D, configurações e placar. Cada cláusula tem mutante próprio.", "eval:redesign": "node tools/eval/redesign-check.mjs", - "//eval:screenquery": "Cada tela visual, inclusive placar, vida baixa, vitória e derrota, abre isoladamente por ?tela=nome ou número sem percorrer o funil. Valida aliases, parâmetros e integração no boot; os mutantes removem cada consumo real.", "eval:screenquery": "node tools/eval/screen-query-check.mjs", - "//eval:screenquery:browser": "Abre mapas, loading 3D, vitória, derrota, HUD normal/vida baixa e placar por ?tela=; captura em 3:2 e mede carrossel, ações GLB, mira, sidebar 1–5 e ausência de sobreposição. Exige Chrome e servidor já no ar, por isso fica fora dos gates rápidos.", "eval:screenquery:browser": "node tools/eval/screen-query-browser.mjs", - "//eval:loadingwall": "Issue #292: abre splash/loading em 16:9 e 3:2, DPR 1 e 2; exige arte inteira em contain sobre preenchimento cover e grava as oito capturas. Exige Chrome e servidor no ar.", "eval:loadingwall": "node tools/eval/loading-wallpaper-check.mjs", - "//eval:matchoptions": "Opções do mapa em tela cheia precisam atravessar o estado e governar o Game real; testa 1/3/5/7 rounds nos dois modos e os padrões históricos.", "eval:matchoptions": "node tools/eval/match-options-check.mjs", - "//eval:charvoice": "O clique no avatar toca uma fala determinística e exclusiva; bordões declarados mantêm a identidade mesmo se o pool mudar. A seleção automática abre muda. Mutantes: sem-clique|auto-fala|mesmo-som|sem-identidade|troca-clubber-rasta|faria-volta-lula|pack-antigo.", "eval:charvoice": "node tools/eval/character-select-voice-check.mjs", "eval:mat": "node tools/eval/mat-check.mjs", - "//eval:seo": "LÊ dist/client/ — o HTML publicado, não o .astro. Rode `npm run build` antes, ou use `npm run check:seo`, que já faz os dois. `--mutate` prova que a régua morde.", "eval:seo": "node tools/eval/seo-check.mjs", "eval:seo:mutate": "node tools/eval/seo-check.mjs --mutate", "check:seo": "npm run build && node tools/eval/seo-check.mjs", - "//eval:jsonld": "Valida o JSON-LD publicado contra o vocabulário OFICIAL do schema.org. Baixa 1,5 MB na primeira execução (cache em tools/eval/.cache/, git-ignorado); `--refresh` rebaixa. NÃO substitui o Rich Results Test do Google: requisito de rich result é política do Google, não do schema.org.", "eval:jsonld": "node tools/eval/jsonld-validate.mjs", "eval:pause": "node tools/eval/pause-check.mjs", - "//eval:select": "BALÃO NA TELA DE SELEÇÃO. Exige BROWSER e o servidor no ar: `npm run eval:serve &` antes. Mede o caminho REAL do preview (buildCharacterModel + arma + IK + curl + ctrl.update), que é onde o defeito mora — a pose-inflate lê o GLB do disco e é cega pra ele (KNOWN-BUGS BUG-25). ~4 min pros 44. Fora do `check` de propósito: `check` roda sem browser.", "eval:select": "node tools/eval/select-inflate.mjs", + "eval:entrada": "node tools/eval/entrada-check.mjs", + "eval:replaycam": "node tools/eval/replaycam-check.mjs", "eval:select:mutate": "node tools/eval/select-inflate.mjs mandrake,pagodeiro --mutate=skin", - "//assert:assets": "REPROVA O BUILD se o pacote de áudio/decalques não chegou inteiro ou se o layout gerado cita arte que saiu do pacote. Os fetches falham em silêncio; e, sem o contrato inverso da issue #77, peças somem da parede com só um console.warn. Roda no buildCommand da Vercel entre os fetches e o build. Mutações: AUDIO_PACK_URL inválida com public/audio vazio; `node tools/eval/assets-check.mjs --mutante=grafite-orfa`.", "assert:assets": "node tools/eval/assets-check.mjs", - "//strip:decalbg": "Alpha-key do branco dos decals de grafite (alfabeto-*.png etc.) depois do fetch-decals.sh. Fundo da folha virava retângulo branco atrás da letra em prod (13/08). Limiar alto (≥248) pra não comer conteúdo claro; STRIP_DECAL_BG=0 pula. Roda no buildCommand da Vercel.", "strip:decalbg": "node tools/strip-decal-bg.mjs", "eval:serve": "node tools/eval/serve.mjs 8123", - "//grafite": "ASSA a colocação do grafite dos 5 mapas. Roda a passada de graffiti_pass.js NO NAVEGADOR (único lugar onde os GLB existem) e congela o resultado em public/js/graffiti_layout.js. Exige `npm run eval:serve` no ar. REGERE depois de mexer em geometria de mapa, pool de decalque ou bandas — layout velho vira peça no lugar errado, e quem cobra isso é o eval:grafite.", "grafite": "node tools/gen-graffiti-layout.mjs", - "//eval:grafitelayout": "O LAYOUT ASSADO (public/js/graffiti_layout.js) não envelhece em silêncio (issue #82). Node puro, milissegundos, no check:fast — o eval:grafite mede cobertura no NAVEGADOR e por isso fica fora do check. Esta cobra MANIFESTO (todo nome de arquivo/mural do layout ainda existe no pool vivo, sem nome morto, peça bem formada, mapa nem a mais nem a menos) e FRESCOR (hash das entradas — fonte de cada map_*.js e da passada, gravado no layout via graffiti-fingerprint.mjs — bate com o de hoje; mexeu em parede/banda/passada sem regerar → VERMELHO, mande rodar `npm run grafite`). Escolhi o hash das entradas em vez do job de navegador do #82 porque roda em TODO PR por milissegundos. `--mutante=orfao|morto|mapa|vazio|mural|geometria|passada` prova que morde; `--duplo` prova o determinismo.", "eval:grafitelayout": "node tools/eval/graffiti-layout-check.mjs", - "//eval:grafite": "COBERTURA DE PAREDE PINTADA, medida no navegador. A decal-probe roda em node, onde nenhum GLB carrega, e por isso jurava 334 peças na Quebrada enquanto o dono via 12,7% de parede com arte. Esta abre cada mapa num Chrome de verdade, atira 16 raios de cada waypoint e conta quantas placas de parede visível têm tinta. Exige `npm run eval:serve` no ar.", + "eval:backendhints": "node tools/eval/backend-hints-check.mjs", + "changelog:check": "node scripts/sync-changelog.mjs --check", "eval:grafite": "node tools/eval/graffiti-census.mjs", - "//eval:grafite:ar": "A régua IRMÃ da cobertura, e ela existe porque cobertura é cega pro defeito oposto: peça flutuando não derruba o número, ela MELHORA (cobre a placa de trás). Mede peça a peça — vão atrás, tapada na frente, sobreposta. Exige navegador e `npm run eval:serve` no ar. `--fotos N` fotografa as piores: quando o número não move depois de um conserto que deveria movê-lo, é a foto que resolve.", "eval:grafite:ar": "node tools/eval/graffiti-audit.mjs", + "check:web": "node tools/eval/runner.mjs eval:grafite eval:select", "audio": "node tools/gen-audio-manifest.mjs", "audio:check": "node tools/gen-audio-manifest.mjs --check", "feet": "node tools/gen-foot-offsets.mjs", "feet:check": "node tools/gen-foot-offsets.mjs --check", - "//anims": "ÍNDICE de quais personagens têm clipe retargetado próprio (models/anims/index.json). Sem ele o jogo pedia os 11 clipes dos 44 e disparava 88 404 por partida — o barulho que enterrava exceção de verdade no console. `--mutante=sobrando|faltando|semguarda|semfetch` prova que a régua morde.", "anims": "node tools/gen-anim-manifest.mjs", "anims:check": "node tools/gen-anim-manifest.mjs --check", - "//anims:merge": "MESCLA os clipes por pasta em 1 GLB com clipes nomeados (.glb). Medido pelo boot-waterfall em 07/08: o boot frio fazia ~520 requests de clipe (1 por estado por personagem) — o custo não era byte, era round trip, e é o que fazia o online abrir muito mais lento que o local. As pastas-fonte continuam no disco (retarget, feet, fallback de runtime). `--check` exige mesclado em dia E versionado — mesclado só local vira 404 no deploy e o ganho evapora em silêncio (mesma armadilha da A4 do anims:check).", "anims:merge": "node tools/merge-anims.mjs", "anims:merge:check": "node tools/merge-anims.mjs --check", - "//walls:check": "Os arrays WALLS/LOADING_WALLS do main.js apontam pra arquivo que EXISTE no disco e é .webp. Nasceu de dois achados: o 404 do wall-1.png em produção (KNOWN-BUGS BUG-08, lista hardcoded que engole arte) e a conversão WebP de 07/08 (22 MB de PNG no boot, medido pelo boot-waterfall) — PNG é fonte, o jogo serve webp, e sem portão o peso volta em silêncio. `--mutante=fantasma|png` prova que morde.", "walls:check": "node tools/eval/walls-check.mjs", - "//verify:skills": "Supply chain (issue #42): recalcula o SHA-256 de cada skill de terceiro instalada em .agents/skills/ e compara com o skills-lock.json — sem isso o lock é texto que ninguém confere e não detecta skill que mudou depois de travada. Algoritmo canônico (UTF-8, sem BOM, LF, um \\n final) documentado no topo do script. Sem .agents/skills/ (o conteúdo saiu do git nesta release, reinstalado do lock) PULA e sai 0. `--update` regrava os hashes das instaladas.", "verify:skills": "node tools/verify-skills.mjs", "media": "node tools/gen-media-manifest.mjs", "media:check": "node tools/gen-media-manifest.mjs --check", - "//menuwalls": "Variantes 3:2 preservam o wallpaper inteiro no formato usado pelo dono; o manifesto liga fonte, receita e saída.", "menuwalls": "node tools/gen-menu-wallpapers.mjs", "menuwalls:check": "node tools/gen-menu-wallpapers.mjs --check", - "//travessao:check": "Nenhum travessão `—` (nem en-dash `–`) no texto público (src/). O em-dash é a marca de texto gerado por IA — num jogo que se vende como original, título/meta/OG/descrições cheios de `—` entregam a origem. A regra (use ` - `) está no CONTRIBUTING.md; esta é o portão pra ninguém reintroduzir. Escopo só src/ (o site); comentários de dev e docs geradas ficam de fora. `--mutante=inject` prova que morde.", "travessao:check": "node tools/eval/travessao-check.mjs", - "//eval:medianet": "Tranca os dois crashes do alpha.41 já consertados no upstream. #117: todo `.play()` de HTMLMediaElement (só nos módulos que constroem mídia — hoje audio.js e main.js) tem `.catch()` ou é Promise capturada e tratada; sem tratador, a rejeição de autoplay virava 'Uncaught (in promise)' e derrubava o boot. #125: o wrapper central `api()` de main.js embrulha o fetch em try/catch e RETORNA do catch (fail-silent) — sem isso, `TypeError: network error` volta a estourar. Estática (grep estruturado), sem browser nem rede: entra no `check`. Os `.play()` de THREE.AnimationAction ficam de fora porque animação e áudio moram em módulos separados (game.js/glbchars.js/fparms.js não constroem mídia). `--mutante=play|fetch` provam que morde.", "eval:medianet": "node tools/eval/media-net-check.mjs", - "//eval:ctfround": "A RODADA de captura fecha por OBJETIVO, não pela rede de segurança de 480 s. `--mutante=pace` reproduz o defeito do dono (1º fecho NUNCA, partida evapora aos 487 s).", "eval:ctfround": "node tools/eval/ctf-round-check.mjs", - "//eval:regen": "Regeneração fora de combate VETADA pelo dono em 05/08. `--mutante=ligado` devolve o 40 -> 100 hp que ele reportou.", "eval:regen": "node tools/eval/regen-check.mjs", - "//eval:ctfwin": "O alvo da rodada de captura é TODAS as bandeiras do mapa, não uma constante. Nasceu do dono jogando: 'na loja H está com 3 capturas quando a vitória tem que ser as 4'. `--mutante=constante` devolve o alvo fixo em 3 (a rodada fecha na 3ª de 4); `--mutante=menos1` prova que a cláusula do FECHO mede o motor e não a declaração.", "eval:ctfwin": "node tools/eval/ctf-win-check.mjs", - "//eval:spawn": "Ninguém nasce no ar nem no andar errado: |y(frame 30) − y(frame 0)| < 0,25 m em TODO spawn de TODO mapa, jogador e bot, pelo caminho real de respawn. Nasceu do dono jogando: 'o respawn do time dentro da loja, eles começam embaixo do mezanino e do nada sobem'. `--mutante=y0` devolve o y literal 0 e acende os 3,40 m de teleporte da Havan.", "eval:spawn": "node tools/eval/spawn-settle-check.mjs", - "//check:deploy": "O PORTÃO QUE RODA NO BUILD DA VERCEL, entre o assert:assets e o astro build. Existe porque a integração Git da Vercel publica em PARALELO com o CI: commit vermelho vai pro ar do mesmo jeito (aconteceu em 07/08, arch:check quebrado deployado). Aqui, vermelho = não publica. É subconjunto do check:fast de propósito — só o que foi PROVADO em clone descartável, sem navegador e sem rede. O eval:invariants ficou de FORA por medição: 376 s no clone limpo, e 6 min em todo deploy compram pouco (ele já roda no CI). Entram aqui só passos baratos e determinísticos: sintaxe, telemetria, identidade, poda do build, HUD do vmlab, default do vm-inspect, ARCH, docs geradas, manifesto de mídia e animação, texto público e aspecto dos cartazes.", - "check:deploy": "node tools/eval/runner.mjs syntax eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vmlabhud eval:vminspect eval:redesign eval:charvoice eval:screenquery arch:check docs:check media:check menuwalls:check anims:check anims:merge:check travessao:check eval:medianet eval:posters", - "//check:fast": "O runner percorre TODOS os passos e só reprova no fim; nenhum vermelho pode esconder os portões seguintes. A ordem ainda é deliberada: docs:check vem antes de arch:check para diagnosticar documentação derivada primeiro, e eval:redesign roda antes dos checks de mídia/animação porque também verifica o contrato entre fonte e assets.", - "check:fast": "node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock", - "//check": "eval:vm ANTES de eval:invariants: as invariantes de VM LEEM o vm_mint_audit.json que o eval:vm ESCREVE. Na ordem antiga o && cortava no primeiro vermelho e o JSON nunca era regenerado — o portão media o viewmodel de ontem e inventava vermelha (26/26 armas 'fora' que viraram 3/26 depois de regenerar). Ver KNOWN-BUGS.md BUG-02.", - "check": "npm run syntax && npm run audio:check && npm run eval:medianet && npm run eval:ctfhud && npm run eval:vm && npm run eval:invariants && npm run eval:kick && npm run eval:bots", - "//eval:tags": "Tag, main e versao contam a MESMA historia. Nasceu de defeito real: push nao atomico de dois refspecs deixou a v2.0.0-alpha.93 apontando pra commit que nunca chegou na main, e a sequencia pulou .92 -> .94. O versao-bumpada compara arquivo x arquivo e o eval:release valida gatilho; ninguem olhava o grafo do git. TM3 exige rede e gh: rode com --rede fora do portao rapido. Mutacoes: --mutante=tag-orfa|versao-atrasada|release-faltando|tag-fora-do-padrao", + "check:deploy": "node tools/eval/runner.mjs syntax eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vmlabhud eval:vminspect eval:redesign eval:charvoice eval:screenquery arch:check docs:check media:check menuwalls:check anims:check anims:merge:check travessao:check eval:medianet eval:posters eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam", + "check:fast": "node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam", "eval:tags": "node tools/eval/tag-main-check.mjs", - "//eval:simclock": "FPS baixo não pode desacelerar o relógio do jogo (issue #295). O clamp de 50 ms era TETO POR FRAME: abaixo de 20 FPS cada frame de 100 ms entregava 50 ms à simulação e a partida andava na metade do relógio de parede — relato de jogador em câmera lenta. O loop agora FATIA o frame em passos de ≤ 50 ms (mesma semântica por passo) com teto de 4 fatias (guard de espiral: máquina que não acompanha descarta o excesso) e só a última fatia renderiza. Estática estrutural (mesmo contrato do eval:medianet): SC1 fatio do delta real · SC2 teto com VALOR lido (nome existindo com 1e9 passava — furo do mutante) · SC3 clamp solto proibido. --mutante=clamp-frame|sem-teto.", - "eval:simclock": "node tools/eval/sim-clock-check.mjs" + "eval:simclock": "node tools/eval/sim-clock-check.mjs", + "eval:mutcega": "node tools/eval/mutacao-cega-check.mjs", + "eval:wfsecret": "node tools/eval/workflow-secret-check.mjs", + "eval:deploygate": "node tools/eval/deploy-gate-check.mjs", + "eval:autofix": "node tools/eval/autofix-check.mjs", + "eval:portaointeiro": "node tools/eval/portao-inteiro-check.mjs" }, "repository": { "type": "git", - "url": "git+https://github.com/rubenmarcus/csbrasil.git" + "url": "git+https://github.com/corosolto/client.git" }, "bugs": { - "url": "https://github.com/rubenmarcus/csbrasil/issues" + "url": "https://github.com/corosolto/client/issues" }, "homepage": "https://www.csbrasil.online", "dependencies": { - "@astrojs/vercel": "^11.0.3", + "@astrojs/vercel": "^11.0.6", "@resvg/resvg-js": "^2.6.2", "@resvg/resvg-wasm": "^2.6.2", "@supabase/supabase-js": "^2.110.7", "astro": "^7.1.1", "dejavu-fonts-ttf": "^2.37.3", + "ndarray-pixels": "^5.2.0", "sharp": "^0.35.3" }, "private": true, "type": "module", + "overrides": { + "@vercel/routing-utils": { + "path-to-regexp": "^6.3.0" + } + }, "devDependencies": { "@gltf-transform/core": "^4.4.1", "@gltf-transform/extensions": "^4.4.1", diff --git a/scripts/ci/autofix_allowlist.py b/scripts/ci/autofix_allowlist.py new file mode 100644 index 000000000..812c79212 --- /dev/null +++ b/scripts/ci/autofix_allowlist.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""A trava do autofix: decide se o que o bot mexeu pode virar commit. + +O bot só pode tocar em ARQUIVO GERADO — o que uma ferramenta do repositório +reescreve inteiro a partir do código. Se o conserto encostar em qualquer outro +caminho, o run aborta e comenta em vez de commitar. + +A regra não é conveniência: sem ela, o primeiro conserto errado reescreve o mapa +de um colaborador e ninguém repara. Com ela, o pior caso do bot é regenerar uma +doc que já era derivada. + +Uso: git status --porcelain | python3 scripts/ci/autofix_allowlist.py + python3 scripts/ci/autofix_allowlist.py --selftest +""" +import sys + +# Caminhos que uma ferramenta REGERA por inteiro. Crescer esta lista é decisão de +# dono: cada entrada nova é um arquivo que o bot passa a poder sobrescrever sozinho. +PERMITIDOS = ( + "ARCH.generated.md", + "README.md", + "STATUS.md", + "SCRIPTS.md", + "docs/", + "tools/eval/ARCH.md", +) + +# Caminhos que NUNCA entram, mesmo que um prefixo permitido pareça cobri-los. O +# `.github/` fica de fora porque um bot que edita o próprio workflow que o governa +# consegue ampliar a própria permissão em um commit. +PROIBIDOS = ( + ".github/", + "scripts/", + "supabase/", + "package.json", + "package-lock.json", + "vercel.json", +) + + +def permitido(caminho: str) -> bool: + if any(caminho.startswith(p) for p in PROIBIDOS): + return False + return any(caminho == p or caminho.startswith(p) for p in PERMITIDOS) + + +def separa(caminhos: list[str]) -> tuple[list[str], list[str]]: + ok = [c for c in caminhos if permitido(c)] + fora = [c for c in caminhos if not permitido(c)] + return ok, fora + + +def caminhos_do_porcelain(texto: str) -> list[str]: + """`git status --porcelain` -> lista de caminhos. + + Renomeio vem como `R velho -> novo`; o que importa é o destino, senão um + rename para fora da lista passaria como se fosse o arquivo de origem. + """ + saida = [] + for linha in texto.splitlines(): + if len(linha) < 4: + continue + caminho = linha[3:].strip() + if " -> " in caminho: + caminho = caminho.split(" -> ", 1)[1] + saida.append(caminho.strip('"')) + return saida + + +def selftest() -> int: + casos = [ + ("doc gerada passa", ["ARCH.generated.md"], True), + ("pasta docs passa", ["docs/docs/comecando.md"], True), + ("código do jogo NÃO passa", ["public/js/main.js"], False), + ("mapa de colaborador NÃO passa", ["public/js/map_havan.js"], False), + ("workflow NÃO passa", [".github/workflows/ci.yml"], False), + ("script de CI NÃO passa", ["scripts/ci/pr_classify.py"], False), + ("package.json NÃO passa", ["package.json"], False), + ("uma proibida contamina o lote", ["STATUS.md", "public/js/game.js"], False), + ("nada mexido é lote vazio", [], True), + ("CHANGELOG não é gerado por ferramenta nossa", ["CHANGELOG.md"], False), + ("lote típico de release só toca gerado", ["README.md", "STATUS.md", "docs/docs/comecando.md"], True), + ] + erros = 0 + for nome, caminhos, esperado in casos: + _, fora = separa(caminhos) + obtido = not fora + ok = obtido == esperado + erros += 0 if ok else 1 + print(f"{'ok ' if ok else 'FALHA'} {nome}: fora={fora!r}") + + porcelain = ' M ARCH.generated.md\nR docs/a.md -> public/js/game.js\n?? novo.txt\n' + lidos = caminhos_do_porcelain(porcelain) + ok = lidos == ["ARCH.generated.md", "public/js/game.js", "novo.txt"] + erros += 0 if ok else 1 + print(f"{'ok ' if ok else 'FALHA'} renomeio é julgado pelo DESTINO: {lidos!r}") + + print("selftest verde" if not erros else f"{erros} caso(s) vermelho(s)") + return 0 if not erros else 1 + + +def main() -> int: + if "--selftest" in sys.argv: + return selftest() + # `--caminhos`: a entrada já é uma lista crua (o `git diff --name-only` do merge), + # e não o porcelain. É o modo que o resolvedor de conflito usa. + if "--caminhos" in sys.argv: + caminhos = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] + else: + caminhos = caminhos_do_porcelain(sys.stdin.read()) + ok, fora = separa(caminhos) + if fora: + print("BLOQUEADO: o conserto encostou em arquivo que o bot não pode reescrever:") + for c in fora: + print(f" - {c}") + return 1 + for c in ok: + print(c) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/check_automerge.py b/scripts/ci/check_automerge.py index 8c1ddd590..65fdac5b5 100644 --- a/scripts/ci/check_automerge.py +++ b/scripts/ci/check_automerge.py @@ -17,8 +17,7 @@ def check_rollup_ok(rollup: list[dict]) -> bool: return True -def main() -> int: - pr = json.load(sys.stdin) +def decide(pr: dict) -> dict: labels = {l["name"] for l in pr.get("labels", [])} files = [f.get("path", "") for f in pr.get("files", [])] touches_workflows = any(path.startswith(".github/workflows/") for path in files) @@ -32,11 +31,69 @@ def main() -> int: and pr.get("mergeStateStatus") in {"CLEAN", "HAS_HOOKS"} and check_rollup_ok(pr.get("statusCheckRollup", [])) ) - print(json.dumps({ + return { "eligible": eligible, "coderabbit_blocked": coderabbit_blocked, "touches_workflows": touches_workflows, - })) + } + + +def selftest() -> int: + """Fixtures do que NUNCA pode virar merge automático. + + Este script decide merge sem humano no caminho. Nasceu sem teste, e um `not` + trocado aqui mescla PR vermelho na main - o defeito mais caro que este + arquivo consegue produzir. Cada caso abaixo é uma condição de bloqueio. + """ + base = { + "isDraft": False, + "labels": [{"name": "safe-automerge"}], + "files": [{"path": "src/pages/index.astro"}], + "reviewDecision": "APPROVED", + "mergeStateStatus": "CLEAN", + "statusCheckRollup": [{"__typename": "CheckRun", "conclusion": "SUCCESS"}], + } + + def com(**kw): + pr = dict(base) + pr.update(kw) + return pr + + casos = [ + ("caminho feliz mescla", base, True), + ("draft não mescla", com(isDraft=True), False), + ("sem o label safe-automerge não mescla", com(labels=[]), False), + ("PR que toca workflow não mescla", com(files=[{"path": ".github/workflows/ci.yml"}]), False), + ("coderabbit pendente bloqueia", + com(labels=[{"name": "safe-automerge"}, {"name": "needs-coderabbit-resolution"}]), False), + ("coderabbit resolvido libera", + com(labels=[{"name": "safe-automerge"}, {"name": "needs-coderabbit-resolution"}, + {"name": "coderabbit-resolved"}]), True), + ("mudança pedida não mescla", com(reviewDecision="CHANGES_REQUESTED"), False), + ("conflito não mescla", com(mergeStateStatus="DIRTY"), False), + ("check vermelho não mescla", + com(statusCheckRollup=[{"__typename": "CheckRun", "conclusion": "FAILURE"}]), False), + ("check pendente não mescla", + com(statusCheckRollup=[{"__typename": "CheckRun", "conclusion": None}]), False), + ("status legado vermelho não mescla", + com(statusCheckRollup=[{"__typename": "StatusContext", "state": "FAILURE"}]), False), + ("skipped e neutral não bloqueiam", + com(statusCheckRollup=[{"__typename": "CheckRun", "conclusion": "SKIPPED"}, + {"__typename": "CheckRun", "conclusion": "NEUTRAL"}]), True), + ] + erros = 0 + for nome, pr, esperado in casos: + obtido = decide(pr)["eligible"] + ok = obtido == esperado + erros += 0 if ok else 1 + print(f" {'ok ' if ok else 'FALHOU'} {nome}: eligible={obtido} (esperado {esperado})") + return 0 if not erros else 1 + + +def main() -> int: + if "--selftest" in sys.argv: + return selftest() + print(json.dumps(decide(json.load(sys.stdin)))) return 0 diff --git a/scripts/ci/coderabbit_gate.py b/scripts/ci/coderabbit_gate.py index afdaf5e57..0a16bf939 100644 --- a/scripts/ci/coderabbit_gate.py +++ b/scripts/ci/coderabbit_gate.py @@ -1,50 +1,60 @@ #!/usr/bin/env python3 -"""Gate de presença dos revisores-bot (CodeRabbit + estraga-codigo). - -PR com review/comentário de bot revisor ganha `needs-coderabbit-resolution` — o -automerge (check_automerge.py) fica bloqueado até o maintainer confirmar com -`coderabbit-resolved`. A confirmação é MANUAL de propósito: threads resolvidas -via API por um agente também valem, o maintainer é o arbítrio final. - -O estraga-codigo posta REVIEW (não issue-comment), então o payload precisa trazer -`reviews` além de `comments` — sem isso ele era invisível ao gate (PRs #302/#303 -passaram sem trava nenhuma em 16/08). -""" import json import sys -REVISORES = ("coderabbit", "estraga-codigo") - -def main() -> int: - payload = json.load(sys.stdin) +def decide(payload: dict) -> dict: comments = payload.get("comments", []) - reviews = payload.get("reviews", []) status_rollup = payload.get("statusCheckRollup", []) - def eh_bot(login): - return any(rev in (login or "").lower() for rev in REVISORES) - has_coderabbit_check = any( item.get("__typename") == "CheckRun" and "coderabbit" in (item.get("name") or "").lower() for item in status_rollup ) - has_bot_comment = any( - eh_bot((comment.get("author") or {}).get("login")) for comment in comments - ) - has_bot_review = any( - eh_bot((review.get("author") or {}).get("login")) for review in reviews + has_coderabbit_comment = any( + "coderabbit" in ((comment.get("author") or {}).get("login") or "").lower() + for comment in comments ) - print( - json.dumps( - { - "needs_resolution": bool(has_coderabbit_check or has_bot_comment or has_bot_review), - "labels_add": ["needs-coderabbit-resolution"] if (has_coderabbit_check or has_bot_comment or has_bot_review) else [], - "labels_remove": ["coderabbit-resolved"] if (has_coderabbit_check or has_bot_comment or has_bot_review) else [], - } - ) - ) + precisa = bool(has_coderabbit_check or has_coderabbit_comment) + return { + "needs_resolution": precisa, + "labels_add": ["needs-coderabbit-resolution"] if precisa else [], + "labels_remove": ["coderabbit-resolved"] if precisa else [], + } + + +def selftest() -> int: + """O que este portão erra é caro nos dois sentidos: falso negativo deixa PR com + revisão pendente virar automerge; falso positivo trava PR que ninguém revisou. + Fixtures portadas do greptile_gate.py, que ganhou as mesmas no PR #209.""" + casos = [ + ("check do coderabbit exige resolução", + {"statusCheckRollup": [{"__typename": "CheckRun", "name": "CodeRabbit"}]}, True), + ("comentário do bot exige resolução", + {"comments": [{"author": {"login": "coderabbitai[bot]"}, "body": "achei isso"}]}, True), + ("check legado do greptile não confunde", + {"statusCheckRollup": [{"__typename": "CheckRun", "name": "Greptile Review"}]}, False), + ("PR sem coderabbit não exige nada", {"comments": [], "statusCheckRollup": []}, False), + ("outro check não confunde", + {"statusCheckRollup": [{"__typename": "CheckRun", "name": "build"}]}, False), + ("author nulo não explode", + {"comments": [{"author": None, "body": "texto qualquer"}]}, False), + ("payload vazio não explode", {}, False), + ] + erros = 0 + for nome, payload, esperado in casos: + d = decide(payload) + ok = d["needs_resolution"] == esperado and bool(d["labels_add"]) == esperado + erros += 0 if ok else 1 + print(f" {'ok ' if ok else 'FALHOU'} {nome}: {d['needs_resolution']}") + return 0 if not erros else 1 + + +def main() -> int: + if "--selftest" in sys.argv: + return selftest() + print(json.dumps(decide(json.load(sys.stdin)))) return 0 diff --git a/scripts/ci/crash_dedupe.py b/scripts/ci/crash_dedupe.py index 3b3350b7f..79fe7fcc4 100644 --- a/scripts/ci/crash_dedupe.py +++ b/scripts/ci/crash_dedupe.py @@ -1,9 +1,50 @@ #!/usr/bin/env python3 +"""Duplicata de crash: agrupa por CLASSE de defeito, não por semelhança de título. + +O detector nasceu comparando títulos com SequenceMatcher a >= 0.84, e por isso não +via o caso mais comum. "Could not create a WebGL context, VENDOR = 0xffff, DEVICE = +0xffff, GL_VENDOR = Disabled…" e "Web page caused context loss and was blocked" são +o MESMO defeito - o navegador não entregou contexto WebGL -, mas o texto que o +navegador cospe muda tanto entre fabricante e driver que a semelhança fica abaixo do +piso. Medido em 12/08/2026: das 33 issues `crash-auto` abertas, **14 eram essa única +classe**, mais 4 de textura GLTF e 3 de shader. O backlog parou de significar. + +A classe vem de um sinal ESTÁVEL (a API que falhou), e não da prosa do erro. Título +que casa a mesma classe é duplicata, sem depender de semelhança. Fora de classe +conhecida, o comportamento antigo continua valendo - classificador que não conhece o +caso não pode virar "não é duplicata" nem "é". + +`--selftest` roda as fixtures com os títulos REAIS das issues que motivaram a +mudança. +""" import json import re import sys from difflib import SequenceMatcher +PISO_SEMELHANCA = 0.84 + +# Classe -> sinal. A ordem importa: a primeira que casar vence, e as mais +# específicas vêm antes (shader é um erro DENTRO de um contexto que existe). +CLASSES: list[tuple[str, re.Pattern]] = [ + ("shader-compilacao", re.compile(r"webglprogram|shader error|validate_status", re.I)), + ("gltf-textura", re.compile(r"gltfloader.*(couldn't load|failed to load)", re.I)), + ("webgl-sem-contexto", re.compile( + r"sem_webgl|could not create a webgl|webgl context could not|context loss|" + r"webgl creation failed|exhausted gl driver", re.I)), + ("midia-bloqueada", re.compile( + r"media resource was aborted|play method is not allowed|notallowederror", re.I)), + ("rede", re.compile(r"load failed|network error|error in input stream|failed to fetch", re.I)), + ("prod-watch", re.compile(r"prod-watch", re.I)), +] + + +def classify(title: str) -> str | None: + for nome, sinal in CLASSES: + if sinal.search(title): + return nome + return None + def normalize(title: str) -> str: t = title.lower().strip() @@ -14,35 +55,100 @@ def normalize(title: str) -> str: def find_duplicate(current: dict, others: list[dict]) -> dict | None: - current_norm = normalize(current["title"]) current_num = current["number"] + current_classe = classify(current["title"]) + current_norm = normalize(current["title"]) - best = None - best_score = 0.0 + # Canônica da classe: a issue ABERTA mais antiga (menor número), INCLUINDO a + # atual. Se a atual É a canônica, não há para onde apontar - marcar a canônica + # de duplicata de uma mais nova é o resultado errado (CodeRabbit, PR #209). + if current_classe: + mesma_classe = sorted( + (i for i in others + if classify(i["title"]) == current_classe), + key=lambda i: i["number"], + ) + if mesma_classe and mesma_classe[0]["number"] != current_num: + alvo = mesma_classe[0] + return { + "number": alvo["number"], + "title": alvo["title"], + "url": alvo.get("url", ""), + "score": 1.0, + "classe": current_classe, + "motivo": "mesma classe de defeito", + } + + best, best_score = None, 0.0 for issue in others: if issue["number"] == current_num: continue score = SequenceMatcher(None, current_norm, normalize(issue["title"])).ratio() if score > best_score: - best_score = score - best = issue + best_score, best = score, issue - if best and best_score >= 0.84: + if best and best_score >= PISO_SEMELHANCA: return { "number": best["number"], "title": best["title"], - "url": best["url"], + "url": best.get("url", ""), "score": round(best_score, 3), + "classe": None, + "motivo": "títulos semelhantes", } return None +def selftest() -> int: + reais = { + 196: "crash em produção: sem_webgl: nenhum contexto foi criado · webgl2/economia: Could not create a WebGL context, VENDOR = 0xffff, DEVICE = 0xffff, GL_VENDOR = Disabled", + 206: "crash em produção: sem_webgl: nenhum contexto foi criado · experimental-webgl/economia: Web page caused context loss and was blocked", + 128: "crash em produção: THREE.WebGLRenderer: A WebGL context could not be created. Reason: WebGL creation failed: * tryANGLE", + 130: "crash em produção: THREE.WebGLProgram: Shader Error 1282 - VALIDATE_STATUS false", + 113: "crash em produção: THREE.GLTFLoader: Couldn't load texture blob:https://www.csbrasil.online/8b888933", + 114: "crash em produção: THREE.GLTFLoader: Couldn't load texture blob:https://www.csbrasil.online/4e3a6d00", + 117: "crash em produção: The play method is not allowed by the user agent or the platform", + 122: "crash em produção: The fetching process for the media resource was aborted by the user agent", + 125: "crash em produção: network error", + 170: "crash em produção: Load failed", + 176: "Sugestão: sistema de progressão e evolução do jogador", + } + outras = [{"number": n, "title": t, "url": f"u{n}"} for n, t in reais.items()] + + casos = [ + ("webgl vendor x context loss", 206, 128, "webgl-sem-contexto"), + ("a canônica da classe não aponta pra mais nova", 128, None, None), + ("shader NÃO cai em webgl", 130, None, None), + ("gltf agrupa com gltf", 114, 113, "gltf-textura"), + ("autoplay e mídia abortada são a mesma classe", 122, 117, "midia-bloqueada"), + ("load failed e network error são a mesma classe", 170, 125, "rede"), + ("issue humana não vira duplicata de crash", 176, None, None), + ] + erros = 0 + for nome, num, esperado_num, esperada_classe in casos: + atual = {"number": num, "title": reais[num]} + d = find_duplicate(atual, outras) + obtido = d["number"] if d else None + classe = d["classe"] if d else None + ok = obtido == esperado_num and (esperada_classe is None or classe == esperada_classe) + erros += 0 if ok else 1 + print(f" {'ok ' if ok else 'FALHOU'} {nome}: #{num} -> {obtido} ({classe})") + + # A classe não pode engolir defeito que ainda não tem classe: sem sinal + # conhecido, o piso de semelhança continua sendo quem decide. + solto = {"number": 900, "title": "crash em produção: SyntaxError: illegal character U+009E"} + d = find_duplicate(solto, outras) + ok = d is None + erros += 0 if ok else 1 + print(f" {'ok ' if ok else 'FALHOU'} defeito sem classe não vira duplicata: {d}") + return 0 if not erros else 1 + + def main() -> int: + if "--selftest" in sys.argv: + return selftest() payload = json.load(sys.stdin) - current = payload["current"] - others = payload["others"] - out = {"duplicate": find_duplicate(current, others)} - print(json.dumps(out)) + print(json.dumps({"duplicate": find_duplicate(payload["current"], payload["others"])})) return 0 diff --git a/scripts/ci/dco_check.py b/scripts/ci/dco_check.py index 554013da4..9d189d1e5 100644 --- a/scripts/ci/dco_check.py +++ b/scripts/ci/dco_check.py @@ -4,23 +4,43 @@ import sys +def faltando(commits: list[tuple[str, str]]) -> list[str]: + return [sha for sha, body in commits if "Signed-off-by:" not in body] + + +def commits_do_intervalo(base: str, head: str) -> list[tuple[str, str]]: + """Um `git show` por sha, sem sentinela de texto. + + A versão anterior cortava o log em `==END==`, e corpo de commit que contenha + essa linha partia o registro e desgrudava o trailer do sha (mesmo defeito + achado no agente_check.py em 12/08). Sha vem de lista; corpo vem separado. + """ + shas = subprocess.check_output(["git", "log", "--format=%H", f"{base}..{head}"], text=True).split() + return [(sha, subprocess.check_output(["git", "show", "-s", "--format=%B", sha], text=True)) for sha in shas] + + +def selftest() -> int: + casos = [ + ("com sign-off", [("a", "fix: x\n\nSigned-off-by: r \n")], []), + ("sem sign-off", [("b", "fix: x\n")], ["b"]), + ("corpo com ==END== não parte o registro", + [("c", "fix: x\n\n==END==\n\nSigned-off-by: r \n")], []), + ("um sem, outro com", [("d", "fix\n"), ("e", "fix\n\nSigned-off-by: r \n")], ["d"]), + ] + erros = 0 + for nome, commits, esperado in casos: + obtido = faltando(commits) + ok = obtido == esperado + erros += 0 if ok else 1 + print(f" {'ok ' if ok else 'FALHOU'} {nome}: {obtido}") + return 0 if not erros else 1 + + def main() -> int: + if "--selftest" in sys.argv: + return selftest() payload = json.load(sys.stdin) - base = payload["baseRefOid"] - head = payload["headRefOid"] - log = subprocess.check_output( - ["git", "log", "--format=%H%x00%B%x00==END==", f"{base}..{head}"], - text=True, - ) - chunks = [c for c in log.split("==END==\n") if c.strip()] - missing = [] - for chunk in chunks: - parts = chunk.split("\x00", 2) - if len(parts) < 2: - continue - sha, body = parts[0].strip(), parts[1] - if "Signed-off-by:" not in body: - missing.append(sha) + missing = faltando(commits_do_intervalo(payload["baseRefOid"], payload["headRefOid"])) print(json.dumps({"ok": not missing, "missing": missing})) return 0 if not missing else 1 diff --git a/scripts/ci/ensure_labels.py b/scripts/ci/ensure_labels.py index 6810e02e0..e54053b22 100644 --- a/scripts/ci/ensure_labels.py +++ b/scripts/ci/ensure_labels.py @@ -17,18 +17,50 @@ "crash-auto": ("B60205", "crash de produção reportado por /api/jserror ou prod-watch"), "stale-backlog": ("c2e0c6", "Issue antiga ou desalinhada com o checkout atual; precisa revalidação"), "needs-repro": ("f9d0c4", "Issue precisa passos de reprodução mais concretos"), + "preview-autorizado": ("0e8a16", "Mantenedor revisou o SHA atual e liberou preview do fork na Vercel"), + "pronto-pra-merge": ("0e8a16", "Portões verdes e diff dentro do aceito: falta só o merge, que é humano"), } -def main() -> int: - for label in sys.argv[1:]: +def comandos(nomes: list[str]) -> list[list[str]]: + """Os `gh label create` que ESTE pedido produz. + + Nome fora do dicionário some em silêncio, e foi assim que `preview-autorizado` + passou meses sendo pedida por um workflow sem nunca ser criada — o job de preview + de fork inteiro virou código morto atrás de um rótulo que ninguém podia aplicar. + """ + out = [] + for label in nomes: if label not in LABELS: continue color, description = LABELS[label] - subprocess.run( - ["gh", "label", "create", label, "--color", color, "--description", description, "--force"], - check=True, - ) + out.append(["gh", "label", "create", label, "--color", color, "--description", description, "--force"]) + return out + + +def selftest() -> int: + casos = [ + ("rótulo conhecido vira um comando", ["safe-automerge"], ["safe-automerge"]), + ("desconhecido é ignorado", ["nao-existe"], []), + ("conhecido junto de desconhecido não contamina", ["nao-existe", "bot-fixable"], ["bot-fixable"]), + ("lista vazia não faz nada", [], []), + ("preview-autorizado está registrado", ["preview-autorizado"], ["preview-autorizado"]), + ] + erros = 0 + for nome, pedido, esperado in casos: + obtido = [c[3] for c in comandos(pedido)] + ok = obtido == esperado + erros += 0 if ok else 1 + print(f"{'ok ' if ok else 'FALHA'} {nome}: {obtido!r}") + print("selftest verde" if not erros else f"{erros} caso(s) vermelho(s)") + return 0 if not erros else 1 + + +def main() -> int: + if "--selftest" in sys.argv: + return selftest() + for comando in comandos(sys.argv[1:]): + subprocess.run(comando, check=True) return 0 diff --git a/scripts/ci/greptile_gate.py b/scripts/ci/greptile_gate.py new file mode 100644 index 000000000..ee725c276 --- /dev/null +++ b/scripts/ci/greptile_gate.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +import json +import sys + + +def decide(payload: dict) -> dict: + comments = payload.get("comments", []) + status_rollup = payload.get("statusCheckRollup", []) + + has_greptile_check = any( + item.get("__typename") == "CheckRun" and item.get("name") == "Greptile Review" + for item in status_rollup + ) + has_greptile_comment = any( + "greptile" in ((comment.get("author") or {}).get("login") or "").lower() + or "greptile" in (comment.get("body") or "").lower() + for comment in comments + ) + + precisa = bool(has_greptile_check or has_greptile_comment) + return { + "needs_resolution": precisa, + "labels_add": ["needs-greptile-resolution"] if precisa else [], + "labels_remove": ["greptile-resolved"] if precisa else [], + } + + +def selftest() -> int: + """O que este portão erra é caro nos dois sentidos: falso negativo deixa PR com + revisão pendente virar automerge; falso positivo trava PR que ninguém revisou.""" + casos = [ + ("check do greptile exige resolução", + {"statusCheckRollup": [{"__typename": "CheckRun", "name": "Greptile Review"}]}, True), + ("comentário do bot exige resolução", + {"comments": [{"author": {"login": "greptile-apps[bot]"}, "body": "achei isso"}]}, True), + ("body citando greptile exige resolução", + {"comments": [{"author": {"login": "alguem"}, "body": "o greptile apontou X"}]}, True), + ("PR sem greptile não exige nada", {"comments": [], "statusCheckRollup": []}, False), + ("outro check não confunde", + {"statusCheckRollup": [{"__typename": "CheckRun", "name": "build"}]}, False), + ("author nulo não explode", + {"comments": [{"author": None, "body": "texto qualquer"}]}, False), + ("payload vazio não explode", {}, False), + ] + erros = 0 + for nome, payload, esperado in casos: + d = decide(payload) + ok = d["needs_resolution"] == esperado and bool(d["labels_add"]) == esperado + erros += 0 if ok else 1 + print(f" {'ok ' if ok else 'FALHOU'} {nome}: {d['needs_resolution']}") + return 0 if not erros else 1 + + +def main() -> int: + if "--selftest" in sys.argv: + return selftest() + print(json.dumps(decide(json.load(sys.stdin)))) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/issue_review.py b/scripts/ci/issue_review.py index d2f2ab501..3ba186fc5 100644 --- a/scripts/ci/issue_review.py +++ b/scripts/ci/issue_review.py @@ -1,4 +1,8 @@ #!/usr/bin/env python3 +"""Triagem de issue: labels de decisão + comentário de review. + +`review_issue()` é importada pelo issue_sweep.py — assinatura é contrato. +""" import json import re import sys @@ -8,6 +12,76 @@ "backend", "ci", "harness", "seguranca", "personagens", "armas", "graficos", "arte" } +LABEL_REASONS = { + "bot-fixable": "escopo pequeno e determinístico — dá pra um bot rascunhar o conserto", + "needs-repro": "relato curto demais — falta contexto antes de alguém atacar", + "covered-by-pr": "já existe PR aberta que fecha esta issue", + "crash-auto": "report automático de crash em produção", +} + + +def render_comment(issue: dict, labels_add: list[str], covered: dict | None) -> str: + number = issue["number"] + title = (issue.get("title") or "").strip() + url = issue.get("url") or f"#" + author = ((issue.get("author") or {}).get("login")) + body = (issue.get("body") or "").strip() + labels_existing = sorted(l["name"] for l in issue.get("labels", [])) + + linhas = ["## csbrasil-bot issue review", ""] + + # ── veredito: a caixa colorida muda com a natureza da issue ─────────── + if covered: + tipo = "IMPORTANT" + elif "needs-repro" in labels_add: + tipo = "WARNING" + elif "bot-fixable" in labels_add: + tipo = "TIP" + else: + tipo = "NOTE" + quem = f"aberta por **@{author}**" if author else "" + veredito = f"**Issue #{number} — {title}** {quem}".strip() + detalhe = "" + if "needs-repro" in labels_add: + detalhe = f"\n> O relato tem **{len(body)} caracteres** — curto demais pra reproduzir. Descreva o que fez, o que esperava e o que aconteceu." + elif "bot-fixable" in labels_add: + detalhe = "\n> Mantainer pode comentar `/bot-fix` pra o bot abrir branch + draft PR." + linhas += [f"> [!{tipo}]", f"> {veredito}", detalhe, ""] + + # ── ficha da triagem: máximo de detalhe que o payload sustenta ──────── + linhas += ["### Ficha da triagem", ""] + if labels_existing: + linhas.append(f"- **labels existentes:** {', '.join(f'`{l}`' for l in labels_existing)}") + else: + linhas.append("- **labels existentes:** nenhuma") + if labels_add: + linhas.append("- **labels do bot:**") + linhas += [f" - `{l}` — {LABEL_REASONS.get(l, 'critério da triagem')}" for l in labels_add] + else: + linhas.append("- **labels do bot:** nenhuma — fora dos critérios automáticos") + linhas.append(f"- **tamanho do relato:** {len(body)} caracteres") + linhas.append(f"- **link:** {url}") + linhas.append("") + + # ── cobertura por PR: a informação que muda a decisão de quem chegou ── + if covered: + linhas += ["### Já tem PR cobrindo", "", + f"**PR #{covered['number']} — {covered['title']}**", f"{covered['url']}", "", + "> [!TIP]", + "> Se o conserto dessa PR resolver, dá close nela que a issue fecha junto.", ""] + + # ── próximo passo: quem lê sabe o que fazer ──────────────────────────── + linhas += ["### Próximo passo", ""] + if covered: + linhas.append("- revisar a PR que já cobre — não abrir frente paralela") + elif "needs-repro" in labels_add: + linhas.append("- autor completa o relato (passos, esperado, aconteceu)") + elif "bot-fixable" in labels_add: + linhas.append("- maintainer avalia `/bot-fix` (bot abre rascunho; humano revisa)") + else: + linhas.append("- triagem humana: esta issue não casou nenhum critério automático") + return "\n".join(linhas).rstrip() + "\n" + def review_issue(payload: dict) -> dict: issue = payload["issue"] @@ -29,38 +103,59 @@ def review_issue(payload: dict) -> dict: covered = None pattern = re.compile(rf"(closes|fixes|resolves)\s+#?{number}\b", re.I) for pr in prs: - body = pr.get("body") or "" - if pattern.search(body): + pr_body = pr.get("body") or "" + if pattern.search(pr_body): covered = {"number": pr["number"], "title": pr["title"], "url": pr["url"]} labels_add.append("covered-by-pr") break - lines = [ - "## csbrasil-bot issue review", - "", - f"- issue: `#{number}`", - ] - if labels_add: - lines.append(f"- labels sugeridas/aplicadas: `{', '.join(sorted(set(labels_add)))}`") - else: - lines.append("- nenhuma label adicional sugerida") - if covered: - lines.append(f"- já parece coberta pela PR #{covered['number']} — {covered['title']}") - if "bot-fixable" in labels_add: - lines.append("- candidata a `/bot-fix` por manter escopo pequeno/determinístico") - if "needs-repro" in labels_add: - lines.append("- falta contexto suficiente; pedir passos de reprodução antes de atacar") - return { "labels_add": sorted(set(labels_add)), - "comment": "\n".join(lines), + "comment": render_comment(issue, sorted(set(labels_add)), covered), "covered_pr": covered, } +def selftest() -> int: + base = {"number": 42, "title": "jogo trava no ferro velho", "url": "https://x/42", + "author": {"login": "ze"}, "labels": [{"name": "documentation"}], + "body": "x" * 80, "state": "open"} + casos = [] + r = review_issue({"issue": base, "prs": []}) + c = r["comment"] + casos += [ + ("heading presente", "## csbrasil-bot issue review" in c), + ("bot-fixable vira caixa TIP", "> [!TIP]" in c), + ("ficha cita tamanho do relato", "80 caracteres" in c), + ("label com motivo humano", "rascunhar o conserto" in c), + ("próximo passo menciona /bot-fix", "/bot-fix" in c), + ("labels_add coerente", r["labels_add"] == ["bot-fixable"]), + ] + curta = dict(base, body="trava", labels=[]) + r2 = review_issue({"issue": curta, "prs": []}) + casos += [ + ("relato curto vira WARNING", "> [!WARNING]" in r2["comment"]), + ("needs-repro com contagem", "5 caracteres" in r2["comment"]), + ] + pr_cobre = dict(base, labels=[]) + r3 = review_issue({"issue": pr_cobre, "prs": [ + {"number": 7, "title": "fix do travamento", "url": "https://x/pull/7", "body": "fixes #42"}]}) + casos += [ + ("PR cobrinte detectada", r3["covered_pr"]["number"] == 7), + ("covered vira caixa IMPORTANT", "> [!IMPORTANT]" in r3["comment"]), + ("link da PR no comentário", "https://x/pull/7" in r3["comment"]), + ] + erros = 0 + for nome, ok in casos: + print(f" {'ok ' if ok else 'FALHOU'} {nome}") + erros += 0 if ok else 1 + return 0 if not erros else 1 + + def main() -> int: - payload = json.load(sys.stdin) - print(json.dumps(review_issue(payload))) + if "--selftest" in sys.argv: + return selftest() + print(json.dumps(review_issue(json.load(sys.stdin)))) return 0 diff --git a/scripts/ci/issue_sweep.py b/scripts/ci/issue_sweep.py index b003b6926..a0d5ad906 100644 --- a/scripts/ci/issue_sweep.py +++ b/scripts/ci/issue_sweep.py @@ -6,8 +6,12 @@ from issue_review import review_issue -def main() -> int: - payload = json.load(sys.stdin) +def sweep(payload: dict) -> dict: + """Varre issues abertas e decide ação por issue: label, comentário ou nada. + + Só abre fio (should_comment) quando há PR cobrindo ou duplicata — label + sozinha não comenta, senão a varredura vira spam semanal. + """ prs = payload.get("prs", []) issues = payload.get("issues", []) actions = [] @@ -28,10 +32,12 @@ def main() -> int: [ comment, "", - "Possível duplicata automática detectada.", - f"- candidata: #{duplicate['number']} — {duplicate['title']}", - f"- link: {duplicate['url']}", - f"- similaridade: {duplicate['score']}", + "> [!TIP]", + "> **Possível duplicata detectada na varredura.**", + "> ", + f"> - candidata: **#{duplicate['number']}** — {duplicate['title']}", + f"> - similaridade: **{duplicate['score']}**", + f"> - comparar: {duplicate['url']}", ] ) actions.append( @@ -43,7 +49,43 @@ def main() -> int: } ) - print(json.dumps({"actions": actions})) + return {"actions": actions} + + +def selftest() -> int: + crash_a = {"number": 1, "title": "crash em produção: TypeError x", + "url": "u1", "author": None, "labels": [{"name": "crash-auto"}], + "body": "b" * 40, "state": "open"} + crash_b = {"number": 2, "title": "crash em produção: TypeError x", + "url": "u2", "author": None, "labels": [{"name": "crash-auto"}], + "body": "b" * 40, "state": "open"} + coberta = {"number": 3, "title": "bug do ferro velho", "url": "u3", + "author": {"login": "ze"}, "labels": [{"name": "documentation"}], + "body": "c" * 40, "state": "open"} + muda = {"number": 4, "title": "issue calada", "url": "u4", + "author": {"login": "ze"}, "labels": [{"name": "graficos"}], + "body": "d" * 60, "state": "open"} + prs = [{"number": 9, "title": "fix ferro velho", "url": "u9", "body": "fixes #3"}] + out = sweep({"prs": prs, "issues": [crash_a, crash_b, coberta, muda]}) + por_issue = {a["issue_number"]: a for a in out["actions"]} + casos = [ + ("issue coberta gera ação", 3 in por_issue), + ("issue coberta comenta (PR existe)", por_issue.get(3, {}).get("should_comment") is True), + ("issue sensível sem PR não gera ação", 4 not in por_issue), + ("crash com gêmeo detecta duplicata", "> [!TIP]" in por_issue.get(1, {}).get("comment", "") or "> [!TIP]" in por_issue.get(2, {}).get("comment", "")), + ("comentário reusa o formato da review", "csbrasil-bot issue review" in por_issue.get(3, {}).get("comment", "")), + ] + erros = 0 + for nome, ok in casos: + print(f" {'ok ' if ok else 'FALHOU'} {nome}") + erros += 0 if ok else 1 + return 0 if not erros else 1 + + +def main() -> int: + if "--selftest" in sys.argv: + return selftest() + print(json.dumps(sweep(json.load(sys.stdin)))) return 0 diff --git a/scripts/ci/pr_comment.py b/scripts/ci/pr_comment.py index ce5d0bbcd..d5810f3b7 100644 --- a/scripts/ci/pr_comment.py +++ b/scripts/ci/pr_comment.py @@ -1,10 +1,49 @@ #!/usr/bin/env python3 +"""Corpo do comentário de classificação de PR. + +O heading `## csbrasil-bot classification` é MARCADOR de dedupe do workflow +(csbrasil-bot-pr-classify.yml): mudou aqui, mudou lá — senão cada gatilho posta +um comentário novo em vez de atualizar o existente (BUG-68). +""" import json import sys +# Razão de cada label em português de gente — a label sozinha não diz nada +# pra quem chegou agora; a frase sim. +LABEL_REASONS = { + "target:main": "vai direto pra `main`, sem fila de integração", + "target:staging": "encaminhada pra branch de integração `staging`", + "target:release": "encaminhada pra branch de release", + "needs-staging": "mudança de runtime/UI — valida integrado no staging antes do merge", + "needs-human-gameplay": "toca gameplay/render/HUD/mapa/personagem — decisão de humano", + "needs-human-backend": "toca API/Supabase/anti-cheat/ranking — decisão de humano", + "safe-automerge": "pequena e reversível, fora de superfície sensível — elegível a merge automático", + "needs-coderabbit-resolution": "há apontamento de revisor automático pendente — não mergear sem `coderabbit-resolved`", + "crash-auto": "veio do report automático de crash em produção", +} -def main() -> int: - payload = json.load(sys.stdin) +AREA_RULES = [ + ("mapas", "🗺️", lambda p: p.startswith("public/js/map_")), + ("jogo", "🎮", lambda p: p.startswith("public/js/")), + ("backend", "⚙️", lambda p: p.startswith("src/pages/api") or p.startswith("src/lib")), + ("site", "🌐", lambda p: p.startswith("src/")), + ("CI", "🤖", lambda p: p.startswith(".github") or p.startswith("scripts/ci")), + ("arnês", "🧪", lambda p: p.startswith("tools/")), + ("docs", "📚", lambda p: p.endswith(".md")), + ("assets", "🎨", lambda p: p.startswith(("public/models", "public/img", "public/audio"))), +] + +AREA_DEFAULT = ("outros", "📦") + + +def area_de(path: str) -> tuple[str, str]: + for nome, icone, casa in AREA_RULES: + if casa(path): + return nome, icone + return AREA_DEFAULT + + +def render(payload: dict) -> str: labels_add = payload.get("labels_add", []) files = payload.get("files", []) changed = payload.get("changedFiles", len(files)) @@ -13,35 +52,113 @@ def main() -> int: assignee = payload.get("add_assignee") retarget = payload.get("retarget_to") - lines = [ - "## csbrasil-bot classification", - "", - f"- arquivos alterados: `{changed}`", - f"- base efetiva: `{base_branch}`", - ] - if author: - lines.append(f"- autor: `@{author}`") - if assignee: - lines.append(f"- assignee aplicado: `@{assignee}`") + entradas = [] + for f in files: + path = f.get("path", "") + entradas.append({ + "path": path, + "add": int(f.get("additions") or 0), + "del": int(f.get("deletions") or 0), + }) + tot_add = sum(e["add"] for e in entradas) + tot_del = sum(e["del"] for e in entradas) + + # ── resumo em caixa colorida: a primeira coisa que o olho pega ───────── + quem = f"**@{author}** abriu" if author else "PR de autor desconhecido com" + tamanho = f"**{changed} arquivo(s)**" + if entradas: + tamanho += f" · **+{tot_add}** **−{tot_del}**" + resumo = [f"{quem} {tamanho}, base **{base_branch}**."] if retarget: - lines.append(f"- PR reencaminhada automaticamente para `{retarget}`") + resumo.append(f"Reencaminhada automaticamente para **{retarget}**.") + caixa = "\n".join(f"> {l}" for l in resumo) + + # ── mapa da mudança: área → quantos arquivos, com amostra ───────────── + linhas = [f"## csbrasil-bot classification", "", f"> [!NOTE]", caixa, ""] + if entradas: + por_area: dict[str, dict] = {} + for e in entradas: + nome, icone = area_de(e["path"]) + por_area.setdefault(nome, {"icone": icone, "itens": []})["itens"].append(e) + linhas += ["### O que mudou", "", + "| área | arquivos | + | − | amostra |", + "|:------|---------:|--:|--:|:--------|"] + for nome, d in sorted(por_area.items(), key=lambda kv: -len(kv[1]["itens"])): + itens = d["itens"] + amostra = ", ".join(f"`{i['path'].split('/')[-1]}`" for i in itens[:3]) + if len(itens) > 3: + amostra += " …" + a = sum(i["add"] for i in itens) + r = sum(i["del"] for i in itens) + linhas.append(f"| {d['icone']} {nome} | {len(itens)} | +{a} | −{r} | {amostra} |") + linhas.append("") + + # ── o que o bot fez de ação (só o que aconteceu de verdade) ─────────── + acoes = [] + if assignee: + acoes.append(f"- **assignee:** @{assignee} — responsável pela revisão") if labels_add: - lines.append(f"- labels sugeridas/aplicadas: `{', '.join(labels_add)}`") - else: - lines.append("- nenhuma label adicional sugerida") - - if "needs-human-gameplay" in labels_add: - lines.append("- motivo: toca gameplay/render/HUD/mapa/personagem") - if "needs-human-backend" in labels_add: - lines.append("- motivo: toca API/Supabase/anti-cheat/ranking") - if "needs-staging" in labels_add: - lines.append("- motivo: mudança de runtime/UI precisa validação integrada") - if "safe-automerge" in labels_add: - lines.append("- motivo: PR pequena e reversível fora de superfícies sensíveis") + linhas += ["### Labels", "", + "| label | por quê |", "|:------|:--------|"] + for lab in labels_add: + motivo = LABEL_REASONS.get(lab, "critério do roteador") + linhas.append(f"| `{lab}` | {motivo} |") + linhas.append("") + elif acoes or entradas: + linhas += ["### Labels", "", "nenhuma label adicional sugerida — PR fora dos critérios de roteamento.", ""] + if "needs-coderabbit-resolution" in labels_add: - lines.append("- bloqueio: há apontamento/check do CodeRabbit; não mergear sem marcar `coderabbit-resolved`") + linhas += ["> [!CAUTION]", + "> Há apontamento de revisor automático **pendente**. Não mergear sem aplicar `coderabbit-resolved`.", ""] + + if acoes: + linhas += ["### Ações do bot", "", *acoes, ""] + return "\n".join(linhas).rstrip() + "\n" + + +def selftest() -> int: + """Fixtures do formato: o marcador de dedupe e os números têm que bater. - print("\n".join(lines)) + O heading é contrato com o workflow (dedupe por marcador) e os totais + vêm de soma dupla (arquivo→área) — os dois já mentiram em texto plano. + """ + casos = [] + p = render({ + "files": [ + {"path": "public/js/game.js", "additions": 10, "deletions": 2}, + {"path": "public/js/map_favela.js", "additions": 5, "deletions": 0}, + {"path": "docs/README.md", "additions": 1, "deletions": 3}, + ], + "changedFiles": 3, "baseRefName": "main", "author_login": "ruben", + "add_assignee": "ruben", + "labels_add": ["needs-staging", "target:main"], + }) + casos += [ + ("heading-marcador-de-dedupe", "## csbrasil-bot classification" in p), + ("resumo soma totais", "+16" in p and "−5" in p), + ("mapa separa área de mapa", "🗺️ mapas | 1 | +5 | −0" in p), + ("mapa separa jogo", "🎮 jogo | 1 | +10 | −2" in p), + ("label com motivo humano", "valida integrado no staging" in p), + ("assignee listado", "**assignee:** @ruben" in p), + ("caixa NOTE presente", "> [!NOTE]" in p), + ] + q = render({"files": [], "changedFiles": 0, "baseRefName": "staging", + "labels_add": ["needs-coderabbit-resolution"]}) + casos += [ + ("sem arquivo não quebra", "0 arquivo(s)" in q), + ("bloqueio coderabbit em CAUTION", "> [!CAUTION]" in q), + ] + erros = 0 + for nome, ok in casos: + print(f" {'ok ' if ok else 'FALHOU'} {nome}") + erros += 0 if ok else 1 + return 0 if not erros else 1 + + +def main() -> int: + if "--selftest" in sys.argv: + return selftest() + print(render(json.load(sys.stdin)), end="") return 0 diff --git a/scripts/ci/workflow_security_check.py b/scripts/ci/workflow_security_check.py index 1740ca9f9..53baec243 100644 --- a/scripts/ci/workflow_security_check.py +++ b/scripts/ci/workflow_security_check.py @@ -9,6 +9,16 @@ def preview_failures(source: str) -> list[str]: + """Contrato do preview de fork POR APROVAÇÃO (desenho antigo). + + Vale enquanto o `preview-bot.yml` tiver o job que publica: ele roda `vercel build`, + isto é, executa o build DO FORK com o token no ambiente, e por isso depende de um + mantenedor revisar o SHA. Quando o preview passou a ser build-sem-segredo + + deploy-sem-código (ver `separacao_failures`), este job deixou de ser necessário — + mas enquanto existir, ele tem de manter TODAS as travas. + """ + if 'vercel deploy' not in source: + return [] # o job de publicação saiu do arquivo: nada a guardar aqui errors = [] required = { 'types: [opened, synchronize, reopened, labeled]': 'evento labeled ausente', @@ -27,13 +37,84 @@ def preview_failures(source: str) -> list[str]: for marker, message in required.items(): if marker not in source: errors.append(message) - if '--add-label "preview-autorizado"' in source: + if '--add-label' in source and 'preview-autorizado' in source: errors.append('workflow autoaprova código de fork') if 'preview_autorizado=true' in source: errors.append('workflow decide autorização sem mantenedor') return errors +def separacao_failures(build: str, deploy: str) -> list[str]: + """Contrato do preview de fork POR SEPARAÇÃO (desenho de 22/08). + + A aprovação manual existia porque UM job fazia as duas coisas: rodava o build do + fork E tinha o token. Separando, os dois lados ficam seguros sozinhos e ninguém + precisa clicar: + + PRV1 quem COMPILA roda código do fork e não recebe segredo — `pull_request` (não + `pull_request_target`), e nenhuma referência a `secrets.` no arquivo; + PRV2 quem PUBLICA roda no contexto base (`workflow_run`), que é o que lhe dá o + segredo mesmo vindo de fork; + PRV4 quem PUBLICA não interpola `${{ }}` dentro de `run:`. A expressão é substituída + no TEXTO do script antes do shell existir, então valor com aspas ou `$(...)` vira + comando — e no caminho de deploy os valores vêm de fora (o número do PR atravessa + um artefato escrito por job que rodou código do fork, a URL é saída de comando). + O CodeQL pegou exatamente isso aqui, como injeção crítica, antes do merge. + PRV3 quem PUBLICA não executa NADA do PR: sem checkout da branch do fork, sem + `npm ci`, sem `npm run`, e o deploy é `--prebuilt` (só envia arquivo). + Furar isto devolve o token para as mãos de quem abriu o PR. + """ + errors = [] + if not build or not deploy: + return ['preview por separação incompleto: falta preview-build.yml ou preview-deploy.yml'] + + # Só a INSTRUÇÃO conta. Os dois arquivos explicam em comentário o que NÃO fazem + # ("não roda npm ci", "não usa pull_request_target") e ler o comentário como se + # fosse código acusaria justamente quem documentou a trava. + def codigo(texto: str) -> str: + return '\n'.join(l for l in texto.splitlines() if not l.lstrip().startswith('#')) + + build, deploy = codigo(build), codigo(deploy) + + if 'pull_request_target' in build: + errors.append('PRV1 o job que COMPILA usa pull_request_target — passaria a enxergar segredo rodando código do fork') + if 'pull_request:' not in build: + errors.append('PRV1 o job que COMPILA não roda em pull_request') + if 'secrets.' in build: + errors.append('PRV1 o job que COMPILA referencia `secrets.` — ele roda código do fork e não pode ter o que roubar') + + if 'workflow_run:' not in deploy: + errors.append('PRV2 o job que PUBLICA não roda em workflow_run — sem contexto base não há segredo em PR de fork') + if 'secrets.VERCEL_TOKEN' not in deploy: + errors.append('PRV2 o job que PUBLICA não usa o token da Vercel') + + # PRV4: `${{ }}` só pode aparecer em `env:`/`with:`/`if:`, nunca dentro do script. + dentro_de_run = False + for linha in deploy.splitlines(): + despido = linha.strip() + if re.match(r'run:\s*\|', despido): + dentro_de_run = True + continue + if dentro_de_run: + # o bloco acaba quando a indentação volta para o nível da chave do passo + if despido and not linha.startswith(' '): + dentro_de_run = False + elif '${{' in linha: + errors.append(f'PRV4 `{despido[:60]}` interpola expressão dentro de run: — passe por env:') + + if '--prebuilt' not in deploy: + errors.append('PRV3 o deploy não é --prebuilt: estaria construindo, e construir é executar código do PR') + for proibido, motivo in ( + ('actions/checkout', 'faz checkout — traria código do PR para o job que tem o token'), + ('npm ci', 'roda npm ci — executaria script do PR'), + ('npm run', 'roda npm run — executaria script do PR'), + ('vercel build', 'roda vercel build — é o build do fork com o token no ambiente'), + ): + if proibido in deploy: + errors.append(f'PRV3 o job que PUBLICA {motivo}') + return errors + + def supply_failures(workflows: dict[Path, str]) -> list[str]: errors = [] for path, source in workflows.items(): @@ -54,7 +135,26 @@ def read_workflows(root: Path = Path('.github/workflows')) -> dict[Path, str]: } +BUILD = Path('.github/workflows/preview-build.yml') +DEPLOY = Path('.github/workflows/preview-deploy.yml') + + +def _ler(p: Path) -> str: + return p.read_text(encoding='utf-8') if p.exists() else '' + + def selftest(source: str) -> list[str]: + build, deploy = _ler(BUILD), _ler(DEPLOY) + separacao = { + 'compila-com-segredo': (build + '\n env:\n X: ${{ secrets.VERCEL_TOKEN }}\n', deploy), + 'compila-com-target': (build.replace(' pull_request:', ' pull_request_target:'), deploy), + 'publica-sem-run': (build, deploy.replace(' workflow_run:', ' schedule:')), + 'publica-faz-checkout': (build, deploy + '\n - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683\n'), + 'publica-constroi': (build, deploy.replace('--prebuilt', '')), + 'publica-interpola': (build, deploy.replace('gh pr comment "$PR_NUM"', 'gh pr comment "${{ steps.pr.outputs.numero }}"')), + } + missed_sep = [n for n, (b, d) in separacao.items() if not separacao_failures(b, d)] + mutations = { 'auto-label': source + '\n# --add-label "preview-autorizado"\n', 'sem-ator': source.replace('repos/$REPO/collaborators/$ACTOR/permission', 'repos/$REPO'), @@ -65,10 +165,22 @@ def selftest(source: str) -> list[str]: 'action-mutável': re.sub(r'actions/checkout@[0-9a-f]{40}', 'actions/checkout@v4', source), 'cli-mutável': re.sub(r'vercel@\d+\.\d+\.\d+', 'vercel@latest', source), } + # As mutações acima descrevem o contrato ANTIGO (preview por aprovação). Quando o + # job que publicava sai do arquivo, elas deixam de ter alvo — testá-las ali seria + # exigir mordida de uma régua que não tem mais o que guardar. O contrato novo é + # medido pelas mutações de `separacao` logo acima. + antigo_vivo = 'vercel deploy' in source missed = [ name for name, mutated in mutations.items() - if not (preview_failures(mutated) + supply_failures({WORKFLOW: mutated})) + if antigo_vivo and not (preview_failures(mutated) + supply_failures({WORKFLOW: mutated})) ] + # Fornecimento (action e CLI presas) vale sempre, e o alvo passou a ser os dois + # arquivos novos: um tem as actions, o outro tem a CLI da Vercel. + fornecimento = { + 'action-mutável': (BUILD, re.sub(r'actions/checkout@[0-9a-f]{40}', 'actions/checkout@v4', build)), + 'cli-mutável': (DEPLOY, re.sub(r'vercel@\d+\.\d+\.\d+', 'vercel@latest', deploy)), + } + missed += [n for n, (alvo, mutado) in fornecimento.items() if not supply_failures({alvo: mutado})] with tempfile.TemporaryDirectory() as tmp: mutant = Path(tmp) / 'mutable-action.yaml' mutant.write_text('steps:\\n - uses: actions/checkout@v4\\n', encoding='utf-8') @@ -83,12 +195,14 @@ def main() -> int: args = parser.parse_args() source = WORKFLOW.read_text(encoding='utf-8') workflows = read_workflows() - errors = preview_failures(source) + supply_failures(workflows) + errors = (preview_failures(source) + + separacao_failures(_ler(BUILD), _ler(DEPLOY)) + + supply_failures(workflows)) if errors: for error in errors: print(f'WFS FAIL: {error}') return 1 - print('WFS PASS: preview de fork exige aprovação manual presa ao SHA') + print('WFS PASS: quem compila código de fork não tem segredo; quem tem segredo não executa código de fork') if args.selftest: missed = selftest(source) if missed: diff --git a/scripts/fetch-audio.sh b/scripts/fetch-audio.sh index 58d27b672..3dd9e6b25 100755 --- a/scripts/fetch-audio.sh +++ b/scripts/fetch-audio.sh @@ -18,7 +18,7 @@ if [ -f "$DEST/manifest.json" ]; then fi mkdir -p "$DEST" echo "Baixando pacote de áudio de: $URL" -curl -fsSL "$URL" -o /tmp/csbrasil-audio.zip +curl --retry 5 --retry-delay 3 --retry-all-errors --connect-timeout 20 -fsSL "$URL" -o /tmp/csbrasil-audio.zip unzip -o -q /tmp/csbrasil-audio.zip -d "$DEST/" [ -f "$DEST/manifest.json" ] || cp "$DEST/manifest.example.json" "$DEST/manifest.json" echo "Pronto. Áudio instalado em $DEST/." diff --git a/scripts/fetch-decals.sh b/scripts/fetch-decals.sh index b868fdbe9..dcafbcf85 100755 --- a/scripts/fetch-decals.sh +++ b/scripts/fetch-decals.sh @@ -12,7 +12,7 @@ cd "$(dirname "$0")/.." # e com eles sumiam 513 das 4.671 peças de grafite dos 5 mapas (30% da Quebrada). Ninguém # tinha como ver isso até o `npm run assert:assets` do T1, que agora reprova o build se o # pacote vier atrás do que o jogo pede. -URL="${DECALS_PACK_URL:-https://github.com/rubenmarcus/csbrasil/releases/download/decals-pack-v2/decals-pack.zip}" +URL="${DECALS_PACK_URL:-https://github.com/corosolto/client/releases/download/decals-pack-v2/decals-pack.zip}" DEST="public/img/decals" # Sentinela conta só o ACERVO baixável (exclui os `or-*.png`, que são obra própria @@ -24,6 +24,6 @@ if [ -d "$DEST" ] && [ "$(ls -1 "$DEST"/*.png 2>/dev/null | grep -cv '/or-')" -g fi mkdir -p "$DEST" echo "Baixando decalques de: $URL" -curl -fsSL "$URL" -o /tmp/csbrasil-decals.zip +curl --retry 5 --retry-delay 3 --retry-all-errors --connect-timeout 20 -fsSL "$URL" -o /tmp/csbrasil-decals.zip unzip -o -q /tmp/csbrasil-decals.zip -d "$DEST/" echo "Pronto. $(ls -1 "$DEST"/*.png | wc -l | tr -d ' ') decalques em $DEST/." diff --git a/scripts/setup.mjs b/scripts/setup.mjs index 3dddb1738..231b364e9 100644 --- a/scripts/setup.mjs +++ b/scripts/setup.mjs @@ -89,7 +89,7 @@ const ruins = resultado.filter((r) => !r.bom); console.log(''); if (!ruins.length && chk.status === 0) { console.log(`${VERDE}Pronto.${OFF} \`npm run dev\` e abra http://localhost:4321\n`); - console.log(` ${AMAR}opcional${OFF} \`npm run check\` roda o portão (~5 min, precisa de nada além disto)`); + console.log(` ${AMAR}opcional${OFF} \`npm run check:fast\` roda o portão (~5 min, precisa de nada além disto)`); console.log(` ${AMAR}opcional${OFF} \`FEEDBACK_TO=voce@exemplo.com\` liga a notificação do formulário`); console.log(` (o formsubmit.co exige ATIVAR o endereço: ele manda um link de`); console.log(` confirmação no primeiro envio, e até você clicar nada chega)`); diff --git a/scripts/sync-changelog.mjs b/scripts/sync-changelog.mjs index 8e511a56e..1c2dfc44a 100644 --- a/scripts/sync-changelog.mjs +++ b/scripts/sync-changelog.mjs @@ -1,21 +1,133 @@ #!/usr/bin/env node +/* Notas de release CONSOLIDADAS: a entrada de cada versão agrega TODOS os PRs + mergeados desde a última tag (decisão do dono, 16/08: consolidar as NOTAS, não a + cadência de versionamento). Extração 100% git — o subject do merge commit traz o + #N e o corpo traz o título do PR, então o roteiro não precisa de rede nem de gh. + O link "Notas completas do release" morreu aqui: a seção do CHANGELOG É as notas + do release — linkar o release nelas mesmas era um ponteiro que girava em círculo + (e ainda gravava o domínio pré-migração, rubenmarcus/csbrasil). */ import { execFileSync } from 'node:child_process'; import { readFileSync, writeFileSync } from 'node:fs'; const version = JSON.parse(readFileSync('package.json', 'utf8')).version; const path = 'CHANGELOG.md'; const current = readFileSync(path, 'utf8'); + +const git = (args) => execFileSync('git', args, { encoding: 'utf8' }); + +function tagExists(tag) { + try { git(['rev-parse', '--verify', '--quiet', `${tag}^{}`]); return true; } catch { return false; } +} + +function lastTag(before) { + try { + return git(['describe', '--tags', '--abbrev=0', ...(before ? [before] : [])]).trim(); + } catch { return null; } +} + +/* PRs mergeados no intervalo. Fallback sem tag: últimos 3 merges (primeira execução + num clone raso ainda lista algo em vez de estourar). + DUAS formas de PR chegar à main: merge commit ("Merge pull request #N", título no + corpo) ou squash merge (commit comum com "(#N)" no fim do subject — o padrão do + automerge). A régua contava só a primeira: com o #344 squash-merged, o check acusava + "1 na seção, 0 no git" e o modo escrita nem listaria o PR. */ +function mergedPRs(fromRef, toRef) { + const range = fromRef ? [`${fromRef}..${toRef || 'HEAD'}`] : ['-3']; + const out = git(['log', '--format=%H\u0001%s\u0001%b\u0002', ...range]); + const prs = []; + for (const chunk of out.split('\u0002')) { + const [, subject, body] = chunk.split('\u0001'); + if (!subject) continue; + let n = null, title = null; + const mm = subject.match(/^Merge pull request #(\d+)/); + const ms = subject.match(/\s\(#(\d+)\)\s*$/); + if (mm) { + n = mm[1]; + title = (body || '').split('\n').map((l) => l.trim()).filter(Boolean) + .find((l) => !/^Signed-off-by:/i.test(l)) + || subject.replace(/^Merge pull request #\d+ from \S+\s*/, ''); + } else if (ms) { + n = ms[1]; + title = subject.slice(0, ms.index); + } else continue; + if (/^chore\(release\)/.test(title)) continue; + prs.push({ n, title: title.replace(/[<>]/g, '').replace(/\s+/g, ' ') }); + } + return prs.reverse(); +} + +/* Commits diretos (sem PR) no intervalo — raro, mas não pode sumir do registro. + Commit com "(#N)" no subject É PR (squash) e já entrou por mergedPRs. */ +function directCommits(fromRef, toRef) { + if (!fromRef) return []; + const out = git(['log', '--no-merges', '--format=%s', `${fromRef}..${toRef || 'HEAD'}`]); + return out.split('\n').map((l) => l.trim()).filter(Boolean) + .filter((s) => !/^chore\(release\)/.test(s)) + .filter((s) => !/\s\(#\d+\)\s*$/.test(s)) + .map((s) => ({ n: null, title: s.replace(/[<>]/g, '') })); +} + +function buildSection(prs, directs) { + const date = new Date().toISOString().slice(0, 10); + const lines = [...prs, ...directs].map((p) => `- ${p.title}${p.n ? ` (#${p.n})` : ''}`); + if (!lines.length) lines.push(`- Publicação ${version}`); + return `\n## [${version}] — ${date}\n\n### Mudado\n${lines.join('\n')}\n`; +} + +function latestSection(text) { + const start = text.indexOf(`\n## [${version}]`); + if (start < 0) return null; + const rest = text.slice(start + 1); + const next = rest.indexOf('\n## ['); + return { start: start + 1, body: next < 0 ? rest : rest.slice(0, next) }; +} + +/* ── modo --check: a régua do CHANGELOG (teto com mutação, ver //changelog:check). + Valida a seção da versão CORRENTE: sem link auto-referente, sem domínio + pré-migração e — quando as duas tags existem localmente — a contagem de PRs + bate com a extraída do git. Clone sem tags (Vercel): pula SÓ a contagem, + declara o pulo; os cheques estruturais mordem em qualquer clone. */ +if (process.argv.includes('--check')) { + const mutante = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1]; + const sec = latestSection(current); + if (!sec) { console.error(`✗ CHANGELOG sem seção [${version}]`); process.exit(1); } + + let body = sec.body; + if (mutante === 'selflink') body = body.replace('\n', '\n- [Notas completas do release](https://github.com/corosolto/client/releases/tag/v' + version + ').\n'); + if (mutante === 'dominio-velho') body = body.replace('\n', '\n- ver https://github.com/rubenmarcus/csbrasil\n'); + + const fails = []; + if (/Notas completas do release/.test(body)) fails.push('link auto-referente (a seção É as notas)'); + if (/rubenmarcus\/csbrasil/.test(body)) fails.push('domínio pré-migração (rubenmarcus/csbrasil)'); + + const tag = `v${version}`; + if (tagExists(tag)) { + const prev = lastTag(`${tag}^`); + if (prev) { + const expected = mergedPRs(prev, tag).length; + const got = (body.match(/ \(#\d+\)/g) || []).length; + if (mutante === 'pr-sumido' && got > 0) fails.push(`contagem de PRs: ${got - 1} na seção, ${expected} no git (mutante)`); + else if (got !== expected) fails.push(`contagem de PRs: ${got} na seção, ${expected} no git (${prev}..${tag})`); + } else { + console.log(' (sem tag anterior local — checagem de contagem pulada, estrutura valendo)'); + } + } else { + console.log(' (sem tags locais — checagem de contagem pulada, estrutura valendo)'); + } + + if (fails.length) { + console.error(`✗ CHANGELOGCHECK ${fails.length} vermelha(s):\n - ${fails.join('\n - ')}`); + process.exit(1); + } + console.log(`✓ CHANGELOGCHECK verde: seção [${version}] sem link circular, domínio canônico, PRs conferidos`); + process.exit(0); +} + +/* ── modo escrita (release.yml chama assim) */ if (current.includes(`## [${version}]`)) process.exit(0); -const body = execFileSync('git', ['log', '-1', '--format=%B'], { encoding: 'utf8' }); -const lines = body.split('\n').map(line => line.trim()).filter(Boolean); -const summary = lines.find(line => !/^Merge pull request /i.test(line) && !/^Signed-off-by:/i.test(line)) - || lines[0] - || `Publicação ${version}`; -const safeSummary = summary.replace(/[<>]/g, '').replace(/\s+/g, ' '); -const date = new Date().toISOString().slice(0, 10); +const from = lastTag(); +const section = buildSection(mergedPRs(from), directCommits(from)); const first = current.indexOf('\n## ['); if (first < 0) throw new Error('CHANGELOG.md não contém nenhuma seção de versão'); - -const section = `\n## [${version}] — ${date}\n\n### Mudado\n- ${safeSummary}\n- [Notas completas do release](https://github.com/rubenmarcus/csbrasil/releases/tag/v${version}).\n`; writeFileSync(path, current.slice(0, first) + section + current.slice(first)); diff --git a/tools/eval/ARCH.md b/tools/eval/ARCH.md index 6b2534269..e8d932ec1 100644 --- a/tools/eval/ARCH.md +++ b/tools/eval/ARCH.md @@ -3,41 +3,41 @@ > Gerado por `node tools/gen-arch.mjs`. **Não edite este bloco à mão.** -> Versão do jogo: 2.0.0-alpha.136 · `npm run arch` para regenerar · `npm run arch:check` no CI. +> Versão do jogo: 2.0.0-alpha.176 · `npm run arch` para regenerar · `npm run arch:check` no CI. ## Tamanho dos arquivos indexados | Arquivo | Linhas | Símbolos | |---|---:|---:| -| `public/js/game.js` | 6561 | 230 | -| `public/js/main.js` | 2513 | 230 | -| `public/js/glbchars.js` | 838 | 60 | +| `public/js/game.js` | 6911 | 251 | +| `public/js/main.js` | 2699 | 252 | +| `public/js/glbchars.js` | 845 | 60 | | `public/js/characters.js` | 1069 | 40 | | `public/js/vmattach.js` | 629 | 4 | | `public/js/springs.js` | 261 | 28 | -| `public/js/weapons.js` | 345 | 20 | +| `public/js/weapons.js` | 347 | 20 | ## Maiores métodos de `game.js` — onde o conflito mora -Os 15 maiores somam **3042 linhas (46% do arquivo)**. Método grande = PR irrevisável e merge conflitante. +Os 15 maiores somam **3105 linhas (45% do arquivo)**. Método grande = PR irrevisável e merge conflitante. | Linhas | Início | Método | | |---:|---:|---|---| -| 809 | 5223 | `_updateBot()` | ⚠️ candidato a extração | -| 546 | 574 | `constructor()` | 🔴 append-only | -| 310 | 4461 | `_updatePlayer()` | ⚠️ candidato a extração | -| 248 | 1888 | `_resetPositions()` | | -| 241 | 1217 | `_buildViewModels()` | | -| 148 | 4771 | `_updatePickups()` | | -| 133 | 4071 | `_botCtf()` | | -| 84 | 3805 | `_initCTF()` | | -| 83 | 2670 | `_tryShoot()` | | -| 80 | 6375 | `_updateHud()` | | -| 79 | 3006 | `_dmgArc()` | | -| 76 | 4210 | `_updateCtfHud()` | | -| 71 | 6045 | `_updateBotNN()` | | -| 70 | 6455 | `update()` | 🔴 append-only | -| 64 | 2942 | `_kill()` | | +| 812 | 5562 | `_updateBot()` | ⚠️ candidato a extração | +| 542 | 586 | `constructor()` | 🔴 append-only | +| 315 | 4797 | `_updatePlayer()` | ⚠️ candidato a extração | +| 248 | 2159 | `_resetPositions()` | | +| 241 | 1225 | `_buildViewModels()` | | +| 148 | 5112 | `_updatePickups()` | | +| 133 | 4374 | `_botCtf()` | | +| 115 | 1903 | `_touchControls()` | | +| 84 | 4108 | `_initCTF()` | | +| 83 | 2947 | `_tryShoot()` | | +| 80 | 6718 | `_updateHud()` | | +| 79 | 3308 | `_dmgArc()` | | +| 76 | 4513 | `_updateCtfHud()` | | +| 76 | 6798 | `update()` | 🔴 append-only | +| 73 | 3235 | `_kill()` | | ## Tabela de CONFLITO — resolvida para as linhas de hoje @@ -47,221 +47,238 @@ faixas disjuntas simultaneamente com zero conflito de conteúdo. | Frente | Faixas em `game.js` | Arquivos exclusivos | |---|---|---| -| **ARMAS / VIEWMODEL** | `25–73` `335–335` `363–457` `484–505` `1217–1615` `2435–2440` `2522–2605` `2624–2817` `3227–3250` `3298–3360` `3426–3442` | `public/js/vmattach.js` `public/js/springs.js` `public/js/weapons.js` `public/js/fparms.js` `public/js/handik.js` `public/js/recoil.js` `public/js/vmlab.js` | -| **BOTS / JOGABILIDADE** | `189–192` `243–243` `269–280` `547–558` `2903–3005` `3749–3804` `3967–4203` `4286–4308` `4461–4770` `5095–5112` `5194–6031` | — | -| **MAPAS / MUNDO** | `1163–1216` `1888–2135` `3805–3944` `4771–4918` | `public/js/maps.js` `public/js/mapprops.js` `public/js/map_brasilia.js` `public/js/map_havan.js` `public/js/map_piscina.js` `public/js/map_piscinao_ramos.js` `public/js/map_ferrovelho.js` | -| **GRÁFICOS / FX** | `1616–1658` `2368–2380` `3251–3289` `3371–3425` | `public/js/bloom.js` `public/js/textures.js` `public/js/vao.js` `public/js/stylize.js` `public/js/gpuparticles.js` | -| **UI / HUD / MENU** | `1120–1162` `2328–2346` `2362–2367` `2381–2387` `3006–3147` `3163–3226` `6198–6261` `6292–6339` `6375–6454` | `public/js/main.js` `public/style.css` `src/pages/index.astro` | +| **ARMAS / VIEWMODEL** | `304–306` `335–429` `456–477` `1225–1623` `2712–2717` `2799–2882` `2901–3094` `3529–3552` `3600–3662` `3729–3745` | `public/js/vmattach.js` `public/js/springs.js` `public/js/weapons.js` `public/js/fparms.js` `public/js/handik.js` `public/js/recoil.js` `public/js/vmlab.js` | +| **BOTS / JOGABILIDADE** | `158–161` `212–212` `238–249` `519–530` `3196–3307` `4052–4107` `4270–4506` `4589–4611` `4797–5111` `5434–5451` `5533–6373` | — | +| **MAPAS / MUNDO** | `1171–1224` `2159–2406` `4108–4247` `5112–5259` | `public/js/maps.js` `public/js/mapprops.js` `public/js/map_brasilia.js` `public/js/map_havan.js` `public/js/map_piscina.js` `public/js/map_piscinao_ramos.js` `public/js/map_ferrovelho.js` | +| **GRÁFICOS / FX** | `1624–1633` `1748–1779` `2642–2654` `3553–3591` `3673–3728` | `public/js/bloom.js` `public/js/textures.js` `public/js/vao.js` `public/js/stylize.js` `public/js/gpuparticles.js` | +| **UI / HUD / MENU** | `1128–1170` `2599–2620` `2636–2641` `2655–2661` `3308–3449` `3465–3528` `6541–6604` `6635–6682` `6718–6797` | `public/js/main.js` `public/style.css` `src/pages/index.astro` | | **ÁUDIO** | — | `public/js/audio.js` | | **PERSONAGENS** | — | `public/js/characters.js` `public/js/glbchars.js` | | **SITE / BACKEND** | — | `src/` `supabase/` | -**🔴 Zonas vermelhas (append-only, qualquer frente pode precisar):** `update()` 6455–6524 · `_dom()` 1120–1162 · `constructor()` 574–1119 +**🔴 Zonas vermelhas (append-only, qualquer frente pode precisar):** `update()` 6798–6873 · `_dom()` 1128–1170 · `constructor()` 586–1127 Nenhuma sobreposição entre frentes — todas as faixas são disjuntas. ✓ -Cobertura: **3755 de 6561 linhas (57%)** do `game.js` têm dono declarado. O resto é território neutro — declare a frente mesmo assim. +Cobertura: **3728 de 6911 linhas (54%)** do `game.js` têm dono declarado. O resto é território neutro — declare a frente mesmo assim.
Índice completo de game.js (todos os símbolos) | Linha | Símbolo | Linhas | |---:|---|---:| -| 25 | `WEAPONS` | 49 | -| 76 | `VMLAB` | 8 | -| 84 | `VM_MAT_LEGACY` | 4 | -| 90 | `DROP_TTL` | 8 | -| 98 | `ROUNDS_MAX` | 27 | -| 128 | `CTF_CLOCK_SHOW` | 4 | -| 132 | `KILLS_PER_PLAYER` | 7 | -| 139 | `PACE` | 33 | -| 172 | `PAUSE_ARM_MS` | 9 | -| 182 | `confirmGate` | 7 | -| 193 | `BOT_AIM_PITCH` | 4 | -| 197 | `BOT_DMG_PLAYER` | 21 | -| 218 | `BOT_FAIR` | 5 | -| 223 | `BOT_MOVE2` | 15 | -| 247 | `BOT_FOCUS_MIN` | 22 | -| 273 | `BOT_TOKEN_REST` | 7 | -| 281 | `MOVE_MUL` | 6 | -| 288 | `MOVE2` | 5 | -| 293 | `RACK_OLD` | 4 | -| 297 | `RACK_RETA` | 25 | -| 324 | `RADIO` | 5 | -| 330 | `MK_LABELS` | 5 | -| 336 | `D2R` | 4 | -| 340 | `DMG_FALLOFF` | 5 | -| 345 | `HS_MUL` | 3 | -| 348 | `BALL_CLASS` | 15 | -| 363 | `STATIC_CLASS` | 75 | -| 439 | `VM_KNOB` | 19 | -| 460 | `vmFovForAspect` | 24 | -| 484 | `VM_OFF` | 22 | -| 506 | `vmOffY` | 35 | -| 541 | `VMP` | 6 | -| 547 | `BOT_SKILLS` | 11 | -| 559 | `diffKey` | 4 | -| 564 | `rollBotSkill` | 7 | -| 574 | `constructor()` | 546 | -| 1120 | `_dom()` | 43 | -| 1163 | `_buildEnv()` | 54 | -| 1217 | `_buildViewModels()` | 241 | -| 1458 | `_vmFrame` | 158 | -| 1616 | `_makePuffTexture()` | 11 | -| 1627 | `_makeFlashTex()` | 22 | -| 1649 | `_makeFlashCoreTex()` | 10 | -| 1659 | `_input()` | 2 | -| 1661 | `_kd` | 39 | -| 1700 | `_ku` | 4 | -| 1704 | `_md` | 34 | -| 1738 | `_mu` | 7 | -| 1745 | `_mm` | 15 | -| 1760 | `_cc` | 1 | -| 1761 | `_blur` | 1 | -| 1762 | `_plc` | 14 | -| 1776 | `_requestLock()` | 23 | -| 1799 | `_travaAtalhos()` | 4 | -| 1803 | `_soltaAtalhos()` | 3 | -| 1806 | `_acceptInput()` | 8 | -| 1814 | `_pauseBackdrop()` | 7 | -| 1821 | `_radioShow()` | 6 | -| 1827 | `_radioUi()` | 8 | -| 1835 | `_radioPick()` | 14 | -| 1849 | `start()` | 4 | -| 1853 | `_startRound()` | 35 | -| 1888 | `_resetPositions()` | 248 | -| 2136 | `_checkCtfAlvo()` | 13 | -| 2149 | `_checkPace()` | 13 | -| 2162 | `_endRound()` | 37 | -| 2199 | `_fimDaPartida()` | 7 | -| 2206 | `_endMatch()` | 58 | -| 2264 | `_ensureDolly()` | 41 | -| 2305 | `_tickDolly()` | 23 | -| 2328 | `setPaused()` | 19 | -| 2347 | `_now()` | 3 | -| 2350 | `pauseArmed()` | 1 | -| 2351 | `_syncPauseArm()` | 7 | -| 2358 | `resume()` | 4 | -| 2362 | `applySettings()` | 6 | -| 2368 | `_applyQuality()` | 13 | -| 2381 | `onResize()` | 7 | -| 2388 | `_switchTeam()` | 47 | -| 2435 | `_applyVmVisibility()` | 6 | -| 2441 | `_vmlabEnsure()` | 14 | -| 2455 | `_vmlabFrame()` | 28 | -| 2483 | `_tuneGet()` | 15 | -| 2498 | `_tune()` | 23 | -| 2521 | `_fxSet()` | 1 | -| 2522 | `_switchWeapon()` | 32 | -| 2554 | `_deploySfx()` | 7 | -| 2561 | `_scope()` | 17 | -| 2578 | `_zoomFov()` | 8 | -| 2586 | `_reloading()` | 1 | -| 2587 | `_startReload()` | 19 | -| 2606 | `_reloadLayers()` | 18 | -| 2624 | `_installRecoil()` | 33 | -| 2657 | `_shotRecoil()` | 13 | -| 2670 | `_tryShoot()` | 83 | -| 2753 | `_meleeHit()` | 12 | -| 2765 | `_fireHitscan()` | 53 | -| 2818 | `_surfaceOf()` | 27 | -| 2845 | `_fleshImpact()` | 19 | -| 2864 | `_fxVoice()` | 9 | -| 2873 | `_impactSfx()` | 14 | -| 2887 | `_tintFx()` | 16 | -| 2903 | `_damage()` | 39 | -| 2942 | `_kill()` | 64 | -| 3006 | `_dmgArc()` | 79 | -| 3085 | `_mkBanner()` | 9 | -| 3094 | `_hitmarker()` | 15 | -| 3109 | `_dmgNumber()` | 20 | -| 3129 | `_feed()` | 19 | -| 3148 | `_skullIcon()` | 6 | -| 3154 | `_killfeedWeaponIcon()` | 9 | -| 3163 | `_wpnIcon()` | 64 | -| 3227 | `_tracer()` | 24 | -| 3251 | `_puff()` | 39 | -| 3290 | `_holeDecalMat()` | 8 | -| 3298 | `_flash()` | 54 | -| 3352 | `_muzzleWorld()` | 9 | -| 3361 | `_updateDoors()` | 10 | -| 3371 | `_updateFx()` | 55 | -| 3426 | `_ejectCasing()` | 17 | -| 3443 | `_makeCtfFlagTex()` | 23 | -| 3466 | `_paintFlagSymbol()` | 9 | -| 3475 | `_flagTexFor()` | 26 | -| 3501 | `_legadoSimbolo()` | 8 | -| 3509 | `_loadCtfSymbols()` | 22 | -| 3531 | `_makeCtfZoneTex()` | 31 | -| 3562 | `_makeSmokeTex()` | 8 | -| 3570 | `_updateSmokeHud()` | 6 | -| 3576 | `_spawnGrenade()` | 11 | -| 3587 | `_throwSmoke()` | 8 | -| 3595 | `_throwFrag()` | 10 | -| 3605 | `_explodeFrag()` | 38 | -| 3643 | `_corDaFumaca()` | 15 | -| 3658 | `_popSmoke()` | 19 | -| 3677 | `_updateGrenades()` | 27 | -| 3704 | `_teamColor()` | 14 | -| 3718 | `_teamInk()` | 6 | -| 3724 | `_factionOf()` | 1 | -| 3725 | `_voiceKey()` | 1 | -| 3726 | `_teamName()` | 1 | -| 3727 | `_teamTag()` | 6 | -| 3733 | `_plaqueta()` | 13 | -| 3746 | `_mirror()` | 3 | -| 3749 | `_botSeparation()` | 56 | -| 3805 | `_initCTF()` | 84 | -| 3889 | `_updateCTF()` | 56 | -| 3945 | `_ctfWin()` | 22 | -| 3967 | `_freeYaw()` | 25 | -| 3992 | `_pullString()` | 23 | -| 4015 | `_walkReach()` | 18 | -| 4033 | `_wpComp()` | 16 | -| 4049 | `_findPathLocal()` | 22 | -| 4071 | `_botCtf()` | 133 | -| 4204 | `_hideCtfHud()` | 6 | -| 4210 | `_updateCtfHud()` | 76 | -| 4286 | `_collide()` | 23 | -| 4309 | `_collideRot()` | 26 | -| 4335 | `_freeSpot()` | 30 | -| 4365 | `_retaAndavel()` | 20 | -| 4385 | `_walkDepth()` | 16 | -| 4401 | `_noteHit()` | 17 | -| 4418 | `_deathFeedback()` | 43 | -| 4461 | `_updatePlayer()` | 310 | -| 4771 | `_updatePickups()` | 148 | -| 4919 | `_wpnMode()` | 5 | -| 4924 | `_botWeapon()` | 12 | -| 4936 | `_municaoInfinita()` | 1 | -| 4937 | `_pickupAllowed()` | 7 | -| 4944 | `_grabPickup()` | 35 | -| 4979 | `_assentarNoChao()` | 11 | -| 4990 | `_dropWeapon()` | 18 | -| 5008 | `_sumirDrop()` | 36 | -| 5044 | `_spawnY()` | 3 | -| 5047 | `_pickSpawn()` | 23 | -| 5070 | `_respawnPlayer()` | 25 | -| 5095 | `_losClear()` | 18 | -| 5113 | `_botCall()` | 37 | -| 5150 | `_teamMarkTex()` | 23 | -| 5173 | `_makeTeamMark()` | 14 | -| 5187 | `_updateTeamMark()` | 7 | -| 5194 | `_botEye()` | 1 | -| 5195 | `_enemyOf()` | 8 | -| 5203 | `_duelToken()` | 20 | -| 5223 | `_updateBot()` | 809 | -| 6032 | `_flushTraining()` | 13 | -| 6045 | `_updateBotNN()` | 71 | -| 6116 | `_botShootNN()` | 44 | -| 6160 | `_radarFoot()` | 38 | -| 6198 | `_updateRadar()` | 64 | -| 6262 | `_banner()` | 26 | -| 6288 | `_resultadoDaRodada()` | 4 | -| 6292 | `_showScoreboard()` | 48 | -| 6340 | `_updateWeaponHud()` | 35 | -| 6375 | `_updateHud()` | 80 | -| 6455 | `update()` | 70 | -| 6525 | `dispose()` | 36 | +| 36 | `REPLAY_CAM` | 3 | +| 45 | `VMLAB` | 8 | +| 53 | `VM_MAT_LEGACY` | 4 | +| 59 | `DROP_TTL` | 8 | +| 67 | `ROUNDS_MAX` | 27 | +| 97 | `CTF_CLOCK_SHOW` | 4 | +| 101 | `KILLS_PER_PLAYER` | 7 | +| 108 | `PACE` | 33 | +| 141 | `PAUSE_ARM_MS` | 9 | +| 151 | `confirmGate` | 7 | +| 162 | `BOT_AIM_PITCH` | 4 | +| 166 | `BOT_DMG_PLAYER` | 21 | +| 187 | `BOT_FAIR` | 5 | +| 192 | `BOT_MOVE2` | 15 | +| 216 | `BOT_FOCUS_MIN` | 22 | +| 242 | `BOT_TOKEN_REST` | 7 | +| 250 | `MOVE_MUL` | 6 | +| 257 | `MOVE2` | 5 | +| 262 | `RACK_OLD` | 4 | +| 266 | `RACK_RETA` | 25 | +| 293 | `RADIO` | 5 | +| 299 | `MK_LABELS` | 5 | +| 304 | `GUNFEEL` | 3 | +| 308 | `D2R` | 4 | +| 312 | `DMG_FALLOFF` | 5 | +| 317 | `HS_MUL` | 3 | +| 320 | `BALL_CLASS` | 15 | +| 335 | `STATIC_CLASS` | 75 | +| 411 | `VM_KNOB` | 19 | +| 432 | `vmFovForAspect` | 24 | +| 456 | `VM_OFF` | 22 | +| 478 | `vmOffY` | 35 | +| 513 | `VMP` | 6 | +| 519 | `BOT_SKILLS` | 11 | +| 531 | `diffKey` | 4 | +| 536 | `rollBotSkill` | 7 | +| 543 | `botTier` | 4 | +| 547 | `_cyclePool` | 4 | +| 551 | `_rosterPool` | 12 | +| 563 | `pickMatchRoster` | 10 | +| 573 | `BOT_WEAPON_POOL` | 5 | +| 578 | `pickMatchWeapons` | 7 | +| 586 | `constructor()` | 542 | +| 1128 | `_dom()` | 43 | +| 1171 | `_buildEnv()` | 54 | +| 1225 | `_buildViewModels()` | 241 | +| 1466 | `_vmFrame` | 158 | +| 1624 | `_makePuffTexture()` | 10 | +| 1634 | `_makeBloodTex()` | 19 | +| 1653 | `_makeBloodPoolTex()` | 21 | +| 1674 | `_bloodDecal()` | 16 | +| 1690 | `_makeBloodFx()` | 20 | +| 1710 | `_bloodSpatter()` | 18 | +| 1728 | `_bloodPoolAt()` | 6 | +| 1734 | `_updateBlood()` | 14 | +| 1748 | `_makeFlashTex()` | 22 | +| 1770 | `_makeFlashCoreTex()` | 10 | +| 1780 | `_input()` | 2 | +| 1782 | `_kd` | 42 | +| 1824 | `_ku` | 4 | +| 1828 | `_md` | 34 | +| 1862 | `_mu` | 7 | +| 1869 | `_mm` | 15 | +| 1884 | `_cc` | 1 | +| 1885 | `_blur` | 1 | +| 1886 | `_plc` | 17 | +| 1903 | `_touchControls()` | 115 | +| 2018 | `_aimAssist()` | 28 | +| 2046 | `_requestLock()` | 24 | +| 2070 | `_travaAtalhos()` | 4 | +| 2074 | `_soltaAtalhos()` | 3 | +| 2077 | `_acceptInput()` | 8 | +| 2085 | `_pauseBackdrop()` | 7 | +| 2092 | `_radioShow()` | 6 | +| 2098 | `_radioUi()` | 8 | +| 2106 | `_radioPick()` | 14 | +| 2120 | `start()` | 4 | +| 2124 | `_startRound()` | 35 | +| 2159 | `_resetPositions()` | 248 | +| 2407 | `_checkCtfAlvo()` | 13 | +| 2420 | `_checkPace()` | 13 | +| 2433 | `_endRound()` | 37 | +| 2470 | `_fimDaPartida()` | 7 | +| 2477 | `_endMatch()` | 58 | +| 2535 | `_ensureDolly()` | 41 | +| 2576 | `_tickDolly()` | 23 | +| 2599 | `setPaused()` | 22 | +| 2621 | `_now()` | 3 | +| 2624 | `pauseArmed()` | 1 | +| 2625 | `_syncPauseArm()` | 7 | +| 2632 | `resume()` | 4 | +| 2636 | `applySettings()` | 6 | +| 2642 | `_applyQuality()` | 13 | +| 2655 | `onResize()` | 7 | +| 2662 | `_switchTeam()` | 50 | +| 2712 | `_applyVmVisibility()` | 6 | +| 2718 | `_vmlabEnsure()` | 14 | +| 2732 | `_vmlabFrame()` | 28 | +| 2760 | `_tuneGet()` | 15 | +| 2775 | `_tune()` | 23 | +| 2798 | `_fxSet()` | 1 | +| 2799 | `_switchWeapon()` | 32 | +| 2831 | `_deploySfx()` | 7 | +| 2838 | `_scope()` | 17 | +| 2855 | `_zoomFov()` | 8 | +| 2863 | `_reloading()` | 1 | +| 2864 | `_startReload()` | 19 | +| 2883 | `_reloadLayers()` | 18 | +| 2901 | `_installRecoil()` | 33 | +| 2934 | `_shotRecoil()` | 13 | +| 2947 | `_tryShoot()` | 83 | +| 3030 | `_meleeHit()` | 12 | +| 3042 | `_fireHitscan()` | 53 | +| 3095 | `_surfaceOf()` | 27 | +| 3122 | `_fleshImpact()` | 35 | +| 3157 | `_fxVoice()` | 9 | +| 3166 | `_impactSfx()` | 14 | +| 3180 | `_tintFx()` | 16 | +| 3196 | `_damage()` | 39 | +| 3235 | `_kill()` | 73 | +| 3308 | `_dmgArc()` | 79 | +| 3387 | `_mkBanner()` | 9 | +| 3396 | `_hitmarker()` | 15 | +| 3411 | `_dmgNumber()` | 20 | +| 3431 | `_feed()` | 19 | +| 3450 | `_skullIcon()` | 6 | +| 3456 | `_killfeedWeaponIcon()` | 9 | +| 3465 | `_wpnIcon()` | 64 | +| 3529 | `_tracer()` | 24 | +| 3553 | `_puff()` | 39 | +| 3592 | `_holeDecalMat()` | 8 | +| 3600 | `_flash()` | 54 | +| 3654 | `_muzzleWorld()` | 9 | +| 3663 | `_updateDoors()` | 10 | +| 3673 | `_updateFx()` | 56 | +| 3729 | `_ejectCasing()` | 17 | +| 3746 | `_makeCtfFlagTex()` | 23 | +| 3769 | `_paintFlagSymbol()` | 9 | +| 3778 | `_flagTexFor()` | 26 | +| 3804 | `_legadoSimbolo()` | 8 | +| 3812 | `_loadCtfSymbols()` | 22 | +| 3834 | `_makeCtfZoneTex()` | 31 | +| 3865 | `_makeSmokeTex()` | 8 | +| 3873 | `_updateSmokeHud()` | 6 | +| 3879 | `_spawnGrenade()` | 11 | +| 3890 | `_throwSmoke()` | 8 | +| 3898 | `_throwFrag()` | 10 | +| 3908 | `_explodeFrag()` | 38 | +| 3946 | `_corDaFumaca()` | 15 | +| 3961 | `_popSmoke()` | 19 | +| 3980 | `_updateGrenades()` | 27 | +| 4007 | `_teamColor()` | 14 | +| 4021 | `_teamInk()` | 6 | +| 4027 | `_factionOf()` | 1 | +| 4028 | `_voiceKey()` | 1 | +| 4029 | `_teamName()` | 1 | +| 4030 | `_teamTag()` | 6 | +| 4036 | `_plaqueta()` | 13 | +| 4049 | `_mirror()` | 3 | +| 4052 | `_botSeparation()` | 56 | +| 4108 | `_initCTF()` | 84 | +| 4192 | `_updateCTF()` | 56 | +| 4248 | `_ctfWin()` | 22 | +| 4270 | `_freeYaw()` | 25 | +| 4295 | `_pullString()` | 23 | +| 4318 | `_walkReach()` | 18 | +| 4336 | `_wpComp()` | 16 | +| 4352 | `_findPathLocal()` | 22 | +| 4374 | `_botCtf()` | 133 | +| 4507 | `_hideCtfHud()` | 6 | +| 4513 | `_updateCtfHud()` | 76 | +| 4589 | `_collide()` | 23 | +| 4612 | `_collideRot()` | 26 | +| 4638 | `_freeSpot()` | 30 | +| 4668 | `_retaAndavel()` | 20 | +| 4688 | `_walkDepth()` | 16 | +| 4704 | `_noteHit()` | 17 | +| 4721 | `_deathFeedback()` | 43 | +| 4764 | `_updateReplayCam()` | 33 | +| 4797 | `_updatePlayer()` | 315 | +| 5112 | `_updatePickups()` | 148 | +| 5260 | `_wpnMode()` | 5 | +| 5265 | `_botWeapon()` | 10 | +| 5275 | `_municaoInfinita()` | 1 | +| 5276 | `_pickupAllowed()` | 7 | +| 5283 | `_grabPickup()` | 35 | +| 5318 | `_assentarNoChao()` | 11 | +| 5329 | `_dropWeapon()` | 18 | +| 5347 | `_sumirDrop()` | 36 | +| 5383 | `_spawnY()` | 3 | +| 5386 | `_pickSpawn()` | 23 | +| 5409 | `_respawnPlayer()` | 25 | +| 5434 | `_losClear()` | 18 | +| 5452 | `_botCall()` | 37 | +| 5489 | `_teamMarkTex()` | 23 | +| 5512 | `_makeTeamMark()` | 14 | +| 5526 | `_updateTeamMark()` | 7 | +| 5533 | `_botEye()` | 1 | +| 5534 | `_enemyOf()` | 8 | +| 5542 | `_duelToken()` | 20 | +| 5562 | `_updateBot()` | 812 | +| 6374 | `_flushTraining()` | 13 | +| 6387 | `_updateBotNN()` | 71 | +| 6458 | `_botShootNN()` | 45 | +| 6503 | `_radarFoot()` | 38 | +| 6541 | `_updateRadar()` | 64 | +| 6605 | `_banner()` | 26 | +| 6631 | `_resultadoDaRodada()` | 4 | +| 6635 | `_showScoreboard()` | 48 | +| 6683 | `_updateWeaponHud()` | 35 | +| 6718 | `_updateHud()` | 80 | +| 6798 | `update()` | 76 | +| 6874 | `dispose()` | 37 |
diff --git a/tools/eval/KNOWN-RED.json b/tools/eval/KNOWN-RED.json index aed9bffbd..80ecd5b0c 100644 --- a/tools/eval/KNOWN-RED.json +++ b/tools/eval/KNOWN-RED.json @@ -16,6 +16,8 @@ "CHR3": "proporções do elenco — mesmo lote do CHR1", "CHR4": "proporções do elenco — mesmo lote do CHR1", "CTF1": "geometria de bandeira fora do critério em mapa(s) legado(s) — ajuste de layout pendente", - "GRAF1": "grafite: 272 peças com vão atrás em 5 mapas (brasília 68 · quebrada 102 · ferro 51 · loja H 28 · piscina 23) e 145 sobrepostas. Saiu de 698/540 em 07/08 alinhando o limiar das duas réguas; o resto é peça em quina de GLB e em superfície curva. Régua: `npm run eval:grafite:ar`. Ver docs/LICOES.md §1 e §2." + "GRAF1": "grafite: 272 peças com vão atrás em 5 mapas (brasília 68 · quebrada 102 · ferro 51 · loja H 28 · piscina 23) e 145 sobrepostas. Saiu de 698/540 em 07/08 alinhando o limiar das duas réguas; o resto é peça em quina de GLB e em superfície curva. Régua: `npm run eval:grafite:ar`. Ver docs/LICOES.md §1 e §2.", + "MAT2": "dispersão de luz do viewmodel 1.63× (teto 1.60) — velho_oeste (mapa 3.92 / vm 4.72 = 1.20×) puxa a dispersão. Mapas de comunidade recém-adicionados; ajuste de iluminação pendente.", + "TEX1": "penitenciaria: 1 superfície grande e clara sem mapa de albedo (#e6e2cf 7.3 m² em 17,1,-25). Mapa de comunidade recém-adicionado; texturização pendente." } } diff --git a/tools/eval/README.md b/tools/eval/README.md index aa22b1a32..20d5061e0 100644 --- a/tools/eval/README.md +++ b/tools/eval/README.md @@ -41,6 +41,7 @@ com código 1 em falha crítica. | Arquivo | O que mede | |---|---| | `map-check.mjs` | Geometria do mapa: colisores, spawns, alcançabilidade. | +| `probe-grafite.mjs` | Sonda descartável da passada viva de grafite: abre `mapview?grafite=vivo` e despeja âncoras (com `__grafiteDebug`), contadores de recusa e superfícies por mapa. Diagnóstico, não é régua. | | `mode-check.mjs` | Rounds × CTF: o modo abre e termina em todos os mapas. | | `ctf-verify.mjs` | Bandeiras alcançáveis a partir dos dois spawns. | | `fv-verify.mjs` | Específico do Ferro Velho (A*/waypoints/LOS com o cânion BECO OESTE). | diff --git a/tools/eval/armas-check.mjs b/tools/eval/armas-check.mjs new file mode 100644 index 000000000..7f10e796b --- /dev/null +++ b/tools/eval/armas-check.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/* ============================================================================ + armas-check.mjs — A PARTIDA CARREGA AS ARMAS DELA, E NENHUM BOT SEGURA CAIXA + ---------------------------------------------------------------------------- + POR QUE EXISTE (medido em 21/08/2026) + `preloadWeapons()` baixava as 26 armas, BLOQUEANDO, antes de construir o Game — 7,5 MB de + download e ~164 MB de VRAM para uma partida que usa ~9. Era a mesma doença do elenco, que o + #368 já tinha curado nos personagens e ninguém tinha olhado nas armas. + + O QUE ELA NÃO PODE DEIXAR PASSAR + Cortar o preload é fácil; cortar E deixar bot empunhando CAIXA procedural é trocar espera por + feiura. `weaponModel()` cai em `buildRifle()` quando o GLB não está em memória, e isso não + aparece em erro nenhum — some calado dentro da cena. Por isso ARM2 cobra a cena, não o código. + + O QUE MEDE (jogo real, ?auto=, rede de verdade) + ARM1 GLBs de arma no preload BLOQUEANTE <= TETO (12). Janela: do goto até `window.__game` + existir — o Game só nasce depois do `await` do preload. Antes do conserto: 26. + ARM2 todo bot em campo tem arma com malha GLB (zero caixa procedural). + ARM3 o resto das armas CHEGA em ocioso — sem isso, o drop do chão no meio da partida vira + caixa. Sonda até 60 s; o contrato é existência, não velocidade. + + Mutantes (kill-switches reais do jogo): sem-lazy (?armaslazy=0) acende ARM3. + Uso: node tools/eval/armas-check.mjs [--mutante=sem-lazy] [--porta=8137] + ============================================================================ */ +import { execSync, spawn } from 'node:child_process'; +import { pathToFileURL } from 'node:url'; + +const arg = (n) => (process.argv.find((a) => a.startsWith(`--${n}=`)) || '').split('=')[1] || ''; +const MUT = arg('mutante'); +if (MUT && MUT !== 'sem-lazy') throw new Error(`mutante desconhecido: ${MUT}`); +const PORTA = arg('porta') || '8137'; +const BASE = `http://127.0.0.1:${PORTA}`; +const TETO = 12; + +const gRoot = execSync('npm root -g').toString().trim(); +const _pw = await import(pathToFileURL(`${gRoot}/playwright/index.js`).href); +const chromium = _pw.chromium || _pw.default?.chromium; + +const srv = spawn('node', ['tools/eval/serve.mjs', PORTA], { stdio: 'ignore' }); +process.on('exit', () => srv.kill()); +for (let i = 0; i < 60; i++) { + try { if ((await fetch(BASE)).ok) break; } catch { /* subindo */ } + await new Promise((r) => setTimeout(r, 500)); +} + +const browser = await chromium.launch({ args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--mute-audio'] }); +const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); +const ARMA_GLB = /\/models\/weapons\/([a-z0-9-]+)\.glb/; +const pedidas = []; +page.on('request', (r) => { const m = ARMA_GLB.exec(r.url()); if (m) pedidas.push({ id: m[1], t: Date.now() }); }); + +const falhas = []; +try { + await page.goto(`${BASE}/?debug=1&auto=P,mst&map=piscina_treta${MUT === 'sem-lazy' ? '&armaslazy=0' : ''}`, + { waitUntil: 'load', timeout: 180000 }); + await page.waitForFunction(() => window.__game, null, { timeout: 180000 }); + const bloqueante = new Set(pedidas.map((p) => p.id)).size; + + await page.waitForFunction(() => window.__game?.state === 'live', null, { timeout: 180000 }); + /* glbchars nomeia a peça: 'arma-glb' quando o GLB entrou, 'arma-caixa' no fallback. */ + const cena = await page.evaluate(() => { + const g = window.__game; + const semGlb = g.bots.filter((b) => b.mesh?.group?.getObjectByName('arma-caixa')).map((b) => b.weapon); + return { bots: g.bots.length, semGlb: [...new Set(semGlb)] }; + }); + + let esperou = 0, total = new Set(pedidas.map((p) => p.id)).size; + while (total < 20 && esperou < 60000) { + await page.waitForTimeout(2000); esperou += 2000; + total = new Set(pedidas.map((p) => p.id)).size; + } + + console.log(` ARM1 armas no bloqueante (<= ${TETO}): ${bloqueante} ${bloqueante <= TETO ? 'OK' : 'FALHOU'}`); + console.log(` ARM2 bots com arma de caixa (== 0): ${cena.semGlb.length} de ${cena.bots} bots${cena.semGlb.length ? ` — ${cena.semGlb.join(', ')}` : ''}`); + console.log(` ARM3 armas carregadas ao todo: ${total} em ${(esperou / 1000).toFixed(0)}s`); + if (bloqueante > TETO) falhas.push(`ARM1 preload bloqueante com ${bloqueante} armas (teto ${TETO})`); + if (cena.semGlb.length) falhas.push(`ARM2 ${cena.semGlb.length} bot(s) empunhando caixa: ${cena.semGlb.join(', ')}`); + if (total <= bloqueante) falhas.push(`ARM3 carga tardia não chegou: parou em ${total} armas`); +} catch (e) { + falhas.push(`ARM não deu para medir: ${String(e).split('\n')[0]}`); +} + +await browser.close(); +srv.kill(); +for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); +if (!falhas.length) console.log(' \x1b[32m✓\x1b[0m ARM partida carrega as armas dela, nenhum bot com caixa e o resto chega em ocioso'); +if (MUT && !falhas.length) { + console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego (lei 3)`); + falhas.push('mutacao-cega'); // prova que não morde é vermelho, não aviso +} +process.exit(falhas.length ? 1 : 0); diff --git a/tools/eval/autofix-check.mjs b/tools/eval/autofix-check.mjs new file mode 100644 index 000000000..ec57c4434 --- /dev/null +++ b/tools/eval/autofix-check.mjs @@ -0,0 +1,141 @@ +#!/usr/bin/env node +/* ============================================================================ + autofix-check.mjs — O BOT CONSERTA, MAS SÓ O QUE É DERIVADO + ---------------------------------------------------------------------------- + O autofix é a primeira máquina deste repositório com permissão de ESCREVER num + PR. Isso só é aceitável enquanto três coisas valerem, e esta régua é quem as + segura: + + AF1 · toda escrita passa pela lista de permissão. Um `git commit`/`git push` no + autofix.yml que não seja precedido pela trava é a régua inteira contornada. + AF2 · a lista nunca cobre caminho de código. `public/`, `src/`, `scripts/`, + `.github/` e os manifestos ficam de fora — inclusive `.github/`, porque um + bot que edita o workflow que o governa amplia a própria permissão. + AF3 · o bot não mergeia. Deixar o PR pronto e fechá-lo são coisas diferentes, e + a segunda é humana (decisão do dono, 21/08). + AF4 · e isso vale para o repositório INTEIRO, não só para o autofix: nenhum + workflow pode chamar `gh pr merge`. O `csbrasil-bot-automerge` chamava, e + era a única coisa que um bot daqui fazia sozinho — justamente a que não + devia. Ele agora aplica `pronto-pra-merge` e o botão continua humano. + AF6 · o autofix acorda quando a MAIN anda, não só quando o PR se mexe. Foram 9 + releases em 20 horas e cada um reabre conflito em todo PR aberto; sem o + gatilho de push o bot só conserta quem empurra commit — ou seja, nunca + quem está parado esperando revisão, que é justamente quem precisa. + AF5 · o resolvedor de conflito passa pela MESMA trava e ABORTA quando sobra + conflito fora dela. Resolver conflito de arquivo gerado é mecânico (todo + `chore(release)` reabre um em cada PR aberto); resolver conflito de código + é julgamento. Sem o `git merge --abort` no caminho de exceção, o bot + resolveria código escolhendo um lado no escuro. + + Uso: node tools/eval/autofix-check.mjs [--mutante=] + ============================================================================ */ +import { readFileSync, readdirSync } from 'node:fs'; + +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +const MUTANTES = { + 'autofix-sem-trava': 'AF1', + 'lista-abre-codigo': 'AF2', + 'lista-abre-workflow': 'AF2', + 'autofix-mergeia': 'AF3', + 'automerge-volta': 'AF4', + 'resolve-conflito-de-codigo': 'AF5', + 'sem-varredura-pos-release': 'AF6', +}; +if (MUT && !MUTANTES[MUT]) { console.error(`mutante desconhecido: ${MUT}`); process.exit(2); } + +const ler = (p) => { try { return readFileSync(p, 'utf8'); } catch { return ''; } }; +const falhas = []; + +let wf = ler('.github/workflows/autofix.yml'); +let lista = ler('scripts/ci/autofix_allowlist.py'); + +if (MUT === 'autofix-sem-trava') wf = wf.replace(/autofix_allowlist\.py/g, 'cat'); +if (MUT === 'autofix-mergeia') wf = wf.replace('git push origin "HEAD:$HEAD_REF"', 'git push origin "HEAD:$HEAD_REF"\n gh pr merge "$PR_NUMBER" --squash --auto'); +if (MUT === 'lista-abre-codigo') lista = lista.replace(' "docs/",', ' "docs/",\n "public/",'); +if (MUT === 'lista-abre-workflow') lista = lista.replace(/^\s*"\.github\/",$/m, ''); + +if (!wf) { console.log(' \x1b[31m✗\x1b[0m AF0 .github/workflows/autofix.yml não existe'); process.exit(1); } + +/* ---- AF1: escrita só depois da trava ---- */ +const usaTrava = /autofix_allowlist\.py/.test(wf); +const escreve = /git (commit|push)/.test(wf); +const posTrava = wf.indexOf('autofix_allowlist.py'); +const posCommit = wf.search(/git commit/); +if (escreve && (!usaTrava || posCommit < posTrava)) { + falhas.push('AF1 autofix.yml escreve no PR sem passar pela lista de permissão antes'); +} +/* O commit tem de usar SÓ o que a trava liberou, não um `git add -A`. */ +if (/git add\s+(-A|--all|\.)(\s|$)/.test(wf)) { + falhas.push('AF1 autofix.yml usa `git add` abrangente — o commit deve sair da saída da trava'); +} + +/* ---- AF2: a lista não cobre código ---- */ +if (!lista) falhas.push('AF2 scripts/ci/autofix_allowlist.py não existe'); +else { + const bloco = (lista.match(/PERMITIDOS = \(([\s\S]*?)\)/) || [])[1] || ''; + const proibidos = (lista.match(/PROIBIDOS = \(([\s\S]*?)\)/) || [])[1] || ''; + for (const perigoso of ['public/', 'src/', 'supabase/', 'scripts/', '.github/']) { + if (new RegExp(`"${perigoso.replace('.', '\\.')}`).test(bloco)) { + falhas.push(`AF2 a lista de permissão cobre \`${perigoso}\` — o bot passa a poder reescrever código`); + } + } + for (const obrigatorio of ['.github/', 'scripts/', 'package.json']) { + if (!new RegExp(`"${obrigatorio.replace('.', '\\.')}`).test(proibidos)) { + falhas.push(`AF2 \`${obrigatorio}\` não está na lista de proibidos`); + } + } +} + +/* ---- AF3: o bot não mergeia ---- */ +if (/gh pr merge/.test(wf)) { + falhas.push('AF3 autofix.yml mergeia PR — o autofix deixa pronto, quem fecha é gente'); +} + +/* ---- AF5: o resolvedor de conflito aborta quando o conflito é de gente ---- */ +if (MUT === 'resolve-conflito-de-codigo') { + wf = wf.replace(/\s*git merge --abort\n/, '\n'); +} +const resolveConflito = /--caminhos/.test(wf) && /git checkout --theirs/.test(wf); +if (resolveConflito) { + const trecho = wf.slice(wf.indexOf('diff-filter=U'), wf.indexOf('git checkout --theirs')); + if (!/autofix_allowlist\.py --caminhos/.test(trecho)) { + falhas.push('AF5 o resolvedor escolhe lado do conflito sem consultar a lista de permissão'); + } + if (!/git merge --abort/.test(trecho)) { + falhas.push('AF5 o resolvedor não aborta o merge quando sobra conflito fora da lista — passaria a decidir código no escuro'); + } + if (!/--caminhos/.test(ler('scripts/ci/autofix_allowlist.py'))) { + falhas.push('AF5 autofix_allowlist.py não entende `--caminhos`, que é como o resolvedor o consulta'); + } +} + +/* ---- AF6: a varredura pós-release existe e enxerga quem ficou para trás ---- */ +if (MUT === 'sem-varredura-pos-release') wf = wf.replace(/\n push:\n branches: \[main\]/, ''); +const temPush = /^\s{2}push:\n\s{4}branches: \[main\]/m.test(wf); +const temVarredura = /varredura:\n\s+if: github\.event_name == 'push'/.test(wf); +const filtraAtrasado = /mergeable != "MERGEABLE"/.test(wf) && /gh workflow run autofix\.yml/.test(wf); +if (!temPush) falhas.push('AF6 autofix.yml não acorda quando a main anda — PR parado esperando revisão nunca é consertado'); +else if (!temVarredura) falhas.push('AF6 o gatilho de push existe mas não há job de varredura'); +else if (!filtraAtrasado) falhas.push('AF6 a varredura não seleciona os PRs desatualizados nem dispara o autofix de cada um'); + +/* ---- AF4: e nenhum outro workflow mergeia tampouco ---- */ +const WORKFLOWS = readdirSync('.github/workflows').filter((f) => /\.ya?ml$/.test(f)); +for (const nome of WORKFLOWS) { + let texto = ler(`.github/workflows/${nome}`); + if (MUT === 'automerge-volta' && nome === 'csbrasil-bot-automerge.yml') { + texto += "\n subprocess.run(['gh', 'pr', 'merge', pr, '--squash', '--auto'])\n"; + } + /* Só a CHAMADA conta: o comentário que explica por que ela saiu tem de poder citá-la. */ + const linhas = texto.split('\n').filter((l) => !/^\s*#/.test(l)); + if (linhas.some((l) => /gh['"]?,\s*['"]pr['"],\s*['"]merge['"]|gh pr merge/.test(l))) { + falhas.push(`AF4 ${nome} mergeia PR sozinho — deixar pronto e fechar são coisas diferentes`); + } +} + +for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); +if (!falhas.length) console.log(' \x1b[32m✓\x1b[0m AF autofix escreve só o que a trava libera, a lista não cobre código e o bot não mergeia'); +if (MUT && !falhas.length) { + console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego`); + falhas.push('mutacao-cega'); // prova que não morde é vermelho, não aviso (MC1) +} +process.exit(falhas.length ? 1 : 0); diff --git a/tools/eval/backend-hints-check.mjs b/tools/eval/backend-hints-check.mjs new file mode 100644 index 000000000..315e4b199 --- /dev/null +++ b/tools/eval/backend-hints-check.mjs @@ -0,0 +1,92 @@ +/* ============================================================================ + backend-hints-check.mjs — O BUNDLE PÚBLICO NÃO NOMEIA O BACKEND + ---------------------------------------------------------------------------- + POR QUE EXISTE + Decisão do dono (15/08/2026): separar totalmente backend de frontend — quem + abre o jogo vê o JOGO, não pistas de qual provedor de banco/autenticação + sustenta o ranking e a telemetria. "ao dar pistas se usamos supabase, postgres + o que, so verem mesmo o jogo web". O backend mora atrás de /api/* e é + descrito em doc interna; o navegador não precisa saber mais que isso. + + O QUE ELA MEDE + Toda superfície SERVIDA CRUA ao navegador, no FONTE (determinístico, sem + build): public/js/**, public/llms.txt, src/pages/**.astro, CHANGELOG.md + (renderizado por /changelog). Nenhuma pode conter nome de provedor de + dados/infra ou termo de credencial de banco. + + FORA DO ESCOPO, DE PROPÓSITO + - `src/pages/api/**` e `src/lib/**`: código de servidor, nunca vai ao browser. + - `docs/` (Docusaurus) builda para /docs e hoje menciona a stack 50×: é doc + de CONTRIBUIDOR e virar neutra é decisão editorial pendente do dono. Quando + decidir, tire a entrada da DÍVIDA abaixo — ela só pode encolher. + + Mutante: --mutante=inject (injeta provedor num arquivo público → tem que + acender). A lista de padrões vazia é detectada SOZINHA em toda corrida: + régua sem padrão é régua cega (o mutante de esvaziar a lista virou asserção + estrutural — mais forte, morde sem --mutante). + + Uso: node tools/eval/backend-hints-check.mjs [--mutante=inject|vazio] + ============================================================================ */ +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +if (MUT && !['inject'].includes(MUT)) throw new Error(`mutante desconhecido: ${MUT}`); + +const PADROES = [ + [/supabase/i, 'supabase'], + [/postgres(?:ql)?/i, 'postgres'], + [/service_role/i, 'service_role'], + [/db-privado/i, 'db-privado'], + [/firebase/i, 'firebase'], + [/mongodb|mysql|redis/i, 'banco de terceiro'], +]; +if (!PADROES.length) { + console.error(' \x1b[31m✗\x1b[0m HINT lista de padrões vazia — régua cega medindo nada'); + process.exit(1); +} + +const SUPERFICIES = ['public/js', 'public/llms.txt', 'src/pages', 'CHANGELOG.md']; +const DÍVIDA = [ + // docs/ buildada em /docs: decisão editorial pendente (ver cabeçalho). + // 'dist/client/docs': 50 menções em 15/08/2026. +]; + +const arquivos = []; +const anda = (p) => { + const s = statSync(p); + if (s.isDirectory()) for (const f of readdirSync(p)) anda(join(p, f)); + else arquivos.push(p); +}; +for (const s of SUPERFICIES) anda(s); +const servidos = arquivos.filter((f) => /\.(js|mjs|txt|astro|md)$/.test(f)); + +/* .astro: o frontmatter (--- ... ---) roda no SERVIDOR — import de supabaseAdmin + ali não chega ao browser. Medir o arquivo inteiro dava falso positivo em + mapa.astro/ranking.astro e ensinaria a ignorar o vermelho. O que o browser + recebe é o que vem DEPOIS do frontmatter (HTML +