From fb97b48d029cb235bff49adafad67e8f78250f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Man=C3=A1=20Soares?= <153231177+manazitto@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:31:17 -0300 Subject: [PATCH 01/14] =?UTF-8?q?feat(i18n):=20g=C3=AAmea=20EN=20do=20/cha?= =?UTF-8?q?ngelog=20e=20idioma=20honesto=20nas=20rotas=20EN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O /changelog ganha /whats-new. Os pares PT↔EN passam a uma tabela só (hreflang, html lang, og:locale, sitemap e JSON-LD), e as duas páginas do changelog compartilham o parser. eval:i18ntwins cobre par, redirect, cromo, lang, sitemap e parser; os seis mutantes mordem. #54 Signed-off-by: Maná Soares <153231177+manazitto@users.noreply.github.com> Agent: Cursor --- SCRIPTS.md | 8 ++ package.json | 3 +- public/js/i18n.js | 2 +- public/js/main.js | 4 +- scripts/aeo.mjs | 3 +- src/layouts/Layout.astro | 37 ++++---- src/lib/changelog.ts | 51 ++++++++++ src/lib/i18n-pairs.ts | 10 ++ src/pages/changelog.astro | 74 +++------------ src/pages/sitemap.xml.ts | 2 + src/pages/whats-new.astro | 161 ++++++++++++++++++++++++++++++++ tools/eval/i18n-twins-check.mjs | 85 +++++++++++++++++ tools/eval/seo-check.mjs | 3 +- tools/eval/site-smoke.mjs | 9 +- 14 files changed, 365 insertions(+), 87 deletions(-) create mode 100644 src/lib/changelog.ts create mode 100644 src/lib/i18n-pairs.ts create mode 100644 src/pages/whats-new.astro create mode 100644 tools/eval/i18n-twins-check.mjs diff --git a/SCRIPTS.md b/SCRIPTS.md index 74b2ebad2..6123025d3 100644 --- a/SCRIPTS.md +++ b/SCRIPTS.md @@ -370,6 +370,14 @@ Contrato de produção do BotBrain: coleta opt-in autenticada por UID, limites c npm run eval:botbrain ``` +## `eval:i18ntwins` + +Pares PT↔EN numa tabela só (`src/lib/i18n-pairs.ts`): hreflang, `html lang`, og:locale, sitemap e o parser único do changelog. A gêmea do `/changelog` é `/whats-new`; o cromo é EN e o corpo continua o `CHANGELOG.md`. Mutantes: sem-par|sem-redirect|chrome-pt|lang-pt|sem-sitemap|parser-dup. + +```bash +npm run eval:i18ntwins +``` + ## `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. diff --git a/package.json b/package.json index 7d9b4e2a9..27c0bd2a9 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "bot:train": "node tools/eval/bot-train.mjs", "bot:brain:check": "node tools/eval/bot-brain-check.mjs", "eval:botbrain": "node tools/eval/botbrain-safety-check.mjs", + "eval:i18ntwins": "node tools/eval/i18n-twins-check.mjs", "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", @@ -118,7 +119,7 @@ "eval:ctfwin": "node tools/eval/ctf-win-check.mjs", "eval:spawn": "node tools/eval/spawn-settle-check.mjs", "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:comentario eval:fixture eval:preload eval:docsautoria", - "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:comentario eval:fixture eval:preload eval:docsautoria", + "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:i18ntwins eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:preload eval:docsautoria", "eval:tags": "node tools/eval/tag-main-check.mjs", "eval:simclock": "node tools/eval/sim-clock-check.mjs" }, diff --git a/public/js/i18n.js b/public/js/i18n.js index 09d70aa69..42a9220ad 100644 --- a/public/js/i18n.js +++ b/public/js/i18n.js @@ -8,7 +8,7 @@ 3. `translateDom(root)`: varre nós de texto e atributos (placeholder/title/aria) do menu ESTÁTICO uma vez no boot — zero mudança no index.astro; 4. `frase(id, ...args)`: os textos DINÂMICOS do game.js (banner, HUD) com template. - Páginas do site e docs em EN são outra frente (issue #54). */ + Páginas do site e docs em EN são outra frente (issue #54; changelog já tem /whats-new). */ let _lang = null; // ?lang=pt|en na URL vence tudo (teste/demonstração — ex.: a live mostra EN sem mexer em config) diff --git a/public/js/main.js b/public/js/main.js index 208e1284a..9c228f10b 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -760,9 +760,9 @@ await resolveGeoLang(); // EN por camada: varre o menu estático UMA vez (PT é a fonte; i18n.js explica o desenho) translateDom(document.body); // links do rodapé por idioma: EN vai pras gêmeas que EXISTEM (characters, how-to-play, -// weapons, maps, about, docs/en); /changelog e /mapa continuam só PT (issue #54) +// weapons, maps, about, whats-new, docs/en); /mapa continua só PT (issue #54) if (LANG === 'en') for (const a of document.querySelectorAll('.menu-footer a')) { - const GEMEA = { '/personagens': '/characters', '/como-jogar': '/how-to-play', '/armas': '/weapons', '/mapas': '/maps', '/sobre': '/about', '/docs/': '/docs/en/' }; + const GEMEA = { '/personagens': '/characters', '/como-jogar': '/how-to-play', '/armas': '/weapons', '/mapas': '/maps', '/sobre': '/about', '/changelog': '/whats-new', '/docs/': '/docs/en/' }; const h = a.getAttribute('href'); if (GEMEA[h]) a.setAttribute('href', GEMEA[h]); } diff --git a/scripts/aeo.mjs b/scripts/aeo.mjs index b13a10e64..c47fc0a87 100644 --- a/scripts/aeo.mjs +++ b/scripts/aeo.mjs @@ -45,7 +45,8 @@ import { fileURLToPath } from 'node:url'; * A home entra assim mesmo, mas com o texto CURADO do `llms.txt`. * · `/ranking`, `/u/*`, `/mapa`: SSR, não têm HTML em build time. As duas * primeiras estão `noindex` enquanto RANKING_ON for false. */ -const CONTENT_PAGES = ['/armas', '/mapas', '/personagens', '/como-jogar', '/sobre', '/changelog']; +const CONTENT_PAGES = ['/armas', '/mapas', '/personagens', '/como-jogar', '/sobre', '/changelog', + '/how-to-play', '/maps', '/weapons', '/characters', '/about', '/whats-new']; function readPage(clientDir, pathname) { const file = pathname === '/' diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index b7d933366..178b5c3b4 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -37,6 +37,7 @@ // plans/08 §1.1). O canarinho do topo sai pelo WebP animado de 96 KB, não pelo // GIF de 368 KB - mesmo conteúdo, 24 quadros, 26% do peso. import { SITE, BRAND, BRAND_FULL, BRAND_ALT, DESC_SHORT, ORG_ID, WEBSITE_ID, abs, RANKING_ON, DISCORD_URL, TELEGRAM_URL, GITHUB_URL } from '../lib/site'; +import { PT_EN_PAIRS } from '../lib/i18n-pairs'; import pkg from '../../package.json'; const V = pkg.version; @@ -105,20 +106,12 @@ const temSub = Astro.slots.has('sub'); const path = Astro.url.pathname.replace(/\/+$/, '') || '/'; const canonical = new URL(path, Astro.site ?? SITE).toString(); -// Tabela fixa porque cada rota EN é uma página própria, não um prefixo de idioma. -// Alimenta o hreflang recíproco e o link PT/EN estático do rodapé (a troca por JS -// não é descoberta por crawl). -const PT_EN_PAIRS: [string, string][] = [ - ['/como-jogar', '/how-to-play'], - ['/personagens', '/characters'], - ['/mapas', '/maps'], - ['/armas', '/weapons'], - ['/sobre', '/about'], -]; -const ptToEn = new Map(PT_EN_PAIRS); -const enToPt = new Map(PT_EN_PAIRS.map(([pt, en]) => [en, pt])); +const ptToEn = new Map(PT_EN_PAIRS.map(([pt, en]) => [pt, en])); +const enToPt = new Map(PT_EN_PAIRS.map(([pt, en]) => [en, pt])); const pairedEn = ptToEn.get(path); const pairedPt = enToPt.get(path); +const pageLang = pairedPt ? 'en' : 'pt-BR'; +const ogLocale = pageLang === 'en' ? 'en_US' : 'pt_BR'; const hreflangPt = pairedPt ? new URL(pairedPt, Astro.site ?? SITE).toString() : canonical; const hreflangEn = pairedEn ? new URL(pairedEn, Astro.site ?? SITE).toString() : pairedPt ? canonical : null; @@ -164,13 +157,19 @@ const pageGraph = hasOwnPageNode ? [] : [{ url: canonical, name: title, description, - inLanguage: 'pt-BR', + inLanguage: pageLang, isPartOf: { '@id': WEBSITE_ID }, primaryImageOfPage: { '@type': 'ImageObject', url: ogImage, width: ogImageWidth, height: ogImageHeight, }, }]; +const jsonldLang = (jsonld as { '@type'?: string | string[]; inLanguage?: string }[]).map((n) => { + const t = n?.['@type']; + const types = Array.isArray(t) ? t : t ? [t] : []; + if (types.some((x) => PAGE_NODE_TYPES.has(x)) && !n.inLanguage) return { ...n, inLanguage: pageLang }; + return n; +}); // O /ranking sai do nav enquanto RANKING_ON for false (src/lib/site.ts). A página // continua respondendo - só deixa de ser oferecida, pra ninguém clicar num aviso. @@ -185,7 +184,7 @@ const NAV: [string, string][] = [ ]; --- - + @@ -211,7 +210,8 @@ const NAV: [string, string][] = [ - + +{hreflangEn && } @@ -235,7 +235,7 @@ const NAV: [string, string][] = [ - + + diff --git a/src/pages/sitemap.xml.ts b/src/pages/sitemap.xml.ts index f066caa8f..b66c06bac 100644 --- a/src/pages/sitemap.xml.ts +++ b/src/pages/sitemap.xml.ts @@ -29,6 +29,7 @@ import type { APIRoute } from 'astro'; import { supabaseAdmin } from '../lib/supabase'; import { SITE, RANKING_ON } from '../lib/site'; +import { PT_EN_PAIRS } from '../lib/i18n-pairs'; import { FACCOES } from '../data/jogo'; import { POR_PAGINA, numeroDePaginas, offsetDaPagina, xmlUrlset, xmlIndex, @@ -61,6 +62,7 @@ export const STATIC: [string, string, string][] = [ ['/sobre', '0.7', 'monthly'], ['/mapa', '0.6', 'daily'], ['/changelog', '0.5', 'weekly'], + ...PT_EN_PAIRS.map(([, en]) => [en, '0.5', 'weekly'] as [string, string, string]), ]; export const GET: APIRoute = async () => { diff --git a/src/pages/whats-new.astro b/src/pages/whats-new.astro new file mode 100644 index 000000000..409dae27f --- /dev/null +++ b/src/pages/whats-new.astro @@ -0,0 +1,161 @@ +--- +// /whats-new - GÊMEA EM INGLÊS do /changelog (issue #54). Mesmo CHANGELOG.md, +// mesmo renderizador, cromo traduzido aqui. As notas de versão continuam em PT: +// traduzir o histórico inteiro é outra frente, e os nomes de mapa/arma são marca. +import Layout from '../layouts/Layout.astro'; +import { BRAND_FULL, GAME_ID, abs } from '../lib/site'; +import { parseChangelog } from '../lib/changelog'; + +/* Mesmo `?raw` do /changelog: o prerender mora em dist/server e um readFileSync + por import.meta.url quebraria o build (BUG documentado na irmã PT). */ +import md from '../../CHANGELOG.md?raw'; + +const { versoes, recentes, atualVer } = parseChangelog(md); + +const jsonld = [ + { + '@type': 'ItemList', + '@id': abs('/whats-new') + '#lista', + name: `${BRAND_FULL} version history`, + numberOfItems: recentes.length, + itemListElement: recentes.map((v, i) => ({ + '@type': 'ListItem', position: i + 1, name: `v${v.versao}`, + })), + }, + { + '@type': 'BreadcrumbList', + itemListElement: [ + { '@type': 'ListItem', position: 1, name: 'Home', item: abs('/') }, + { '@type': 'ListItem', position: 2, name: 'Changelog', item: abs('/whats-new') }, + ], + }, + { '@id': GAME_ID, softwareVersion: atualVer }, +]; +--- + + + Everything that changed, version by version. Production is v{atualVer}. + The full file lives in CHANGELOG.md in the repository - this page is that + file, rendered. + + +
+ + {recentes.map((v, i) => ( +
+ + v{v.versao} + {v.data && · {v.data}} + # + +
+
+ ))} + + {versoes.length > recentes.length && ( +

+ Versions older than v{recentes[recentes.length - 1].versao} live in + CHANGELOG.md. +

+ )} +
+ + + + + + diff --git a/tools/eval/i18n-twins-check.mjs b/tools/eval/i18n-twins-check.mjs new file mode 100644 index 000000000..7a9b1e2f9 --- /dev/null +++ b/tools/eval/i18n-twins-check.mjs @@ -0,0 +1,85 @@ +/* ============================================================================ + i18n-twins-check.mjs — pares PT↔EN das páginas estáticas (issue #54) + ---------------------------------------------------------------------------- + Uma tabela (`src/lib/i18n-pairs.ts`) alimenta hreflang, lang, og:locale e + sitemap. A gêmea do /changelog é /whats-new; o cromo é EN e o corpo continua + o CHANGELOG.md, parseado numa fonte só. + + Mutantes: sem-par | sem-redirect | chrome-pt | lang-pt | sem-sitemap | parser-dup + ============================================================================ */ +import { existsSync, readFileSync } from 'node:fs'; + +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +if (MUT && !['sem-par', 'sem-redirect', 'chrome-pt', 'lang-pt', 'sem-sitemap', 'parser-dup'].includes(MUT)) + throw new Error(`mutante desconhecido: ${MUT}`); + +const read = (file) => (existsSync(file) ? readFileSync(file, 'utf8') : ''); + +let pairs = read('src/lib/i18n-pairs.ts'); +let layout = read('src/layouts/Layout.astro'); +let sitemap = read('src/pages/sitemap.xml.ts'); +let changelog = read('src/pages/changelog.astro'); +let gemea = read('src/pages/whats-new.astro'); +let menu = read('public/js/main.js'); + +if (MUT === 'sem-par') + pairs = pairs.replace(" ['/changelog', '/whats-new'],\n", ''); +if (MUT === 'sem-redirect') + changelog = changelog.replace("location.replace('/whats-new')", "location.replace('/changelog')"); +if (MUT === 'chrome-pt') + gemea = gemea.replace(/production version/g, 'versão em produção'); +if (MUT === 'lang-pt') + layout = layout.replace('', ''); +if (MUT === 'sem-sitemap') + sitemap = sitemap.replaceAll('PT_EN_PAIRS', '[]'); +if (MUT === 'parser-dup') + gemea = gemea.replace("from '../lib/changelog'", "from '../lib/site'"); + +const failures = []; +if (!pairs.includes("['/changelog', '/whats-new']")) + failures.push('I18N0 a tabela única não declara /changelog ↔ /whats-new'); +if (!layout.includes("from '../lib/i18n-pairs'") || layout.includes("const PT_EN_PAIRS: [string, string][]")) + failures.push('I18N0 Layout ainda copia a tabela em vez de importar src/lib/i18n-pairs.ts'); +if (!sitemap.includes("from '../lib/i18n-pairs'")) + failures.push('I18N0 sitemap não lê a tabela única'); + +if (!existsSync('src/pages/whats-new.astro')) + failures.push('I18N1 falta src/pages/whats-new.astro — gêmea EN do /changelog'); +if (!changelog.includes("location.replace('/whats-new')")) + failures.push('I18N2 /changelog em EN não manda para /whats-new'); +if (!gemea.includes("location.replace('/changelog')")) + failures.push('I18N2 /whats-new em PT não manda para /changelog'); +if (!layout.includes("'/changelog': ['/whats-new'")) + failures.push('I18N3 Layout GEMEA em EN não troca o href do changelog'); +if (!menu.includes("'/changelog': '/whats-new'")) + failures.push('I18N3 menu do jogo em EN não troca o href do changelog'); +if (gemea && !gemea.includes('production version')) + failures.push('I18N4 cromo da gêmea ainda não está em inglês (kicker)'); +if (gemea && !gemea.includes('filter versions')) + failures.push('I18N4 cromo da gêmea ainda não está em inglês (busca)'); +if (gemea && !gemea.includes("import md from '../../CHANGELOG.md?raw'")) + failures.push('I18N5 gêmea EN não renderiza o mesmo CHANGELOG.md'); + +if (!layout.includes('') || !layout.includes("pairedPt ? 'en' : 'pt-BR'")) + failures.push('I18N6 gêmea EN ainda declara html lang=pt-BR (crawler lê mentira)'); +if (!layout.includes("pageLang === 'en' ? 'en_US' : 'pt_BR'")) + failures.push('I18N6 og:locale das gêmeas EN continua pt_BR'); +if (!layout.includes("inLanguage: pageLang") || !layout.includes('jsonldLang')) + failures.push('I18N6 JSON-LD das gêmeas EN continua inLanguage pt-BR'); + +if (!sitemap.includes('PT_EN_PAIRS') || !/PT_EN_PAIRS\.map/.test(sitemap)) + failures.push('I18N7 sitemap não lista as rotas EN da tabela única'); + +if (!changelog.includes("from '../lib/changelog'") || !gemea.includes("from '../lib/changelog'") + || !changelog.includes('{ versoes, recentes, atualVer }') || !gemea.includes('{ versoes, recentes, atualVer }')) + failures.push('I18N8 /changelog e /whats-new não compartilham o parser (vão divergir)'); + +for (const failure of failures) console.error(` \x1b[31m✗\x1b[0m ${failure}`); +if (MUT && !failures.length) failures.push(`mutação ${MUT} não foi detectada`); + +if (failures.length) { + console.error(`\x1b[31mI18N TWINS ${failures.length} VERMELHA(S)\x1b[0m${MUT ? ` (mutante=${MUT})` : ''}`); + process.exitCode = 1; +} else { + console.error('\x1b[32mI18N TWINS verde: pares numa tabela, lang/og/sitemap honestos, changelog com parser único\x1b[0m'); +} diff --git a/tools/eval/seo-check.mjs b/tools/eval/seo-check.mjs index 3a32b0a08..62f6fd98d 100644 --- a/tools/eval/seo-check.mjs +++ b/tools/eval/seo-check.mjs @@ -55,7 +55,8 @@ const JSON_OUT = process.argv.includes('--json'); const SITE = 'https://www.csbrasil.online'; /** Páginas prerenderizadas que o build produz. `/ranking`, `/mapa` e `/u/*` são * SSR e não existem aqui — o que o crawler vê nelas é medido em runtime. */ -const PAGES = ['/', '/armas', '/mapas', '/personagens', '/como-jogar', '/sobre', '/changelog']; +const PAGES = ['/', '/armas', '/mapas', '/personagens', '/como-jogar', '/sobre', '/changelog', + '/how-to-play', '/maps', '/weapons', '/characters', '/about', '/whats-new']; /* Afirmações que só podem aparecer com RANKING_ON=true. São recortes de frase, não palavras soltas: "ranking" sozinho é legítimo (o menu RANKING do jogo diff --git a/tools/eval/site-smoke.mjs b/tools/eval/site-smoke.mjs index ffedfde76..e0b79c7a3 100644 --- a/tools/eval/site-smoke.mjs +++ b/tools/eval/site-smoke.mjs @@ -59,7 +59,7 @@ const BASE = EXTERNO || `http://localhost:${PORTA}`; // --------------------------------------------------------------------------- // 8 páginas indexáveis hoje; /ranking entra na conta só com o ranking ligado — // desligado ele é noindex (ranking.astro), e noindex no sitemap é contradição. -const XML_LOC_MIN = RANKING_ON ? 9 : 8; +const XML_LOC_MIN = RANKING_ON ? 15 : 14; const CONTRATO = [ { rota: '/', esperado: '200 + com "CORO SOLTO"', @@ -95,7 +95,8 @@ const CONTRATO = [ `public/js` existe e o defeito não pode acontecer. Quem mede o artefato publicado é `npm run eval:ssr`. Esta linha pega a próxima página que quebrar por outro motivo; ela não teria pego aquela. */ - ...['/como-jogar', '/mapas', '/mapa', '/armas', '/personagens', '/sobre', '/changelog'].map((rota) => ({ + ...['/como-jogar', '/mapas', '/mapa', '/armas', '/personagens', '/sobre', '/changelog', + '/how-to-play', '/maps', '/weapons', '/characters', '/about', '/whats-new'].map((rota) => ({ rota, esperado: '200 + corpo não vazio', checa: ({ status, corpo }) => { if (status !== 200) return `status ${status}`; @@ -110,6 +111,7 @@ const CONTRATO = [ if (!/^\s*<\?xml/.test(corpo)) return 'não começa com declaração XML'; const locs = corpo.match(/<loc>/g) || []; if (locs.length < XML_LOC_MIN) return `${locs.length} <loc>, mínimo ${XML_LOC_MIN}`; + if (!corpo.includes('/whats-new')) return 'sitemap sem a gêmea EN /whats-new'; // XML mal formado é o modo de falha silenciosa aqui: um & solto derruba o // parse de quem consome e o status continua 200. const abertas = (corpo.match(/<url>/g) || []).length; @@ -151,7 +153,8 @@ const CONTRATO = [ // Páginas que precisam ter todo bloco JSON-LD parseável. JSON-LD quebrado é // invisível: some do resultado rico e ninguém percebe por meses. -const PAGINAS_JSONLD = ['/', '/ranking', '/como-jogar', '/mapas', '/armas', '/personagens', '/sobre', '/changelog']; +const PAGINAS_JSONLD = ['/', '/ranking', '/como-jogar', '/mapas', '/armas', '/personagens', '/sobre', '/changelog', + '/how-to-play', '/maps', '/weapons', '/characters', '/about', '/whats-new']; // --------------------------------------------------------------------------- const RE_JSONLD = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi; From 16c34f4286e224b3dcd31752cc66eafcfc00fb4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Man=C3=A1=20Soares?= <153231177+manazitto@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:31:20 -0300 Subject: [PATCH 02/14] =?UTF-8?q?docs(gerados):=20re-deriva=20o=20bloco=20?= =?UTF-8?q?de=20scripts=20ap=C3=B3s=20eval:i18ntwins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DOCS1: check:fast e a contagem de comandos npm passam a incluir o novo eval:i18ntwins. Sem isso o portão fica vermelho na main. Signed-off-by: Maná Soares <153231177+manazitto@users.noreply.github.com> Agent: Cursor --- ARCH.generated.md | 4 ++-- STATUS.md | 2 +- docs/docs/comecando.md | 4 ++-- .../en/docusaurus-plugin-content-docs/current/comecando.md | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ARCH.generated.md b/ARCH.generated.md index dee215c79..2eb05ffba 100644 --- a/ARCH.generated.md +++ b/ARCH.generated.md @@ -25,10 +25,10 @@ Comandos e scripts atuais do quality gate: <!-- BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` --> ```bash -npm run 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:comentario eval:fixture eval:preload eval:docsautoria +npm run 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:i18ntwins eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:preload eval:docsautoria ``` -`package.json` tem **117 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. +`package.json` tem **118 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/STATUS.md b/STATUS.md index cd281af50..dbd787eb1 100644 --- a/STATUS.md +++ b/STATUS.md @@ -5,7 +5,7 @@ - **Versão:** `2.0.0-alpha.166` - **Conteúdo jogável:** 5 facções, 44 personagens, 12 mapas e 26 armas com GLB - **Código do jogo:** 31.744 linhas em 44 módulos JavaScript -- **Automação:** 117 comandos npm, 192 scripts de avaliação e 54 scripts de pipeline +- **Automação:** 118 comandos npm, 192 scripts de avaliação e 54 scripts de pipeline > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `package.json · CHARACTERS · MAPS · public/models/weapons · public/js · tools/` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index f8ca842e1..564202d3b 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -275,10 +275,10 @@ E os dois quality gates, com a lista exata do que cada um roda — direto do `pa {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -npm run 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:comentario eval:fixture eval:preload eval:docsautoria +npm run 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:i18ntwins eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:preload eval:docsautoria ``` -`package.json` tem **117 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. +`package.json` tem **118 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index b87396be4..d113bc91f 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -277,10 +277,10 @@ And the two gates, with the exact list of what each one runs — straight from ` {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -npm run 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:comentario eval:fixture eval:preload eval:docsautoria +npm run 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:i18ntwins eval:velhooeste eval:penitenciaria eval:comentario eval:fixture eval:preload eval:docsautoria ``` -`package.json` has **117 scripts**; the reason behind each one lives in `SCRIPTS.md`. +`package.json` has **118 scripts**; the reason behind each one lives in `SCRIPTS.md`. > Block generated by `node tools/gen-docs.mjs`. Source: `node -p "Object.keys(require('./package.json').scripts)"` From 3e836ca2d0a6bcf48cbb6359d31a9ffd5a581979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Man=C3=A1=20Soares?= <153231177+manazitto@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:37:08 -0300 Subject: [PATCH 03/14] =?UTF-8?q?docs(gerados):=20conta=20o=20eval:i18ntwi?= =?UTF-8?q?ns=20no=20arn=C3=AAs=20(192=20=E2=86=92=20193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DOCS1: o bloco de scripts já tinha o comando novo; faltava a contagem do harness nos 7 arquivos que o portão lê. Sem isso o CI fica vermelho. Signed-off-by: Maná Soares <153231177+manazitto@users.noreply.github.com> Agent: Cursor --- ARCH.generated.md | 2 +- README.md | 2 +- STATUS.md | 2 +- docs/docs/comecando.md | 2 +- docs/docs/quality-gates.md | 2 +- .../i18n/en/docusaurus-plugin-content-docs/current/comecando.md | 2 +- .../en/docusaurus-plugin-content-docs/current/quality-gates.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ARCH.generated.md b/ARCH.generated.md index 2eb05ffba..80eca047d 100644 --- a/ARCH.generated.md +++ b/ARCH.generated.md @@ -10,7 +10,7 @@ Números atuais das zonas, do quality gate e do `package.json`: |---|---|---|---| | `public/` | o **jogo** | 44 arquivos `.js`, 31.744 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | | `src/` | o **site** | 18 páginas `.astro`, 20 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | -| `tools/` | o **arnês** | 192 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | +| `tools/` | o **arnês** | 193 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | **Não existe `public/index.html`.** O HTML do jogo é `src/pages/index.astro`, servido na rota `/`. Servir `public/` estaticamente entrega os arnêses visuais, **não o jogo** — é a pegadinha que custa a primeira hora de todo mundo. diff --git a/README.md b/README.md index 5c5e40f3b..a41ee18aa 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 192 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 193 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Versão | `2.0.0-alpha.166` | `public/js/version.js` e `package.json` (batem) | diff --git a/STATUS.md b/STATUS.md index dbd787eb1..adfde5d18 100644 --- a/STATUS.md +++ b/STATUS.md @@ -5,7 +5,7 @@ - **Versão:** `2.0.0-alpha.166` - **Conteúdo jogável:** 5 facções, 44 personagens, 12 mapas e 26 armas com GLB - **Código do jogo:** 31.744 linhas em 44 módulos JavaScript -- **Automação:** 118 comandos npm, 192 scripts de avaliação e 54 scripts de pipeline +- **Automação:** 118 comandos npm, 193 scripts de avaliação e 54 scripts de pipeline > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `package.json · CHARACTERS · MAPS · public/models/weapons · public/js · tools/` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 564202d3b..cfb29622f 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -49,7 +49,7 @@ esta página envelhecia no primeiro commit — ver | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 192 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 193 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Versão | `2.0.0-alpha.166` | `public/js/version.js` e `package.json` (batem) | diff --git a/docs/docs/quality-gates.md b/docs/docs/quality-gates.md index 766a21af1..ffab9ccf7 100644 --- a/docs/docs/quality-gates.md +++ b/docs/docs/quality-gates.md @@ -15,7 +15,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2.275 linhas**, **65 identificadores de invariante declarados** (`put()`), dos quais **28** têm caminho de `skip()` declarado. -- O arnês inteiro são **192 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **54 scripts** de pipeline em `tools/`. +- O arnês inteiro são **193 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **54 scripts** de pipeline em `tools/`. - Quantas invariantes rodam como **críticas** numa execução **não é derivável do fonte**: depende de qual insumo existe na máquina (o JSON do auditor de viewmodel, um GLB, uma pasta de anims). Esse número só sai rodando o quality gate — e o lugar dele é o cabeçalho do `KNOWN-BUGS.md`, atualizado com saída real. Reproduza: diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index d113bc91f..e7952cafd 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -51,7 +51,7 @@ this page was aging at the very first commit — see | Playable characters | 44, in 5 factions | `CHARACTERS` array in `characters.js` | | Maps in the registry | 12 | `MAPS` object in `maps.js` | | Visual harnesses in HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Harness scripts | 192 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Harness scripts | 193 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Written entry tasks | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Version | `2.0.0-alpha.166` | `public/js/version.js` and `package.json` (match) | diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md index 4d6a4702a..9b4d9e613 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md @@ -18,7 +18,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2,275 lines**, **65 declared invariant identifiers**, with **28** declared `skip()` paths. -- The harness contains **192 scripts** in `tools/eval/`, plus **54 pipeline scripts** in `tools/`. +- The harness contains **193 scripts** in `tools/eval/`, plus **54 pipeline scripts** in `tools/`. - The number of critical checks in one run depends on the inputs present on that machine; dated results belong in `KNOWN-BUGS.md`. ```bash From dc29eadd1babc86f1a94820d3fa43914029b093e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Man=C3=A1=20Soares?= <153231177+manazitto@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:40:35 -0300 Subject: [PATCH 04/14] =?UTF-8?q?docs(gerados):=20/whats-new=20entra=20na?= =?UTF-8?q?=20conta=20das=20p=C3=A1ginas=20(18=20=E2=86=92=2019)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O bloco de zonas do ARCH.generated.md ainda dizia 18 páginas .astro. O CI do DOCS1 caiu só neste arquivo depois da contagem do arnês. Signed-off-by: Maná Soares <153231177+manazitto@users.noreply.github.com> Agent: Cursor --- ARCH.generated.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCH.generated.md b/ARCH.generated.md index 80eca047d..525dcdc1f 100644 --- a/ARCH.generated.md +++ b/ARCH.generated.md @@ -9,7 +9,7 @@ Números atuais das zonas, do quality gate e do `package.json`: | Zona | O que é | Tamanho medido | Regra | |---|---|---|---| | `public/` | o **jogo** | 44 arquivos `.js`, 31.744 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | -| `src/` | o **site** | 18 páginas `.astro`, 20 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | +| `src/` | o **site** | 19 páginas `.astro`, 20 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | | `tools/` | o **arnês** | 193 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | **Não existe `public/index.html`.** O HTML do jogo é `src/pages/index.astro`, servido na rota `/`. Servir `public/` estaticamente entrega os arnêses visuais, **não o jogo** — é a pegadinha que custa a primeira hora de todo mundo. From d71c5aa7a3cdadffaa2890ce6117aaf625c631e1 Mon Sep 17 00:00:00 2001 From: csbrasil-bot <csbrasil-bot@users.noreply.github.com> Date: Sat, 22 Aug 2026 04:18:08 +0000 Subject: [PATCH 05/14] =?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 <csbrasil-bot@users.noreply.github.com> --- package-lock.json | 4 +- package.json | 13 ++- scripts/aeo.mjs | 3 +- scripts/ci/autofix_allowlist.py | 123 ++++++++++++++++++++++ scripts/ci/ensure_labels.py | 44 ++++++-- scripts/ci/workflow_security_check.py | 122 +++++++++++++++++++++- scripts/fetch-audio.sh | 2 +- scripts/fetch-decals.sh | 2 +- tools/eval/ARCH.md | 2 +- tools/eval/autofix-check.mjs | 141 ++++++++++++++++++++++++++ tools/eval/deploy-gate-check.mjs | 75 ++++++++++++++ tools/eval/fixture-check.mjs | 1 - tools/eval/portao-inteiro-check.mjs | 68 +++++++++++++ tools/eval/seo-check.mjs | 3 +- tools/eval/site-smoke.mjs | 9 +- tools/eval/workflow-secret-check.mjs | 74 ++++++++++++++ 16 files changed, 655 insertions(+), 31 deletions(-) create mode 100644 scripts/ci/autofix_allowlist.py create mode 100644 tools/eval/autofix-check.mjs create mode 100644 tools/eval/deploy-gate-check.mjs create mode 100644 tools/eval/portao-inteiro-check.mjs create mode 100644 tools/eval/workflow-secret-check.mjs diff --git a/package-lock.json b/package-lock.json index 07a86a805..e25564558 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.175", + "version": "2.0.0-alpha.176", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coro-solto", - "version": "2.0.0-alpha.175", + "version": "2.0.0-alpha.176", "license": "AGPL-3.0-only", "dependencies": { "@astrojs/vercel": "^11.0.6", diff --git a/package.json b/package.json index a2b422f07..29eebdad5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.175", + "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": { @@ -66,7 +66,6 @@ "bot:train": "node tools/eval/bot-train.mjs", "bot:brain:check": "node tools/eval/bot-brain-check.mjs", "eval:botbrain": "node tools/eval/botbrain-safety-check.mjs", - "eval:i18ntwins": "node tools/eval/i18n-twins-check.mjs", "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", @@ -120,11 +119,15 @@ "eval:regen": "node tools/eval/regen-check.mjs", "eval:ctfwin": "node tools/eval/ctf-win-check.mjs", "eval:spawn": "node tools/eval/spawn-settle-check.mjs", - "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: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:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam", + "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": "node tools/eval/sim-clock-check.mjs", - "eval:mutcega": "node tools/eval/mutacao-cega-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", diff --git a/scripts/aeo.mjs b/scripts/aeo.mjs index c47fc0a87..b13a10e64 100644 --- a/scripts/aeo.mjs +++ b/scripts/aeo.mjs @@ -45,8 +45,7 @@ import { fileURLToPath } from 'node:url'; * A home entra assim mesmo, mas com o texto CURADO do `llms.txt`. * · `/ranking`, `/u/*`, `/mapa`: SSR, não têm HTML em build time. As duas * primeiras estão `noindex` enquanto RANKING_ON for false. */ -const CONTENT_PAGES = ['/armas', '/mapas', '/personagens', '/como-jogar', '/sobre', '/changelog', - '/how-to-play', '/maps', '/weapons', '/characters', '/about', '/whats-new']; +const CONTENT_PAGES = ['/armas', '/mapas', '/personagens', '/como-jogar', '/sobre', '/changelog']; function readPage(clientDir, pathname) { const file = pathname === '/' 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/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/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 b9697f17b..dcafbcf85 100755 --- a/scripts/fetch-decals.sh +++ b/scripts/fetch-decals.sh @@ -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/tools/eval/ARCH.md b/tools/eval/ARCH.md index aa10a2e21..e8d932ec1 100644 --- a/tools/eval/ARCH.md +++ b/tools/eval/ARCH.md @@ -3,7 +3,7 @@ <!-- BEGIN:GERADO — não edite à mão, rode `npm run arch` --> > Gerado por `node tools/gen-arch.mjs`. **Não edite este bloco à mão.** -> Versão do jogo: 2.0.0-alpha.175 · `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 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=<nome>] + ============================================================================ */ +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/deploy-gate-check.mjs b/tools/eval/deploy-gate-check.mjs new file mode 100644 index 000000000..6b5e5716b --- /dev/null +++ b/tools/eval/deploy-gate-check.mjs @@ -0,0 +1,75 @@ +#!/usr/bin/env node +/* ============================================================================ + deploy-gate-check.mjs — QUEM BLOQUEIA O PR É O CI, NÃO A VERCEL + ---------------------------------------------------------------------------- + MEDIDO EM 21/08/2026: Vercel vermelha em 13 dos 14 PRs vindos de fork, sempre + com `Authorization required to deploy.` - a proteção de fork da própria Vercel, + que não se resolve por commit nenhum. E em 4 PRs de casa o vermelho era build + real, uma vez por `fetch-decals.sh` não ter baixado o acervo (196 de 197 + decalques faltando) num soluço de rede. + + Para poder tirar a Vercel do caminho crítico do PR, duas coisas precisam valer: + + DG1 · o CI constrói o site no próprio PR. Sem isso, tirar a Vercel do bloqueio + deixaria o PR sem NENHUMA prova de que o site compila. + DG2 · nenhum download do caminho do build corre sem retry. `set -e` mais um + `curl` sem `--retry` transforma soluço de rede em PR vermelho, e o autor + não tem o que consertar - é o mesmo dano do portão que não pode medir. + DG3 · o preview de fork roda sem pedir aprovação a ninguém, porque quem compila + não tem segredo e quem tem segredo não compila. Os dois arquivos existem e o + contrato deles é guardado em detalhe pelo `scripts/ci/workflow_security_check.py` + (PRV1/PRV2/PRV3); aqui só se confere que o par não sumiu. + + Uso: node tools/eval/deploy-gate-check.mjs [--mutante=<nome>] + ============================================================================ */ +import { readFileSync, readdirSync } from 'node:fs'; + +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +const MUTANTES = { 'ci-sem-build': 'DG1', 'download-sem-retry': 'DG2', 'preview-sem-par': 'DG3' }; +if (MUT && !MUTANTES[MUT]) { console.error(`mutante desconhecido: ${MUT}`); process.exit(2); } + +const ler = (p) => { try { return readFileSync(p, 'utf8'); } catch { return ''; } }; +const falhas = []; + +/* ---- DG1: o CI constrói no PR ---- */ +let ci = ler('.github/workflows/ci.yml'); +if (MUT === 'ci-sem-build') ci = ci.replace('run: npm run build', 'run: echo pulei o build'); +const on = (ci.match(/^on:\n([\s\S]*?)(?=^\S)/m) || [])[1] || ''; +if (!/^\s{2}pull_request:/m.test(on) || !/run:\s*npm run build/.test(ci)) { + falhas.push('DG1 ci.yml não constrói o site em pull_request — sem isso a Vercel não pode sair do bloqueio'); +} + +/* ---- DG2: download do build com retry ---- */ +const FETCHERS = readdirSync('scripts').filter((f) => /^fetch-.*\.sh$/.test(f)); +for (const f of FETCHERS) { + let sh = ler(`scripts/${f}`); + if (MUT === 'download-sem-retry' && f === 'fetch-decals.sh') { + sh = sh.replace(/curl --retry \d+[^"]*-fsSL/, 'curl -fsSL'); + } + for (const linha of sh.split('\n')) { + if (!/^\s*curl\s/.test(linha)) continue; + if (!/--retry\b/.test(linha)) { + falhas.push(`DG2 scripts/${f}: curl sem --retry no caminho do build — soluço de rede vira PR vermelho`); + } + } +} +if (!FETCHERS.length) falhas.push('DG2 nenhum scripts/fetch-*.sh encontrado — a régua perdeu o alvo'); + +/* ---- DG3: a etiqueta do preview nasce com quem a usa ---- */ +let preview = ler('.github/workflows/preview-bot.yml'); +if (MUT === 'preview-sem-par') preview = preview.replace(/ensure_labels\.py preview-autorizado/, 'ensure_labels.py'); +const temBuild = ler('.github/workflows/preview-build.yml'); +const temDeploy = ler('.github/workflows/preview-deploy.yml'); +if (MUT === 'preview-sem-par') { /* nome antigo do mutante: agora derruba o par */ } +const par = (MUT === 'preview-sem-par') ? '' : (temBuild && temDeploy); +if (!par) { + falhas.push('DG3 falta preview-build.yml ou preview-deploy.yml — sem o par, preview de fork volta a depender de aprovação humana'); +} + +for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); +if (!falhas.length) console.log(` \x1b[32m✓\x1b[0m DG o PR é bloqueado pelo build do CI; download tem retry (${FETCHERS.length} fetchers); preview de fork sem aprovação`); +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/fixture-check.mjs b/tools/eval/fixture-check.mjs index 987f43be2..a10fd1b42 100644 --- a/tools/eval/fixture-check.mjs +++ b/tools/eval/fixture-check.mjs @@ -35,7 +35,6 @@ const base = arg('base') || 'origin/main'; /* Dívida herdada: script de decisão que nasceu sem fixture. A lista só encolhe. Quem tocar num destes escreve o `--selftest` no mesmo PR. */ const SEM_FIXTURE = new Set([ - 'scripts/ci/ensure_labels.py', 'scripts/ci/issue_triage.py', 'scripts/ci/pr_classify.py', 'scripts/ci/pr_route.py', diff --git a/tools/eval/portao-inteiro-check.mjs b/tools/eval/portao-inteiro-check.mjs new file mode 100644 index 000000000..b0b361e8f --- /dev/null +++ b/tools/eval/portao-inteiro-check.mjs @@ -0,0 +1,68 @@ +#!/usr/bin/env node +/* ============================================================================ + portao-inteiro-check.mjs — PORTÃO NÃO PODE CITAR PASSO QUE NÃO EXISTE + ---------------------------------------------------------------------------- + `check:fast` e `check:deploy` são listas de nomes de script. Se um nome citado + ali não está definido em `scripts`, o npm morre com "missing script" e o portão + inteiro para no meio - os passos DEPOIS do nome quebrado nunca rodam. + + DOIS CASOS REAIS, os dois em 22/08/2026: + · o PR #372 citava `eval:gelo` no `check:fast` e nunca definiu o script. A régua + `treta-gelo-check.mjs` existia e simplesmente não estava ligada em lugar nenhum; + · resolvendo o conflito de `package.json` do #408, eu fiquei com a linha do + `check:` de um lado e perdi as DEFINIÇÕES do outro. Três réguas viraram nome + órfão de uma vez - e conflito de `package.json` é justamente o que mais + acontece, porque todo PR que acrescenta régua mexe na mesma linha. + + PI1 · todo nome citado em `check:fast`/`check:deploy` existe em `scripts`. + PI2 · todo script `eval:*` aponta para arquivo que existe em disco. + + Uso: node tools/eval/portao-inteiro-check.mjs [--mutante=<nome>] + ============================================================================ */ +import { readFileSync, existsSync } from 'node:fs'; + +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +const MUTANTES = { 'passo-orfao': 'PI1', 'regua-fantasma': 'PI2' }; +if (MUT && !MUTANTES[MUT]) { console.error(`mutante desconhecido: ${MUT}`); process.exit(2); } + +const pkg = JSON.parse(readFileSync('package.json', 'utf8')); +const scripts = { ...pkg.scripts }; +if (MUT === 'passo-orfao') scripts['check:fast'] += ' eval:nao-existe'; +if (MUT === 'regua-fantasma') scripts['eval:fantasma'] = 'node tools/eval/nao-existe-check.mjs'; + +const falhas = []; +const PORTOES = ['check:fast', 'check:deploy']; + +/* ---- PI1: nome citado tem de estar definido ---- */ +for (const portao of PORTOES) { + const linha = scripts[portao]; + if (!linha) { falhas.push(`PI1 o portão \`${portao}\` não existe em scripts`); continue; } + /* A linha é `node tools/eval/runner.mjs a b c`: passo é token com `:` que não é + caminho. `syntax` é o único passo sem `:` e está definido como script. */ + const passos = linha.split(/\s+/).slice(2).filter((t) => t && !t.includes('/')); + for (const passo of passos) { + if (!scripts[passo]) { + falhas.push(`PI1 ${portao} cita \`${passo}\`, que não está definido em scripts — o npm morre em "missing script" e os passos seguintes NÃO rodam`); + } + } +} + +/* ---- PI2: régua citada tem de existir em disco ---- */ +for (const [nome, cmd] of Object.entries(scripts)) { + if (nome.startsWith('//')) continue; // chave de documentação, não é script + const m = String(cmd).match(/node\s+(tools\/[^\s]+\.mjs)/); + if (m && !existsSync(m[1])) { + falhas.push(`PI2 \`${nome}\` aponta para ${m[1]}, que não existe em disco`); + } +} + +for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); +if (!falhas.length) { + const total = PORTOES.reduce((n, p) => n + (scripts[p] || '').split(/\s+/).slice(2).filter((t) => t && !t.includes('/')).length, 0); + console.log(` \x1b[32m✓\x1b[0m PI todo passo dos dois portões está definido e aponta para arquivo que existe (${total} citações)`); +} +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 (MC1) +} +process.exit(falhas.length ? 1 : 0); diff --git a/tools/eval/seo-check.mjs b/tools/eval/seo-check.mjs index 62f6fd98d..3a32b0a08 100644 --- a/tools/eval/seo-check.mjs +++ b/tools/eval/seo-check.mjs @@ -55,8 +55,7 @@ const JSON_OUT = process.argv.includes('--json'); const SITE = 'https://www.csbrasil.online'; /** Páginas prerenderizadas que o build produz. `/ranking`, `/mapa` e `/u/*` são * SSR e não existem aqui — o que o crawler vê nelas é medido em runtime. */ -const PAGES = ['/', '/armas', '/mapas', '/personagens', '/como-jogar', '/sobre', '/changelog', - '/how-to-play', '/maps', '/weapons', '/characters', '/about', '/whats-new']; +const PAGES = ['/', '/armas', '/mapas', '/personagens', '/como-jogar', '/sobre', '/changelog']; /* Afirmações que só podem aparecer com RANKING_ON=true. São recortes de frase, não palavras soltas: "ranking" sozinho é legítimo (o menu RANKING do jogo diff --git a/tools/eval/site-smoke.mjs b/tools/eval/site-smoke.mjs index e0b79c7a3..ffedfde76 100644 --- a/tools/eval/site-smoke.mjs +++ b/tools/eval/site-smoke.mjs @@ -59,7 +59,7 @@ const BASE = EXTERNO || `http://localhost:${PORTA}`; // --------------------------------------------------------------------------- // 8 páginas indexáveis hoje; /ranking entra na conta só com o ranking ligado — // desligado ele é noindex (ranking.astro), e noindex no sitemap é contradição. -const XML_LOC_MIN = RANKING_ON ? 15 : 14; +const XML_LOC_MIN = RANKING_ON ? 9 : 8; const CONTRATO = [ { rota: '/', esperado: '200 + <title> com "CORO SOLTO"', @@ -95,8 +95,7 @@ const CONTRATO = [ `public/js` existe e o defeito não pode acontecer. Quem mede o artefato publicado é `npm run eval:ssr`. Esta linha pega a próxima página que quebrar por outro motivo; ela não teria pego aquela. */ - ...['/como-jogar', '/mapas', '/mapa', '/armas', '/personagens', '/sobre', '/changelog', - '/how-to-play', '/maps', '/weapons', '/characters', '/about', '/whats-new'].map((rota) => ({ + ...['/como-jogar', '/mapas', '/mapa', '/armas', '/personagens', '/sobre', '/changelog'].map((rota) => ({ rota, esperado: '200 + corpo não vazio', checa: ({ status, corpo }) => { if (status !== 200) return `status ${status}`; @@ -111,7 +110,6 @@ const CONTRATO = [ if (!/^\s*<\?xml/.test(corpo)) return 'não começa com declaração XML'; const locs = corpo.match(/<loc>/g) || []; if (locs.length < XML_LOC_MIN) return `${locs.length} <loc>, mínimo ${XML_LOC_MIN}`; - if (!corpo.includes('/whats-new')) return 'sitemap sem a gêmea EN /whats-new'; // XML mal formado é o modo de falha silenciosa aqui: um & solto derruba o // parse de quem consome e o status continua 200. const abertas = (corpo.match(/<url>/g) || []).length; @@ -153,8 +151,7 @@ const CONTRATO = [ // Páginas que precisam ter todo bloco JSON-LD parseável. JSON-LD quebrado é // invisível: some do resultado rico e ninguém percebe por meses. -const PAGINAS_JSONLD = ['/', '/ranking', '/como-jogar', '/mapas', '/armas', '/personagens', '/sobre', '/changelog', - '/how-to-play', '/maps', '/weapons', '/characters', '/about', '/whats-new']; +const PAGINAS_JSONLD = ['/', '/ranking', '/como-jogar', '/mapas', '/armas', '/personagens', '/sobre', '/changelog']; // --------------------------------------------------------------------------- const RE_JSONLD = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi; diff --git a/tools/eval/workflow-secret-check.mjs b/tools/eval/workflow-secret-check.mjs new file mode 100644 index 000000000..37254ec04 --- /dev/null +++ b/tools/eval/workflow-secret-check.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node +/* ============================================================================ + workflow-secret-check.mjs — PORTÃO QUE NÃO PODE MEDIR NÃO VOTA + ---------------------------------------------------------------------------- + MEDIDO EM 21/08/2026, nos últimos 29 PRs: `classify` vermelho em 11 dos 14 PRs + vindos de FORK e em 0 dos 15 de casa. A causa não era o código de ninguém. + + A regra do GitHub: um workflow disparado por `pull_request`, `pull_request_review` + ou `pull_request_review_comment` a partir de um FORK roda SEM os secrets do + repositório. Só `pull_request_target`, `workflow_dispatch`, `schedule` e + `workflow_run` correm no contexto base e enxergam secret. + + Um job que (a) escuta um desses gatilhos cegos e (b) sai com erro quando o secret + está vazio, reprova todo PR externo por um motivo que o autor não tem como + consertar - e ainda ensina o time a ignorar vermelho, que é o dano caro. + + O QUE ESTA RÉGUA EXIGE: se o job pode ser disparado por gatilho cego, ele precisa + de um `if:` que o exclua desse caso. O guard de secret continua podendo existir e + continuar falhando alto onde o secret DEVERIA estar lá. + + Uso: node tools/eval/workflow-secret-check.mjs [--mutante=<nome>] + ============================================================================ */ +import { readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; + +const DIR = '.github/workflows'; +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +const MUTANTES = { 'guard-sem-if': 'WSEC1', 'gatilho-cego-novo': 'WSEC1' }; +if (MUT && !MUTANTES[MUT]) { console.error(`mutante desconhecido: ${MUT}`); process.exit(2); } + +/* Gatilhos que, num PR de fork, chegam sem secret. `pull_request` entra na lista + porque é o caso clássico; os outros dois foram os que sangraram aqui. */ +const CEGOS = ['pull_request', 'pull_request_review', 'pull_request_review_comment']; + +const arquivos = readdirSync(DIR).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml')); +const falhas = []; + +for (const nome of arquivos) { + let texto = readFileSync(join(DIR, nome), 'utf8'); + if (MUT === 'guard-sem-if' && nome === 'csbrasil-bot-pr-classify.yml') { + texto = texto.replace(/ if: >-\n( github\.event_name[^\n]*\n)+/, ''); + } + if (MUT === 'gatilho-cego-novo' && nome === 'preview-bot.yml') { + texto = texto.replace(' pull_request_target:', ' pull_request_review:\n types: [submitted]\n pull_request_target:'); + } + + /* O bloco `on:` vai até a primeira chave de topo seguinte (coluna zero). Ler o + arquivo inteiro daria falso positivo com `pull_request` citado em comentário + dentro de um step. */ + const mOn = texto.match(/^on:\n([\s\S]*?)(?=^\S)/m); + if (!mOn) continue; + const bloco = mOn[1]; + const cegos = CEGOS.filter((g) => new RegExp(`^\\s{2}${g}:`, 'm').test(bloco)); + if (!cegos.length) continue; + + /* Guard de secret = o job aborta quando a variável de secret está vazia. */ + const temGuard = /-z "\$GH_TOKEN"|-z "\$\{?[A-Z_]*TOKEN/.test(texto) && /exit 1/.test(texto); + if (!temGuard) continue; + + /* A defesa aceita: um `if:` de job que amarre o event_name ou compare o repo da + head com o repositório - as duas formas de dizer "aqui eu tenho secret". */ + const temDefesa = /if:[\s\S]{0,400}?(github\.event_name\s*==|head\.repo\.full_name\s*==)/.test(texto); + if (!temDefesa) { + falhas.push(`WSEC1 ${nome}: guard de secret + gatilho cego (${cegos.join(', ')}) sem \`if:\` que exclua o caso de fork`); + } +} + +for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); +if (!falhas.length) console.log(` \x1b[32m✓\x1b[0m WSEC1 nenhum portão reprova PR de fork por secret que aquele gatilho não pode ter (${arquivos.length} workflows)`); +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); From ac9743288231b50dcac9f4014e7eef0c3e546e9d Mon Sep 17 00:00:00 2001 From: csbrasil-bot <csbrasil-bot@users.noreply.github.com> Date: Sat, 22 Aug 2026 04:18:08 +0000 Subject: [PATCH 06/14] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Signed-off-by: csbrasil-bot <csbrasil-bot@users.noreply.github.com> --- ARCH.generated.md | 6 +++--- README.md | 4 ++-- STATUS.md | 2 +- docs/docs/comecando.md | 8 ++++---- docs/docs/quality-gates.md | 2 +- .../docusaurus-plugin-content-docs/current/comecando.md | 8 ++++---- .../current/quality-gates.md | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ARCH.generated.md b/ARCH.generated.md index ff40fa804..a7b9c8635 100644 --- a/ARCH.generated.md +++ b/ARCH.generated.md @@ -10,7 +10,7 @@ Números atuais das zonas, do quality gate e do `package.json`: |---|---|---|---| | `public/` | o **jogo** | 46 arquivos `.js`, 32.430 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | | `src/` | o **site** | 19 páginas `.astro`, 21 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | -| `tools/` | o **arnês** | 199 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | +| `tools/` | o **arnês** | 203 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | **Não existe `public/index.html`.** O HTML do jogo é `src/pages/index.astro`, servido na rota `/`. Servir `public/` estaticamente entrega os arnêses visuais, **não o jogo** — é a pegadinha que custa a primeira hora de todo mundo. @@ -25,10 +25,10 @@ Comandos e scripts atuais do quality gate: <!-- BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` --> ```bash -npm run 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:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam +npm run 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 ``` -`package.json` tem **121 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. +`package.json` tem **124 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/README.md b/README.md index 180163f30..ee4bed4ae 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,10 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 199 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.175` | `public/js/version.js` e `package.json` (batem) | +| Versão | `2.0.0-alpha.175` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.176`** | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` diff --git a/STATUS.md b/STATUS.md index f593823a5..1f0131223 100644 --- a/STATUS.md +++ b/STATUS.md @@ -5,7 +5,7 @@ - **Versão:** `2.0.0-alpha.175` - **Conteúdo jogável:** 5 facções, 44 personagens, 12 mapas e 26 armas com GLB - **Código do jogo:** 32.430 linhas em 46 módulos JavaScript -- **Automação:** 121 comandos npm, 199 scripts de avaliação e 54 scripts de pipeline +- **Automação:** 124 comandos npm, 203 scripts de avaliação e 54 scripts de pipeline > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `package.json · CHARACTERS · MAPS · public/models/weapons · public/js · tools/` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index b18b2bb5c..389138d7b 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -49,10 +49,10 @@ esta página envelhecia no primeiro commit — ver | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 199 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.175` | `public/js/version.js` e `package.json` (batem) | +| Versão | `2.0.0-alpha.175` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.176`** | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` @@ -275,10 +275,10 @@ E os dois quality gates, com a lista exata do que cada um roda — direto do `pa {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -npm run 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:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam +npm run 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 ``` -`package.json` tem **121 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. +`package.json` tem **124 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/docs/docs/quality-gates.md b/docs/docs/quality-gates.md index 4160f517a..ed9184f1c 100644 --- a/docs/docs/quality-gates.md +++ b/docs/docs/quality-gates.md @@ -15,7 +15,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2.275 linhas**, **65 identificadores de invariante declarados** (`put()`), dos quais **28** têm caminho de `skip()` declarado. -- O arnês inteiro são **199 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **54 scripts** de pipeline em `tools/`. +- O arnês inteiro são **203 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **54 scripts** de pipeline em `tools/`. - Quantas invariantes rodam como **críticas** numa execução **não é derivável do fonte**: depende de qual insumo existe na máquina (o JSON do auditor de viewmodel, um GLB, uma pasta de anims). Esse número só sai rodando o quality gate — e o lugar dele é o cabeçalho do `KNOWN-BUGS.md`, atualizado com saída real. Reproduza: diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index 5e2188c69..6dcc43c46 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -51,10 +51,10 @@ this page was aging at the very first commit — see | Playable characters | 44, in 5 factions | `CHARACTERS` array in `characters.js` | | Maps in the registry | 12 | `MAPS` object in `maps.js` | | Visual harnesses in HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Harness scripts | 199 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Harness scripts | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Written entry tasks | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Version | `2.0.0-alpha.175` | `public/js/version.js` and `package.json` (match) | +| Version | `2.0.0-alpha.175` | `public/js/version.js` and `package.json` **DIVERGE** | > Block generated by `node tools/gen-docs.mjs`. Source: `the command in the right column of each row` @@ -277,10 +277,10 @@ And the two gates, with the exact list of what each one runs — straight from ` {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -npm run 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:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam +npm run 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 ``` -`package.json` has **121 scripts**; the reason behind each one lives in `SCRIPTS.md`. +`package.json` has **124 scripts**; the reason behind each one lives in `SCRIPTS.md`. > Block generated by `node tools/gen-docs.mjs`. Source: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md index b3fcef1e0..c92fb4a2a 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md @@ -18,7 +18,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2,275 lines**, **65 declared invariant identifiers**, with **28** declared `skip()` paths. -- The harness contains **199 scripts** in `tools/eval/`, plus **54 pipeline scripts** in `tools/`. +- The harness contains **203 scripts** in `tools/eval/`, plus **54 pipeline scripts** in `tools/`. - The number of critical checks in one run depends on the inputs present on that machine; dated results belong in `KNOWN-BUGS.md`. ```bash From 84a281670068213aed8f72627194225889891419 Mon Sep 17 00:00:00 2001 From: csbrasil-bot <csbrasil-bot@users.noreply.github.com> Date: Sat, 22 Aug 2026 04:20:06 +0000 Subject: [PATCH 07/14] =?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 <csbrasil-bot@users.noreply.github.com> --- package-lock.json | 4 ++-- package.json | 2 +- tools/eval/ARCH.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e25564558..3d6977d77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.176", + "version": "2.0.0-alpha.177", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coro-solto", - "version": "2.0.0-alpha.176", + "version": "2.0.0-alpha.177", "license": "AGPL-3.0-only", "dependencies": { "@astrojs/vercel": "^11.0.6", diff --git a/package.json b/package.json index 29eebdad5..c574a47b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.176", + "version": "2.0.0-alpha.177", "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": { diff --git a/tools/eval/ARCH.md b/tools/eval/ARCH.md index e8d932ec1..cb96bc69f 100644 --- a/tools/eval/ARCH.md +++ b/tools/eval/ARCH.md @@ -3,7 +3,7 @@ <!-- BEGIN:GERADO — não edite à mão, rode `npm run arch` --> > Gerado por `node tools/gen-arch.mjs`. **Não edite este bloco à mão.** -> Versão do jogo: 2.0.0-alpha.176 · `npm run arch` para regenerar · `npm run arch:check` no CI. +> Versão do jogo: 2.0.0-alpha.177 · `npm run arch` para regenerar · `npm run arch:check` no CI. ## Tamanho dos arquivos indexados From ea4f19dae80d11abb8bcee00ae45b8e3a8897b9c Mon Sep 17 00:00:00 2001 From: csbrasil-bot <csbrasil-bot@users.noreply.github.com> Date: Sat, 22 Aug 2026 04:20:06 +0000 Subject: [PATCH 08/14] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Signed-off-by: csbrasil-bot <csbrasil-bot@users.noreply.github.com> --- README.md | 2 +- docs/docs/comecando.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee4bed4ae..700f32bfe 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Scripts do arnês | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.175` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.176`** | +| Versão | `2.0.0-alpha.175` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.177`** | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 389138d7b..0f126c3f5 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -52,7 +52,7 @@ esta página envelhecia no primeiro commit — ver | Scripts do arnês | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.175` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.176`** | +| Versão | `2.0.0-alpha.175` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.177`** | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` From 1c6bfc35f169c95b0111745aa76323ce87f30d40 Mon Sep 17 00:00:00 2001 From: csbrasil-bot <csbrasil-bot@users.noreply.github.com> Date: Sat, 22 Aug 2026 04:20:45 +0000 Subject: [PATCH 09/14] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Signed-off-by: csbrasil-bot <csbrasil-bot@users.noreply.github.com> --- ARCH.generated.md | 6 +++--- README.md | 8 ++++---- STATUS.md | 4 ++-- docs/docs/colaborar.md | 2 +- docs/docs/comecando.md | 4 ++-- docs/docs/quality-gates.md | 2 +- .../docusaurus-plugin-content-docs/current/colaborar.md | 2 +- .../docusaurus-plugin-content-docs/current/comecando.md | 4 ++-- .../current/quality-gates.md | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/ARCH.generated.md b/ARCH.generated.md index e23771a09..a7b9c8635 100644 --- a/ARCH.generated.md +++ b/ARCH.generated.md @@ -8,9 +8,9 @@ Números atuais das zonas, do quality gate e do `package.json`: | Zona | O que é | Tamanho medido | Regra | |---|---|---|---| -| `public/` | o **jogo** | 44 arquivos `.js`, 32.001 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | -| `src/` | o **site** | 18 páginas `.astro`, 21 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | -| `tools/` | o **arnês** | 200 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | +| `public/` | o **jogo** | 46 arquivos `.js`, 32.430 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | +| `src/` | o **site** | 19 páginas `.astro`, 21 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | +| `tools/` | o **arnês** | 203 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | **Não existe `public/index.html`.** O HTML do jogo é `src/pages/index.astro`, servido na rota `/`. Servir `public/` estaticamente entrega os arnêses visuais, **não o jogo** — é a pegadinha que custa a primeira hora de todo mundo. diff --git a/README.md b/README.md index 391db7ab2..a9e19dffc 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| Código do jogo | 32.430 linhas em 46 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | | `game.js` | **6.910** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | @@ -44,7 +44,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Versão | `2.0.0-alpha.177` | `public/js/version.js` e `package.json` (batem) | @@ -89,7 +89,7 @@ arquitetura): `cd docs && npm install && npm start` → <http://localhost:3000/d | Camada | Ferramenta | Versão | |---|---|---| | Motor 3D (WebGL) | **Three.js**, vendorizado | `r160` | -| Jogo | ES modules vanilla, **zero build** | 44 arquivos | +| Jogo | ES modules vanilla, **zero build** | 46 arquivos | | Site | **Astro** com SSR | `^7.1.1` | | Hospedagem | adapter **Vercel** | `^11.0.6` | | Banco | **Postgres gerenciado** (RLS; schema privado, fora do repo) | `^2.110.7` | @@ -100,7 +100,7 @@ arquitetura): `cd docs && npm install && npm start` → <http://localhost:3000/d | Esta documentação | **Docusaurus** | `3.6.3` | | Runtime de CI | **Node** | `22` | -Three.js sai de `public/vendor/three.module.js` (**sem CDN, sem npm no runtime**). Astro e Vercel de `package.json` + `astro.config.mjs` + `vercel.json`. Dos scripts de `tools/`, **110** importam Playwright, **37** importam gltf-transform e **4** importam meshoptimizer. +Three.js sai de `public/vendor/three.module.js` (**sem CDN, sem npm no runtime**). Astro e Vercel de `package.json` + `astro.config.mjs` + `vercel.json`. Dos scripts de `tools/`, **111** importam Playwright, **37** importam gltf-transform e **4** importam meshoptimizer. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js` diff --git a/STATUS.md b/STATUS.md index 40aeac98b..da5ea5ba8 100644 --- a/STATUS.md +++ b/STATUS.md @@ -4,8 +4,8 @@ - **Versão:** `2.0.0-alpha.177` - **Conteúdo jogável:** 5 facções, 44 personagens, 12 mapas e 26 armas com GLB -- **Código do jogo:** 32.001 linhas em 44 módulos JavaScript -- **Automação:** 124 comandos npm, 200 scripts de avaliação e 54 scripts de pipeline +- **Código do jogo:** 32.430 linhas em 46 módulos JavaScript +- **Automação:** 124 comandos npm, 203 scripts de avaliação e 54 scripts de pipeline > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `package.json · CHARACTERS · MAPS · public/models/weapons · public/js · tools/` diff --git a/docs/docs/colaborar.md b/docs/docs/colaborar.md index 2d5b3e074..960b6cb7e 100644 --- a/docs/docs/colaborar.md +++ b/docs/docs/colaborar.md @@ -14,7 +14,7 @@ O número abaixo não é retórica, e não é escrito à mão: sai de `git short {/* BEGIN:GERADO:pessoas — não edite à mão, rode `npm run docs` */} -**11 identidades de autoria humana** assinam commit no histórico **desta branch**: `ruben-cytonic`, `Ruben`, `Emerson Garrido`, `rubenmarcus`, `Ruben Marcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. O resto dos commits é assinado por agentes de IA. Branch não é repositório: quem contribuiu num ramo que esta branch não contém **não aparece aqui**. +**11 identidades de autoria humana** assinam commit no histórico **desta branch**: `ruben-cytonic`, `Ruben`, `Emerson Garrido`, `rubenmarcus`, `Ruben Marcus`, `William Oliveira`, `Juan Versolato Lopes`, `Maná Soares`, `daeeseD`, `matheusgb`, `daltonfontes`. O resto dos commits é assinado por agentes de IA. Branch não é repositório: quem contribuiu num ramo que esta branch não contém **não aparece aqui**. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `git shortlog -sn --no-merges (descontando autores que são agentes)` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 8c9d253c4..03c1b3414 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -39,7 +39,7 @@ esta página envelhecia no primeiro commit — ver | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| Código do jogo | 32.430 linhas em 46 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | | `game.js` | **6.910** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | @@ -49,7 +49,7 @@ esta página envelhecia no primeiro commit — ver | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Versão | `2.0.0-alpha.177` | `public/js/version.js` e `package.json` (batem) | diff --git a/docs/docs/quality-gates.md b/docs/docs/quality-gates.md index e1fad825a..ed9184f1c 100644 --- a/docs/docs/quality-gates.md +++ b/docs/docs/quality-gates.md @@ -15,7 +15,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2.275 linhas**, **65 identificadores de invariante declarados** (`put()`), dos quais **28** têm caminho de `skip()` declarado. -- O arnês inteiro são **200 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **54 scripts** de pipeline em `tools/`. +- O arnês inteiro são **203 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **54 scripts** de pipeline em `tools/`. - Quantas invariantes rodam como **críticas** numa execução **não é derivável do fonte**: depende de qual insumo existe na máquina (o JSON do auditor de viewmodel, um GLB, uma pasta de anims). Esse número só sai rodando o quality gate — e o lugar dele é o cabeçalho do `KNOWN-BUGS.md`, atualizado com saída real. Reproduza: diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md index ad72a3a1d..609e0f910 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md @@ -17,7 +17,7 @@ The number below is not rhetoric, and it is not hand-written: it comes from `git {/* BEGIN:GERADO:pessoas — não edite à mão, rode `npm run docs` */} -**11 human author identities** sign commits in this branch: `ruben-cytonic`, `Ruben`, `Emerson Garrido`, `rubenmarcus`, `Ruben Marcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. Automated identities are excluded. A Git author name is not necessarily one unique person. +**11 human author identities** sign commits in this branch: `ruben-cytonic`, `Ruben`, `Emerson Garrido`, `rubenmarcus`, `Ruben Marcus`, `William Oliveira`, `Juan Versolato Lopes`, `Maná Soares`, `daeeseD`, `matheusgb`, `daltonfontes`. Automated identities are excluded. A Git author name is not necessarily one unique person. > Block generated by `node tools/gen-docs.mjs`. Source: `git shortlog -sn --no-merges (descontando autores que são agentes)` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index 888a3d9ae..d63f6d58c 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -41,7 +41,7 @@ this page was aging at the very first commit — see | What | How much | Where to check | |---|---:|---| -| Game code | 32,001 lines in 44 files | `git ls-files public/js/*.js \| xargs wc -l` | +| Game code | 32,430 lines in 46 files | `git ls-files public/js/*.js \| xargs wc -l` | | `game.js` | **6,910** lines | `wc -l public/js/game.js` | | `main.js` | 2,698 lines | `wc -l public/js/main.js` | | Weapons with GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | @@ -51,7 +51,7 @@ this page was aging at the very first commit — see | Playable characters | 44, in 5 factions | `CHARACTERS` array in `characters.js` | | Maps in the registry | 12 | `MAPS` object in `maps.js` | | Visual harnesses in HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Harness scripts | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Harness scripts | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Written entry tasks | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Version | `2.0.0-alpha.177` | `public/js/version.js` and `package.json` (match) | diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md index 53b1f97a3..c92fb4a2a 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md @@ -18,7 +18,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2,275 lines**, **65 declared invariant identifiers**, with **28** declared `skip()` paths. -- The harness contains **200 scripts** in `tools/eval/`, plus **54 pipeline scripts** in `tools/`. +- The harness contains **203 scripts** in `tools/eval/`, plus **54 pipeline scripts** in `tools/`. - The number of critical checks in one run depends on the inputs present on that machine; dated results belong in `KNOWN-BUGS.md`. ```bash From b0cd97704ff3574850fb4a6aa4aa3f75ed232ec6 Mon Sep 17 00:00:00 2001 From: csbrasil-bot <csbrasil-bot@users.noreply.github.com> Date: Sat, 22 Aug 2026 05:50:20 +0000 Subject: [PATCH 10/14] =?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 <csbrasil-bot@users.noreply.github.com> --- tools/eval/autofix-check.mjs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tools/eval/autofix-check.mjs b/tools/eval/autofix-check.mjs index ec57c4434..c60da58a7 100644 --- a/tools/eval/autofix-check.mjs +++ b/tools/eval/autofix-check.mjs @@ -17,6 +17,10 @@ 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. + AF7 · todo commit que o bot faz carrega os trailers que o CI cobra de humano: + `Agent:` (agente_check) e `Signed-off-by:` (dco_check). Sem eles o bot conserta + o PR e reprova o mesmo PR no passo seguinte - foi o que aconteceu no primeiro + disparo real, em 22/08. 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 @@ -40,6 +44,7 @@ const MUTANTES = { 'automerge-volta': 'AF4', 'resolve-conflito-de-codigo': 'AF5', 'sem-varredura-pos-release': 'AF6', + 'commit-sem-trailer': 'AF7', }; if (MUT && !MUTANTES[MUT]) { console.error(`mutante desconhecido: ${MUT}`); process.exit(2); } @@ -109,6 +114,14 @@ if (resolveConflito) { } } +/* ---- AF7: os commits do bot levam os trailers que o CI cobra ---- */ +if (MUT === 'commit-sem-trailer') wf = wf.replace(/\n\s*-m "Agent: csbrasil-bot \(autofix\)" \\/g, ''); +const commits = [...wf.matchAll(/git commit[\s\S]{0,600}?(?=\n\s{0,10}[a-z-]+:|\n\s*$)/g)].map((m) => m[0]); +for (const c of commits) { + if (!/Agent:/.test(c)) falhas.push('AF7 um `git commit` do bot não leva o trailer `Agent:` — o agente_check reprova o PR que ele acabou de consertar'); + if (!/Signed-off-by:/.test(c)) falhas.push('AF7 um `git commit` do bot não leva `Signed-off-by:` — o dco reprova o PR que ele acabou de consertar'); +} + /* ---- 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); From d28f4a32439403ea34541db5b80e7c9459fa1914 Mon Sep 17 00:00:00 2001 From: csbrasil-bot <csbrasil-bot@users.noreply.github.com> Date: Sat, 22 Aug 2026 05:51:00 +0000 Subject: [PATCH 11/14] =?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 <csbrasil-bot@users.noreply.github.com> --- package-lock.json | 4 ++-- package.json | 2 +- tools/eval/ARCH.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d6977d77..30c2652b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.177", + "version": "2.0.0-alpha.178", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coro-solto", - "version": "2.0.0-alpha.177", + "version": "2.0.0-alpha.178", "license": "AGPL-3.0-only", "dependencies": { "@astrojs/vercel": "^11.0.6", diff --git a/package.json b/package.json index c574a47b5..efd435237 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.177", + "version": "2.0.0-alpha.178", "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": { diff --git a/tools/eval/ARCH.md b/tools/eval/ARCH.md index cb96bc69f..d1020873f 100644 --- a/tools/eval/ARCH.md +++ b/tools/eval/ARCH.md @@ -3,7 +3,7 @@ <!-- BEGIN:GERADO — não edite à mão, rode `npm run arch` --> > Gerado por `node tools/gen-arch.mjs`. **Não edite este bloco à mão.** -> Versão do jogo: 2.0.0-alpha.177 · `npm run arch` para regenerar · `npm run arch:check` no CI. +> Versão do jogo: 2.0.0-alpha.178 · `npm run arch` para regenerar · `npm run arch:check` no CI. ## Tamanho dos arquivos indexados From d5544f05aba1ec1bf639e875fe8623138aa28b02 Mon Sep 17 00:00:00 2001 From: csbrasil-bot <csbrasil-bot@users.noreply.github.com> Date: Sat, 22 Aug 2026 05:51:00 +0000 Subject: [PATCH 12/14] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Agent: csbrasil-bot (autofix) Signed-off-by: csbrasil-bot <csbrasil-bot@users.noreply.github.com> --- README.md | 2 +- docs/docs/comecando.md | 2 +- .../i18n/en/docusaurus-plugin-content-docs/current/comecando.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a9e19dffc..0fd61a4b3 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Scripts do arnês | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.177` | `public/js/version.js` e `package.json` (batem) | +| Versão | `2.0.0-alpha.177` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.178`** | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 03c1b3414..8470c470c 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -52,7 +52,7 @@ esta página envelhecia no primeiro commit — ver | Scripts do arnês | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.177` | `public/js/version.js` e `package.json` (batem) | +| Versão | `2.0.0-alpha.177` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.178`** | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index d63f6d58c..1a2618144 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -54,7 +54,7 @@ this page was aging at the very first commit — see | Harness scripts | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Written entry tasks | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Version | `2.0.0-alpha.177` | `public/js/version.js` and `package.json` (match) | +| Version | `2.0.0-alpha.177` | `public/js/version.js` and `package.json` **DIVERGE** | > Block generated by `node tools/gen-docs.mjs`. Source: `the command in the right column of each row` From 89317e1be6c9093bb275e3db20b340891e4a0a1f Mon Sep 17 00:00:00 2001 From: csbrasil-bot <csbrasil-bot@users.noreply.github.com> Date: Sat, 22 Aug 2026 05:52:02 +0000 Subject: [PATCH 13/14] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Agent: csbrasil-bot (autofix) Signed-off-by: csbrasil-bot <csbrasil-bot@users.noreply.github.com> --- README.md | 8 ++++---- docs/docs/comecando.md | 4 ++-- .../docusaurus-plugin-content-docs/current/comecando.md | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f24151e75..ba4fc5ca9 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| Código do jogo | 32.430 linhas em 46 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | | `game.js` | **6.910** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | @@ -44,7 +44,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Versão | `2.0.0-alpha.178` | `public/js/version.js` e `package.json` (batem) | @@ -89,7 +89,7 @@ arquitetura): `cd docs && npm install && npm start` → <http://localhost:3000/d | Camada | Ferramenta | Versão | |---|---|---| | Motor 3D (WebGL) | **Three.js**, vendorizado | `r160` | -| Jogo | ES modules vanilla, **zero build** | 44 arquivos | +| Jogo | ES modules vanilla, **zero build** | 46 arquivos | | Site | **Astro** com SSR | `^7.1.1` | | Hospedagem | adapter **Vercel** | `^11.0.6` | | Banco | **Postgres gerenciado** (RLS; schema privado, fora do repo) | `^2.110.7` | @@ -100,7 +100,7 @@ arquitetura): `cd docs && npm install && npm start` → <http://localhost:3000/d | Esta documentação | **Docusaurus** | `3.6.3` | | Runtime de CI | **Node** | `22` | -Three.js sai de `public/vendor/three.module.js` (**sem CDN, sem npm no runtime**). Astro e Vercel de `package.json` + `astro.config.mjs` + `vercel.json`. Dos scripts de `tools/`, **110** importam Playwright, **37** importam gltf-transform e **4** importam meshoptimizer. +Three.js sai de `public/vendor/three.module.js` (**sem CDN, sem npm no runtime**). Astro e Vercel de `package.json` + `astro.config.mjs` + `vercel.json`. Dos scripts de `tools/`, **111** importam Playwright, **37** importam gltf-transform e **4** importam meshoptimizer. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 60a11d299..2d4bdec4f 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -39,7 +39,7 @@ esta página envelhecia no primeiro commit — ver | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| Código do jogo | 32.430 linhas em 46 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | | `game.js` | **6.910** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | @@ -49,7 +49,7 @@ esta página envelhecia no primeiro commit — ver | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Versão | `2.0.0-alpha.178` | `public/js/version.js` e `package.json` (batem) | diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index 18eac13ad..a309563ae 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -41,7 +41,7 @@ this page was aging at the very first commit — see | What | How much | Where to check | |---|---:|---| -| Game code | 32,001 lines in 44 files | `git ls-files public/js/*.js \| xargs wc -l` | +| Game code | 32,430 lines in 46 files | `git ls-files public/js/*.js \| xargs wc -l` | | `game.js` | **6,910** lines | `wc -l public/js/game.js` | | `main.js` | 2,698 lines | `wc -l public/js/main.js` | | Weapons with GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | @@ -51,7 +51,7 @@ this page was aging at the very first commit — see | Playable characters | 44, in 5 factions | `CHARACTERS` array in `characters.js` | | Maps in the registry | 12 | `MAPS` object in `maps.js` | | Visual harnesses in HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Harness scripts | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Harness scripts | 203 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Written entry tasks | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Version | `2.0.0-alpha.178` | `public/js/version.js` and `package.json` (match) | From c30e425fd563f26b86771e1866127c9a3e1b528f Mon Sep 17 00:00:00 2001 From: csbrasil-bot <csbrasil-bot@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:13:54 +0000 Subject: [PATCH 14/14] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Signed-off-by: csbrasil-bot <csbrasil-bot@users.noreply.github.com> --- ARCH.generated.md | 6 +++--- README.md | 8 ++++---- STATUS.md | 4 ++-- docs/docs/comecando.md | 4 ++-- docs/docs/quality-gates.md | 2 +- docs/docs/stack.md | 4 ++-- .../docusaurus-plugin-content-docs/current/comecando.md | 4 ++-- .../current/quality-gates.md | 2 +- .../en/docusaurus-plugin-content-docs/current/stack.md | 4 ++-- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/ARCH.generated.md b/ARCH.generated.md index 725b84f0c..280f55cab 100644 --- a/ARCH.generated.md +++ b/ARCH.generated.md @@ -8,9 +8,9 @@ Números atuais das zonas, do quality gate e do `package.json`: | Zona | O que é | Tamanho medido | Regra | |---|---|---|---| -| `public/` | o **jogo** | 44 arquivos `.js`, 32.001 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | -| `src/` | o **site** | 18 páginas `.astro`, 21 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | -| `tools/` | o **arnês** | 201 scripts em `tools/eval/`, 55 em `tools/` | node puro: sobe o jogo real sem browser | +| `public/` | o **jogo** | 46 arquivos `.js`, 32.430 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | +| `src/` | o **site** | 19 páginas `.astro`, 21 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | +| `tools/` | o **arnês** | 204 scripts em `tools/eval/`, 55 em `tools/` | node puro: sobe o jogo real sem browser | **Não existe `public/index.html`.** O HTML do jogo é `src/pages/index.astro`, servido na rota `/`. Servir `public/` estaticamente entrega os arnêses visuais, **não o jogo** — é a pegadinha que custa a primeira hora de todo mundo. diff --git a/README.md b/README.md index 182647bda..d8a422bae 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| Código do jogo | 32.430 linhas em 46 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | | `game.js` | **6.910** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | @@ -44,7 +44,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 201 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 204 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 55 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Versão | `2.0.0-alpha.180` | `public/js/version.js` e `package.json` (batem) | @@ -89,7 +89,7 @@ arquitetura): `cd docs && npm install && npm start` → <http://localhost:3000/d | Camada | Ferramenta | Versão | |---|---|---| | Motor 3D (WebGL) | **Three.js**, vendorizado | `r160` | -| Jogo | ES modules vanilla, **zero build** | 44 arquivos | +| Jogo | ES modules vanilla, **zero build** | 46 arquivos | | Site | **Astro** com SSR | `^7.1.1` | | Hospedagem | adapter **Vercel** | `^11.0.6` | | Banco | **Postgres gerenciado** (RLS; schema privado, fora do repo) | `^2.110.7` | @@ -100,7 +100,7 @@ arquitetura): `cd docs && npm install && npm start` → <http://localhost:3000/d | Esta documentação | **Docusaurus** | `3.6.3` | | Runtime de CI | **Node** | `22` | -Three.js sai de `public/vendor/three.module.js` (**sem CDN, sem npm no runtime**). Astro e Vercel de `package.json` + `astro.config.mjs` + `vercel.json`. Dos scripts de `tools/`, **110** importam Playwright, **39** importam gltf-transform e **4** importam meshoptimizer. +Three.js sai de `public/vendor/three.module.js` (**sem CDN, sem npm no runtime**). Astro e Vercel de `package.json` + `astro.config.mjs` + `vercel.json`. Dos scripts de `tools/`, **111** importam Playwright, **39** importam gltf-transform e **4** importam meshoptimizer. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js` diff --git a/STATUS.md b/STATUS.md index 1678591de..a11288290 100644 --- a/STATUS.md +++ b/STATUS.md @@ -4,8 +4,8 @@ - **Versão:** `2.0.0-alpha.180` - **Conteúdo jogável:** 5 facções, 44 personagens, 12 mapas e 26 armas com GLB -- **Código do jogo:** 32.001 linhas em 44 módulos JavaScript -- **Automação:** 124 comandos npm, 201 scripts de avaliação e 55 scripts de pipeline +- **Código do jogo:** 32.430 linhas em 46 módulos JavaScript +- **Automação:** 124 comandos npm, 204 scripts de avaliação e 55 scripts de pipeline > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `package.json · CHARACTERS · MAPS · public/models/weapons · public/js · tools/` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 476fa7596..2c484a066 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -39,7 +39,7 @@ esta página envelhecia no primeiro commit — ver | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| Código do jogo | 32.430 linhas em 46 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | | `game.js` | **6.910** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | @@ -49,7 +49,7 @@ esta página envelhecia no primeiro commit — ver | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 201 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 204 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 55 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Versão | `2.0.0-alpha.180` | `public/js/version.js` e `package.json` (batem) | diff --git a/docs/docs/quality-gates.md b/docs/docs/quality-gates.md index d802c0845..42f2be208 100644 --- a/docs/docs/quality-gates.md +++ b/docs/docs/quality-gates.md @@ -15,7 +15,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2.275 linhas**, **65 identificadores de invariante declarados** (`put()`), dos quais **28** têm caminho de `skip()` declarado. -- O arnês inteiro são **201 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **55 scripts** de pipeline em `tools/`. +- O arnês inteiro são **204 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **55 scripts** de pipeline em `tools/`. - Quantas invariantes rodam como **críticas** numa execução **não é derivável do fonte**: depende de qual insumo existe na máquina (o JSON do auditor de viewmodel, um GLB, uma pasta de anims). Esse número só sai rodando o quality gate — e o lugar dele é o cabeçalho do `KNOWN-BUGS.md`, atualizado com saída real. Reproduza: diff --git a/docs/docs/stack.md b/docs/docs/stack.md index e5818daeb..d41e078d9 100644 --- a/docs/docs/stack.md +++ b/docs/docs/stack.md @@ -17,7 +17,7 @@ a partir do `package.json`, do `docs/package.json` e do próprio Three.js vendor | Camada | Ferramenta | Versão | |---|---|---| | Motor 3D (WebGL) | **Three.js**, vendorizado | `r160` | -| Jogo | ES modules vanilla, **zero build** | 44 arquivos | +| Jogo | ES modules vanilla, **zero build** | 46 arquivos | | Site | **Astro** com SSR | `^7.1.1` | | Hospedagem | adapter **Vercel** | `^11.0.6` | | Banco | **Postgres gerenciado** (RLS; schema privado, fora do repo) | `^2.110.7` | @@ -28,7 +28,7 @@ a partir do `package.json`, do `docs/package.json` e do próprio Three.js vendor | Esta documentação | **Docusaurus** | `3.6.3` | | Runtime de CI | **Node** | `22` | -Three.js sai de `public/vendor/three.module.js` (**sem CDN, sem npm no runtime**). Astro e Vercel de `package.json` + `astro.config.mjs` + `vercel.json`. Dos scripts de `tools/`, **110** importam Playwright, **39** importam gltf-transform e **4** importam meshoptimizer. +Three.js sai de `public/vendor/three.module.js` (**sem CDN, sem npm no runtime**). Astro e Vercel de `package.json` + `astro.config.mjs` + `vercel.json`. Dos scripts de `tools/`, **111** importam Playwright, **39** importam gltf-transform e **4** importam meshoptimizer. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index 72fb351e4..1335bccf4 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -41,7 +41,7 @@ this page was aging at the very first commit — see | What | How much | Where to check | |---|---:|---| -| Game code | 32,001 lines in 44 files | `git ls-files public/js/*.js \| xargs wc -l` | +| Game code | 32,430 lines in 46 files | `git ls-files public/js/*.js \| xargs wc -l` | | `game.js` | **6,910** lines | `wc -l public/js/game.js` | | `main.js` | 2,698 lines | `wc -l public/js/main.js` | | Weapons with GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | @@ -51,7 +51,7 @@ this page was aging at the very first commit — see | Playable characters | 44, in 5 factions | `CHARACTERS` array in `characters.js` | | Maps in the registry | 12 | `MAPS` object in `maps.js` | | Visual harnesses in HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Harness scripts | 201 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Harness scripts | 204 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 55 | `git ls-files 'tools/*.mjs' \| wc -l` | | Written entry tasks | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Version | `2.0.0-alpha.180` | `public/js/version.js` and `package.json` (match) | diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md index 2a6ea015a..f52637609 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md @@ -18,7 +18,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2,275 lines**, **65 declared invariant identifiers**, with **28** declared `skip()` paths. -- The harness contains **201 scripts** in `tools/eval/`, plus **55 pipeline scripts** in `tools/`. +- The harness contains **204 scripts** in `tools/eval/`, plus **55 pipeline scripts** in `tools/`. - The number of critical checks in one run depends on the inputs present on that machine; dated results belong in `KNOWN-BUGS.md`. ```bash diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md index fa6e23626..697415fe5 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md @@ -20,7 +20,7 @@ from `package.json`, `docs/package.json` and the vendored Three.js itself. | Layer | Tool | Version | |---|---|---| | 3D engine (WebGL) | **Three.js**, vendored | `r160` | -| Game | vanilla ES modules, **zero build** | 44 files | +| Game | vanilla ES modules, **zero build** | 46 files | | Site | **Astro** with SSR | `^7.1.1` | | Hosting | **Vercel** adapter | `^11.0.6` | | Database | **managed Postgres** (RLS; private schema) | `^2.110.7` | @@ -31,7 +31,7 @@ from `package.json`, `docs/package.json` and the vendored Three.js itself. | This documentation | **Docusaurus** | `3.6.3` | | CI runtime | **Node** | `22` | -Three.js comes from `public/vendor/three.module.js`. Of the scripts in `tools/`, **110** import Playwright, **39** import gltf-transform, and **4** import meshoptimizer. +Three.js comes from `public/vendor/three.module.js`. Of the scripts in `tools/`, **111** import Playwright, **39** import gltf-transform, and **4** import meshoptimizer. > Block generated by `node tools/gen-docs.mjs`. Source: `dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js`