mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 08:53:11 +00:00
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.
268 lines
16 KiB
Markdown
268 lines
16 KiB
Markdown
# 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 modulo `scripts/**` alcancavel por um spec (transitivo)
|
|
deve ser high-risk, fail-closed - um diff so-lib nao pode mais classificar
|
|
`classifyTestScope` como `none` e deixar de rodar a unit que cobre o modulo
|
|
(ex.: `scripts/lib/worktree-env.mjs`, pinada por `tests/unit/worktree.unit.spec.ts:16`
|
|
e importada por `scripts/lib/auto-unblock.mjs:10`, hoje classifica `none`). F2:
|
|
`isCodePath` (`scripts/lib/test-affected-core.mjs:134`) deve cobrir `scripts/`
|
|
para que `classifyStaticScope` devolva `code` e 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 em `HIGH_RISK_EXACT`) nao pode
|
|
voltar. Pins so chamam `.has()` - `HIGH_RISK_EXACT` continua sendo um `Set`
|
|
exportado; nenhum pin afirma que um path de `scripts/` NAO e high-risk.
|
|
- **O que reavaliar:** (1) "invariante derivado" NAO pode derivar em tempo de
|
|
import - `test-affected-core.mjs` e 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 em `HIGH_RISK_EXACT`; (2) "spec unit" literal (41 paths) vs
|
|
todas as arvores de spec (`tests/unit` + `tests/int`, 42 paths - so acrescenta
|
|
`scripts/cityReportSnapshot.mjs`, importado por `tests/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
|
|
|
|
```mermaid
|
|
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 importador `tests/int/cityReportSnapshot.int.spec.ts:16`
|
|
adiciona 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) criar
|
|
`const SCRIPTS_SPEC_PINNED = [ ... ]` **nao exportado**, documentando o bloco, e
|
|
espalhar (`...SCRIPTS_SPEC_PINNED`) dentro do `new 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.
|
|
- **F2: `path.startsWith('scripts/')` vs lista exata de scripts.** Opcoes: A)
|
|
prefixo `scripts/` em `isCodePath`; B) enumerar os scripts relevantes.
|
|
Recomendacao: A - `isCodePath` ja opera por prefixo/categoria (`src/`, `tests/`,
|
|
`eslint*`, `knip*`); todo `.mjs` de `scripts/` 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; `classifyBuildScope` fica intocado (`isBuildPath` nao
|
|
inclui `scripts/`).
|
|
- **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`):
|
|
novo `const SCRIPTS_SPEC_PINNED` nao-exportado com os modulos `scripts/**`
|
|
alcancaveis por specs (8 ja cobertos + 33 faltantes + `cityReportSnapshot.mjs`);
|
|
espalhado no `new 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`; mais `scripts/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`): acrescentar
|
|
`if (path.startsWith('scripts/')) return true`; `classifyStaticScope` passa a
|
|
devolver `code` para diff so-scripts. `classifyBuildScope`/`isBuildPath`
|
|
intocados.
|
|
- **Pins:** `tests/unit/testAffected.unit.spec.ts` (loop `:22-35` + caso
|
|
`classifyStaticScope`), `tests/unit/ciSkipInvariants.unit.spec.ts` (novo `it`
|
|
do invariante do grafo + pin comportamental de `classifyTestScope` para
|
|
`scripts/lib/worktree-env.mjs`, e pin de `isCodePath('scripts/...')`).
|
|
- **Changelog:** `docs/changelog/2026-09-16-ops119plusplus.md` (follow-up nomeado
|
|
pela entrada `2026-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
|
|
|
|
1. **F1 - risk map (~35min).** Introduzir `SCRIPTS_SPEC_PINNED` em
|
|
`scripts/lib/test-affected-core.mjs` com os 42 paths (41 + `cityReportSnapshot.mjs`),
|
|
espalhar em `HIGH_RISK_EXACT`; novo `it` em
|
|
`tests/unit/ciSkipInvariants.unit.spec.ts` que anda `tests/unit/**` + `tests/int/**`,
|
|
extrai imports estaticos relativos (`from './x.mjs'`/`from '../...'`), calcula o
|
|
fechamento transitivo restrito a `scripts/**` e afere
|
|
`closure.filter(p => !HIGH_RISK_EXACT.has(p))` vazio (falha listando os
|
|
faltantes); acrescentar o mesmo caso comportamental
|
|
`classifyTestScope([{ 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:unit` verde.
|
|
2. **F2 - static scope (~15min).** Extender `isCodePath` com
|
|
`path.startsWith('scripts/')`; pins `isCodePath('scripts/lib/cli.mjs') === true` e
|
|
`classifyStaticScope([{ path: 'scripts/lib/cli.mjs', status: 'M' }]).mode === 'code'`
|
|
em `ciSkipInvariants`/`testAffected`. Verificacao: `pnpm test:unit` verde;
|
|
`classifyBuildScope` segue `none` num diff so-scripts (pin de nao-regressao se
|
|
barato).
|
|
3. **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 em `HIGH_RISK_EXACT`: rejeitado na intencao
|
|
(engoliria libs sem harness e forca `full` em 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 `import`
|
|
estaticos 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`/`isBuildPath` para `scripts/`: fora - `build_mode`
|
|
segue `none`, nao e o achado.
|
|
- Guard `isMain` em 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 `it` inclui
|
|
um **self-check** que assere arestas conhecidas, **direta**
|
|
(`scripts/lib/worktree-env.mjs`) e **transitiva** (`scripts/lib/cityReportTerritory.mjs`
|
|
so alcancavel via outro modulo `scripts/`); a travessia segue toda aresta
|
|
relativa (nao so as que caem em `scripts/`), entao um import via
|
|
`tests/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 e2e **`curated`** (6 specs congeladas,
|
|
OPS86); `build_mode` segue `none` (`isBuildPath` nao inclui `scripts/`).
|
|
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_EXACT` estoura/degrada leitura.** ~42 entradas + grupo nomeado.
|
|
Mitigacao: `SCRIPTS_SPEC_PINNED` documentado 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_PINNED` fora do closure) falha
|
|
numa entrada que nao e mais alcancavel. Existencia em disco repo-wide sobre
|
|
todo o `HIGH_RISK_EXACT` segue 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_RE` sem boundary**: casa
|
|
`from './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 de `scripts/**` 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 o `stale`
|
|
ja 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
|
|
|
|
- [x] Aceite de produto da intencao coberto: diff so-`scripts/lib/<lib>` pinada
|
|
por spec classifica `full` (F1) e diff so-scripts classifica `code` em
|
|
`classifyStaticScope` (F2).
|
|
- [x] Invariantes AGENTS/engineering-standards: identificadores em ingles, prosa
|
|
pt-BR; nenhuma migration; nenhum access/Consent/UI/URL publica tocado;
|
|
`HIGH_RISK_EXACT` segue `Set` exportado.
|
|
- [x] Testes de dominio previstos: `tests/unit/testAffected.unit.spec.ts` e
|
|
`tests/unit/ciSkipInvariants.unit.spec.ts` - todos unit, sem harness novo.
|
|
- [x] `pnpm gate:fast` verde; `pnpm push` no fluxo normal de PR.
|
|
|
|
## Self-score decision-quality
|
|
|
|
1. 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.
|
|
2. 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.
|
|
3. Rabbit holes nomeados? **5/5** - prefixo `scripts/lib/` inteiro, existencia
|
|
repo-wide, AST/dynamic import, `classifyBuildScope`, S1/S2/S3, guard
|
|
`isMain`, UI/Consent.
|
|
4. Depth check reusa shells/helpers? **4/5** - reusa `HIGH_RISK_EXACT`,
|
|
`isCodePath`/`classifyStaticScope` e 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 tem `fs`.
|
|
5. 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.**
|