Fecha o resíduo da classe S6 da OPS119+ no resto de scripts/. F1: SCRIPTS_SPEC_PINNED (42 módulos scripts/** alcançáveis de specs unit+int) espalhado em HIGH_RISK_EXACT, sincronizado por invariante de igualdade que anda o grafo de imports dos specs (missing = lib nova pinada; stale = rename). F2: isCodePath cobre scripts/ (classifyStaticScope = code). Sem migration/access/Consent/UI. gate:fast verde.
16 KiB
Impl: OPS119++ — blast radius de scripts/: risk map das libs restantes + static scope
Status: em execução Atualizado em: 2026-09-16 Gate: aprovado pelo humano (superset unit+int, 42 paths — confirmado 2026-09-16) Issue: #1077 Intencao: docs/plans/escala-dry-pos-scripts-blast-radius.md Appetite restante: herdado (~1h eng) - F1 ~35min + F2 ~15min + gates ~10min, sem corte Impeccable: A - N/A (CLI/CI, sem superficie de produto) - no designer dispatch
Leitura da intencao
- Outcome: fechar a classe S6 remanescente da OPS119+ (#1071) para o resto de
scripts/. F1: qualquer moduloscripts/**alcancavel por um spec (transitivo) deve ser high-risk, fail-closed - um diff so-lib nao pode mais classificarclassifyTestScopecomononee deixar de rodar a unit que cobre o modulo (ex.:scripts/lib/worktree-env.mjs, pinada portests/unit/worktree.unit.spec.ts:16e importada porscripts/lib/auto-unblock.mjs:10, hoje classificanone). F2:isCodePath(scripts/lib/test-affected-core.mjs:134) deve cobrirscripts/para queclassifyStaticScopedevolvacodee destrave typecheck/knip/cycles num diff so-scripts. - O que NAO negociar: risco e integridade de guard (blast radius fail-closed),
nao refactor; Impeccable A (CLI/CI); appetite ~1h; sem migration, sem
schema/Consent/access, sem URL publica/API, sem UI; a alternativa rejeitada na
intencao-mae (prefixo
scripts/lib/inteiro emHIGH_RISK_EXACT) nao pode voltar. Pins so chamam.has()-HIGH_RISK_EXACTcontinua sendo umSetexportado; nenhum pin afirma que um path descripts/NAO e high-risk. - O que reavaliar: (1) "invariante derivado" NAO pode derivar em tempo de
import -
test-affected-core.mjse documentado puro (:2-6: "No git, no fs, no process"), entao a lista segue literal e o que "deriva" e um teste de invariante fail-closed que anda o grafo de imports dos specs e exige o fechamento contido emHIGH_RISK_EXACT; (2) "spec unit" literal (41 paths) vs todas as arvores de spec (tests/unit+tests/int, 42 paths - so acrescentascripts/cityReportSnapshot.mjs, importado portests/int/cityReportSnapshot.int.spec.ts:16); (3) "append direto" vs grupo nomeado nao-exportado (SCRIPTS_SPEC_PINNED) - o grupo documenta o bloco e evita knip dead-export.
Abordagem recomendada
flowchart LR
spec[specs tests/unit + tests/int] -->|walker fs+regex| graph[grafo de imports]
graph -->|closure restrita a scripts/**| closure[scripts reachable]
closure -->|assert subset| HRE[HIGH_RISK_EXACT]
HRE --> classifyTestScope
classifyTestScope -->|full| full[unit+int full / e2e curated]
isCodePath -->|startsWith scripts/| classifyStaticScope
classifyStaticScope -->|code| static[typecheck+knip+cycles]
pin1[testAffected.unit.spec] -->|classify full| isCodePath
pin2[ciSkipInvariants.unit.spec] -->|invariante do grafo| closure
Opcoes consideradas: A | B | C
Recomendacao: A - manter HIGH_RISK_EXACT literal e adicionar um teste de
invariante fail-closed (tests/unit/ciSkipInvariants.unit.spec.ts) que anda o
grafo de imports dos specs via fs+regex (test-only), calcula o fechamento
transitivo restrito a scripts/** e afere closure subset HIGH_RISK_EXACT,
falhando com a lista de faltantes. Porque test-affected-core.mjs e puro por
contrato (nao pode ler o repo em tempo de import), o "invariante derivado" so
existe como teste; o teste e o que mata a classe na raiz - uma lib nova
pinada por spec sem entrada no mapa quebra o invariante no PR que a adiciona, em
vez de abrir o buraco em silencio. Escopo: superset de todas as arvores de
spec (42, inclui scripts/cityReportSnapshot.mjs do int), divergencia deliberada
do "spec unit" literal da intencao (41) - mesmo walker, +1 entrada, fecha a mesma
classe S6 tambem para int; confirmar no gate.
Rejeitadas: (B) estender o allowlist literal apenas com os arquivos
conhecidos sem o teste de invariante - e literalmente a opcao rejeitada na
intencao-mae: reabre o buraco a cada lib nova; (C) prefixo scripts/lib/
inteiro - engole as ~40 libs scripts/lib/*.mjs sem harness e forca full em
diffs triviais, rejeitado na intencao; (D) modulo de dados gerado/commitado com o
closure - mesma indirecao extra, mesmo teste de sincronia necessario, sem ganho
sobre a lista literal + invariante.
Decisoes de engenharia
- Forma do invariante: derivacao em import vs teste fail-closed. Opcoes: A)
teste que anda o grafo e afere subset; B) exportar o closure do modulo puro; C)
manter literal sem teste. Recomendacao: A - e a unica forma compativel com o
contrato "pure, no fs" do dono (
test-affected-core.mjs:2-6); o custo de CI e zero (roda na suite unit normal). Rejeitadas: B porque o modulo puro nao pode ler o repo e um valor derivado de dado externo exigiria injecao/codigo gerado (opcao D acima); C porque repete a opcao B da intencao (reabre o buraco). - Escopo do walker: so unit vs unit+int. Opcoes: A) so
tests/unit(41, palavra literal da intencao); B) todas as arvores (tests/unit+tests/int, 42). Recomendacao: A superset B - o espec importadortests/int/cityReportSnapshot.int.spec.ts:16adiciona exatamente um path (scripts/cityReportSnapshot.mjs); mesmo walker, fecha a mesma classe para int. Rejeitadas: A sozinho deixaria um modulo int-pinado de fora por fidelidade literal ao texto, sem ganho de seguranca; divergencia registrada para o gate. - Shape da lista: append direto vs grupo nomeado nao-exportado. Opcoes: A)
acrescentar os 33 paths soltos no fim de
HIGH_RISK_EXACT; B) criarconst SCRIPTS_SPEC_PINNED = [ ... ]nao exportado, documentando o bloco, e espalhar (...SCRIPTS_SPEC_PINNED) dentro donew Set([...]). Recomendacao: B- o bloco de ~40 scripts fica legivel/documentado como "modulos scripts/
pinados por spec" e nao vira export novo (evita dead-export do knip). O grupo
nao e exportado; o que os pins consomem continua sendo
HIGH_RISK_EXACT. Rejeitadas: A reduz a legibilidade do mapa mas e funcionalmente identica; nao ha razao para carregar 42 entradas soltas. Gatilho de revisao: se um pin precisar do subconjunto puro, exportar com pin proprio.
- o bloco de ~40 scripts fica legivel/documentado como "modulos scripts/
pinados por spec" e nao vira export novo (evita dead-export do knip). O grupo
nao e exportado; o que os pins consomem continua sendo
- F2:
path.startsWith('scripts/')vs lista exata de scripts. Opcoes: A) prefixoscripts/emisCodePath; B) enumerar os scripts relevantes. Recomendacao: A -isCodePathja opera por prefixo/categoria (src/,tests/,eslint*,knip*); todo.mjsdescripts/e codigo de ferramenta e afeta typecheck/knip/cycles, entao a categoria e a granularidade certa. Rejeitadas: B por manutencao (nova lib so-scripts voltaria a ficar de fora) e por contrariar o padrao do predicado;classifyBuildScopefica intocado (isBuildPathnao incluiscripts/). - Divergencia unit vs unit+int. Registrada acima; decisao do gate, nao do agente. Se o gate exigir fidelidade literal, cai-se para o subconjunto de 41 removendo 1 path do grupo - reversao barata e mecanica.
Componentes / mudancas
HIGH_RISK_EXACT/SCRIPTS_SPEC_PINNED(scripts/lib/test-affected-core.mjs:25-56): novoconst SCRIPTS_SPEC_PINNEDnao-exportado com os modulosscripts/**alcancaveis por specs (8 ja cobertos + 33 faltantes +cityReportSnapshot.mjs); espalhado nonew Set([...]). Os 33 faltantes (scripts/lib/...salvo indicacao):agent-forgejo.mjs,agent-plan-lifecycle.mjs,agent-pool-cursor.mjs,agent-pool-eligibility.mjs,agent-pool-models.mjs,agent-pool-prompt.mjs,agent-pool-state.mjs,ansi.mjs,auto-unblock.mjs,camaraSpeeches.mjs,changelog.mjs,cityReportBlocks.mjs,cityReportDatabase.mjs,cityReportFormat.mjs,cityReportRender.mjs,cityReportResearch.mjs,cityReportTerritory.mjs,conflictMarkers.mjs,db-start.mjs,deploy-trigger.mjs,github-api.mjs,github-branch-protection.mjs,github-pr-flow.mjs,imageFill.mjs,imageResize.mjs,issues-panel.mjs,markdown-ansi.mjs,playwright-e2e-args.mjs,portalTransparenciaEmendas.mjs,sollaCeuciSalvadorMetrics.mjs,testing-audit-metrics-core.mjs,worktree-env.mjs,wpArticles.mjs; maisscripts/cityReportSnapshot.mjs(int). Nao-reachable confirmados e deixados fora:scripts/lib/agent-pool-forgejo.mjs,camaraFetch.mjs,topology.mjs.isCodePath(scripts/lib/test-affected-core.mjs:134-143): acrescentarif (path.startsWith('scripts/')) return true;classifyStaticScopepassa a devolvercodepara diff so-scripts.classifyBuildScope/isBuildPathintocados.- Pins:
tests/unit/testAffected.unit.spec.ts(loop:22-35+ casoclassifyStaticScope),tests/unit/ciSkipInvariants.unit.spec.ts(novoitdo invariante do grafo + pin comportamental declassifyTestScopeparascripts/lib/worktree-env.mjs, e pin deisCodePath('scripts/...')). - Changelog:
docs/changelog/2026-09-16-ops119plusplus.md(follow-up nomeado pela entrada2026-09-16-ops119plus.md). - Migration: sem migration.
- Access / Consent: n/a.
- UI: Impeccable A - N/A; no designer dispatch.
Dados -> forma (se aplicavel)
N/A - sem superficie de produto; o "dado" e a lista de paths, cuja forma
(grupo nomeado nao-exportado + Set exportado) foi decidida acima.
Fases verificaveis
- F1 - risk map (~35min). Introduzir
SCRIPTS_SPEC_PINNEDemscripts/lib/test-affected-core.mjscom os 42 paths (41 +cityReportSnapshot.mjs), espalhar emHIGH_RISK_EXACT; novoitemtests/unit/ciSkipInvariants.unit.spec.tsque andatests/unit/**+tests/int/**, extrai imports estaticos relativos (from './x.mjs'/from '../...'), calcula o fechamento transitivo restrito ascripts/**e afereclosure.filter(p => !HIGH_RISK_EXACT.has(p))vazio (falha listando os faltantes); acrescentar o mesmo caso comportamentalclassifyTestScope([{ path: 'scripts/lib/worktree-env.mjs', status: 'M' }]).mode === 'full'. Verificacao: invariante verde com o mapa novo; e removendo mentalmente uma entrada o teste falha (provado no PR).pnpm test:unitverde. - F2 - static scope (~15min). Extender
isCodePathcompath.startsWith('scripts/'); pinsisCodePath('scripts/lib/cli.mjs') === trueeclassifyStaticScope([{ path: 'scripts/lib/cli.mjs', status: 'M' }]).mode === 'code'emciSkipInvariants/testAffected. Verificacao:pnpm test:unitverde;classifyBuildScopeseguenonenum diff so-scripts (pin de nao-regressao se barato). - Gates (~10min).
pnpm gate:fast(lint + typecheck + unit);pnpm push. Sem migration e sem e2e novo (Impeccable A, sem superficie de produto).
Rabbit holes / Nao escopo (engenharia)
- Prefixo
scripts/lib/inteiro emHIGH_RISK_EXACT: rejeitado na intencao (engoliria libs sem harness e forcafullem diffs triviais). O que entra e exatamente o fechamento alcancavel por specs, nao o diretorio. - Invariante de existencia em disco repo-wide sobre toda a lista
HIGH_RISK_EXACT/HIGH_RISK_PREFIXES: named follow-up da OPS119+, fora do appetite; aqui so o invariante de cobertura do grafo. - Walker com parsing real de AST/aliases/dynamic import: regex de
importestaticos e suficiente para o contrato (specs importam libs relativas); se um dia houver alias/dynamic import, o invariante falha fail-closed (nao ve a aresta -> ainda pode sub-reportar; ver Riscos). - Tocar
classifyBuildScope/isBuildPathparascripts/: fora -build_modeseguenone, nao e o achado. - Guard
isMainem scripts, S1/S2/S3, prefill upstream, UI/Consent/access: fora, conforme a intencao.
Riscos e mitigacao
- Walker sub-reporta (falso verde). Regex que perde uma aresta de import
(alias, dynamic import, re-export) encurta o closure. Mitigacao: o
itinclui um self-check que assere arestas conhecidas, direta (scripts/lib/worktree-env.mjs) e transitiva (scripts/lib/cityReportTerritory.mjsso alcancavel via outro moduloscripts/); a travessia segue toda aresta relativa (nao so as que caem emscripts/), entao um import viatests/helpers/src/tambem entra. Se um alias aparecer, o self-check quebra primeiro. - Custo de CI sobe em diff so-scripts. Adicionar ao mapa faz um diff
so-
scripts/rodar unit+int full e e2ecurated(6 specs congeladas, OPS86);build_modeseguenone(isBuildPathnao incluiscripts/). Precedente: OPS119+ ja aceitou isso para as 3 libs de launch. Mitigacao: esperado e desejado (fail-closed); registrar no changelog; nao ampliar alem do fechamento alcancavel por specs. HIGH_RISK_EXACTestoura/degrada leitura. ~42 entradas + grupo nomeado. Mitigacao:SCRIPTS_SPEC_PINNEDdocumentado e exportado (consumido pelo invariante; knip limpo), com o overlap de 8 paths explicado no doc do grupo.- Entrada high-risk morta por rename. Mitigacao: o invariante e de
igualdade — o half
stale(SCRIPTS_SPEC_PINNEDfora do closure) falha numa entrada que nao e mais alcancavel. Existencia em disco repo-wide sobre todo oHIGH_RISK_EXACTsegue debito diferido (abaixo).
Debitos diferidos (triagem /simplify)
Triagem autonoma dos achados dos 2 revisores; nenhum vira Issue nova (nenhum
expensive_lock score >=4 — a integridade do guard ja e fail-closed).
- A (score 2, cheap_polish) —
STATIC_IMPORT_REsem boundary: casafrom './x.mjs'em comentario/string. Direcao fail-closed (so adiciona pin/aresta inexistente), nunca falso verde. Gatilho: primeira falha do invariante por import relativo comentado/em string. - D (score 3, defer_trigger) — closure cobre unit+int, nao e2e: um
setup/spec e2e que importe um modulo
scripts/**fora do mapa nao seria pinado (hoje o unico,seed-minimal-manifest.mjs, ja e high-risk). Gatilho: primeiro import descripts/**por um spec/setup e2e fora do mapa. - E (score 3, defer_trigger) — existencia em disco repo-wide sobre todo o
HIGH_RISK_EXACT/HIGH_RISK_PREFIXES(alem do grupo scripts, que ostaleja cobre). Ja nomeado na intencao-mae. Gatilho: primeira entrada high-risk morta fora do grupo scripts. - B/C descartados: overlap de 8 paths com o literal (intencional — o grupo e o mapa exato da igualdade) e custo de CI maior em diff so-scripts (aceito, fail-closed, precedente OPS119+).
Aceite de engenharia
- Aceite de produto da intencao coberto: diff so-
scripts/lib/<lib>pinada por spec classificafull(F1) e diff so-scripts classificacodeemclassifyStaticScope(F2). - Invariantes AGENTS/engineering-standards: identificadores em ingles, prosa
pt-BR; nenhuma migration; nenhum access/Consent/UI/URL publica tocado;
HIGH_RISK_EXACTsegueSetexportado. - Testes de dominio previstos:
tests/unit/testAffected.unit.spec.tsetests/unit/ciSkipInvariants.unit.spec.ts- todos unit, sem harness novo. pnpm gate:fastverde;pnpm pushno fluxo normal de PR.
Self-score decision-quality
- Decisoes caras tem rejeitadas? 5/5 - forma do invariante, escopo
unit-vs-unit+int, shape da lista e F2 prefixo-vs-lista registraram
Opcoes/Recomendacao/Rejeitadas; as rejeitadas herdadas da intencao-mae
(prefixo
scripts/lib/) ficam explicitas. - Cabe no appetite (~1h)? 5/5 - F1 ~35min (grupo + invariante + 2 pins), F2 ~15min (1 predicado + pins), gates ~10min; sem migration, sem e2e novo, sem UI.
- Rabbit holes nomeados? 5/5 - prefixo
scripts/lib/inteiro, existencia repo-wide, AST/dynamic import,classifyBuildScope, S1/S2/S3, guardisMain, UI/Consent. - Depth check reusa shells/helpers? 4/5 - reusa
HIGH_RISK_EXACT,isCodePath/classifyStaticScopee os specs donos que ja importam o modulo; nao cria modulo paralelo nem derived-data commitado; o walker e local ao spec-to-be que ja temfs. - Intencao (aceite de produto) intacta? 5/5 - a engenharia nao reescreveu o outcome; o invariante fail-closed e a unica forma compativel com o contrato pure do dono e cobre superset com divergencia registrada.
Score: 24/25 - aprovado; gate confirmou o superset unit+int (42 paths) em 2026-09-16.