teqo-backup/docs/plans/escala-dry-pos-ops119-impl.md

120 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Impl: OPS119+ — launch do worktree: risk map das libs + pin do wiring do `plan`
Status: em execução
Atualizado em: 2026-09-16
Issue: #1071
Intenção: docs/plans/escala-dry-pos-ops119.md
Appetite restante: herdado (~1h eng) — F1 ~30min + F2 ~30min, sem corte
## Leitura da intenção
- **Outcome:** fechar os dois achados de score 3 do `/simplify` da OPS119 na mesma superfície (launch do worktree ↔ agent-session). F1: um diff que toca só `scripts/lib/worktree.mjs`, `scripts/lib/agent-session.mjs` ou `scripts/agent-session.mjs` hoje classifica `none` (a unit não roda e o static scope também é `none`) — o risk map deve forçar `full`, fail-closed. F2: o repasse `argument: bag` de `cmdPlan` → `cmdNamespaceBranch` não pode mais ser dropado em silêncio — o wiring vira estrutura pinada por unit.
- **O que NÃO negociar:** o outcome é integridade de guard (blast radius fail-closed) e pin do wiring, não refactor de arquitetura; Impeccable A (CLI/shell, sem superfície de produto); appetite ~1h; S1/S2/S3 e o prefill upstream ficam fora; a alternativa rejeitada na intenção (prefixo `scripts/lib/` inteiro em `HIGH_RISK_EXACT`) não pode voltar; migration/Consent/UI não se aplicam.
- **O que reavaliar:** (1) “o F2 precisa tornar `cmdNamespaceBranch` testável” — não precisa: **derivar o descriptor dentro do runner elimina o parâmetro dropável por construção**, sem importar `scripts/worktree.mjs` (que hoje dispara o CLI no import — `:950`, `:1043-1062`); (2) “o descriptor deveria sanear o bag” — a sanitização já tem dois donos (`opencodeLaunchDirective:228`, `purposeInvocation:198`) e S2 é defer, então sanear de novo reintroduziria a duplicação; (3) “o descriptor pertence a `scripts/lib/agent-session.mjs`” — apesar de `SESSION_COMMAND_BY_PURPOSE` morar lá, `noun`/`sessionLabel` são vocabulário de branch/namespace do worktree (decisão abaixo).
## Abordagem recomendada
```mermaid
flowchart LR
cmdPlan -->|purpose plan + bag| cmdNamespaceBranch
cmdNew -->|purpose new + bag| cmdNamespaceBranch
cmdFix -->|purpose fix + bag| cmdNamespaceBranch
cmdNamespaceBranch -->|deriva| namespaceLaunchDescriptor
namespaceLaunchDescriptor -->|noun, sessionLabel, argument| cmdNamespaceBranch
cmdNamespaceBranch --> opencodeLaunchDirective
cmdNamespaceBranch --> headlessDirective
worktree.unit.spec --> namespaceLaunchDescriptor
testAffected.unit.spec --> HIGH_RISK_EXACT
ciSkipInvariants.unit.spec --> HIGH_RISK_EXACT
```
**Opções consideradas:** A | B | C
**Recomendação:** A — extrair `namespaceLaunchDescriptor({ purpose, bag })` para `scripts/lib/worktree.mjs` e fazer `cmdNamespaceBranch` **derivar o descriptor internamente** a partir de `{ purpose, bag }`, removendo `noun`/`sessionLabel`/`argument` da assinatura; os 3 callers passam só `purpose`+`bag` (+ `branchName`, `flags`, `headless`, `directivePath`). Porque a classe do S4 (parâmetro dropado no caller) deixa de existir estruturalmente: não há `argument` para dropar. O pin unitário cai na lib pura que o spec já importa (`tests/unit/worktree.unit.spec.ts:17-35`), sem exigir guard de importabilidade em `worktree.mjs`.
**Rejeitadas:** B — tornar `cmdNamespaceBranch` injetável/importável e testar as opções com mocks: exigiria adicionar guard `isMain` a `scripts/worktree.mjs` e costurar seams de `git`/`fs`/`provision` que hoje são `execFileSync`/`readFileSync` diretos (`:696-746`) — refactor grande para ~0 teste novo além do descriptor; C — manter os parâmetros e exportar só o mapa: o pin pegaria o descriptor, mas o caller continuaria podendo dropar `argument`, ou seja, a classe do bug sobrevive; o teste que cria worktree de verdade segue rejeitado (lento, side effects, colide no slot, já descartado na intenção).
### Componentes / mudanças
- **`namespaceLaunchDescriptor`** (`scripts/lib/worktree.mjs`, **novo**): mapa puro `purpose`+`bag` → `{ noun, sessionLabel, argument }`, absorvendo os literais hoje inline nos 3 callers (`noun` por purpose; `sessionLabel` = `lote "…"`/`bag "…"`/`bug "…"` com bag, senão `sequencial`; `argument` = bag para `plan`/`fix`, `null` para `new`). Não reimplementa regra de branch nem sanitização — reusa a convenção já existente no módulo (prefixos e `namespaceBranchName` ficam intocados).
- **`cmdNamespaceBranch`** (`scripts/worktree.mjs:679-747`): remove `noun`/`sessionLabel`/`argument` da assinatura, recebe `bag`, e chama `namespaceLaunchDescriptor({ purpose, bag })` uma vez; os usos em `:709` (erro), `:716` (sessão exibida), `:737` (`headlessDirective({ report })`) e `:744` (`printLaunchDirective({ argument })`) passam a ler o descriptor.
- **`cmdPlan`/`cmdNew`/`cmdFix`** (`scripts/worktree.mjs:759-769`, `:780-789`, `:806-818`): passam `purpose`+`bag`; `branchName`, `flags`, `headless`, `directivePath` seguem iguais.
- **`HIGH_RISK_EXACT`** (`scripts/lib/test-affected-core.mjs:25-53`): acrescentar `scripts/agent-session.mjs`, `scripts/lib/agent-session.mjs`, `scripts/lib/worktree.mjs`.
- **Pins de teste:** `tests/unit/testAffected.unit.spec.ts` (loop `:22-35`), `tests/unit/ciSkipInvariants.unit.spec.ts` (`:100-116`), `tests/unit/worktree.unit.spec.ts` (novo `describe` do descriptor).
- **Migration:** sem migration.
- **Access / Consent:** n/a.
- **UI:** Impeccable A — N/A.
### Decisões de engenharia
- **Dono do descriptor: `scripts/lib/worktree.mjs` vs `scripts/lib/agent-session.mjs`.** Opções: A) `worktree.mjs`; B) `agent-session.mjs` (junto de `SESSION_COMMAND_BY_PURPOSE`). Recomendação: A — `noun`/`sessionLabel` são vocabulário de branch/namespace do worktree (mesmo módulo de `planBranchName`/`workBranchName`/`fixBranchName` e do `opencodeLaunchDirective` que consome `argument`), e o spec que já importa o módulo (`worktree.unit.spec.ts:17-35`) pina sem import cruzado novo. Rejeitadas: B porque espalharia metade do contrato de launch (label de branch) para o dono do ciclo de vida de sessão, criando dependência invertida; o que é genuinamente compartilhado com `agent-session` é só o flag `--argument` na borda CLI, que os dois specs já pinam de cada lado.
- **Bag no descriptor: cru vs saneado.** Opções: A) cru (trim só para decidir o label); B) aplicar `replace(/["\\]/g,'')` no descriptor. Recomendação: A — a sanitização é dos donos (`opencodeLaunchDirective:228` no caminho terminal, `purposeInvocation:198` no caminho do driver) e S2 é defer explícito. Rejeitadas: B porque criaria o **terceiro** call site da sanitização — exatamente o gatilho de S2 — e faria o `report` do `headlessDirective` divergir do comportamento atual (que sanitiza só na montagem do argv).
- **`purpose` desconhecido: fail-high vs fail-safe.** Opções: A) `namespaceLaunchDescriptor` lança em purpose não mapeado; B) degradar para um descriptor neutro. Recomendação: A — os únicos callers são `plan`/`new`/`fix`; um purpose novo sem descriptor é erro de wiring e deve falhar alto (mesmo padrão de `resolveWorktreeModel`). Rejeitadas: B porque inventaria `noun`/label silenciosamente, que é a própria classe de bug que o pin fecha.
- **Corte do pin de F1: membership-only vs membership+existência em disco.** Opções: A) só `HIGH_RISK_EXACT.has(path)`; B) membership + `fs` confirmando que cada um dos 3 paths resolve. Recomendação: B, no `it` dedicado de `ciSkipInvariants` — um literal high-risk morto (typo/rename) é exatamente a integridade que o S6 protege e não existe invariante de existência hoje. Rejeitadas: A deixa o valor do F1 degradar em silêncio; o invariante de existência **repo-wide** sobre toda a lista (que revelaria entradas antigas mortas) fica como follow-up nomeado, fora do appetite.
- **Cross-pin `purposeInvocation` × descriptor (2ª perna).** Opções: A) pinar os dois juntos num spec; B) manter os dois pins independentes. Recomendação: B — o contrato entre worktree e agent-session é o flag `--argument`, já pinado nas duas pontas (`opencodeLaunchDirective` por purpose e `purposeInvocation` por argument); um teste combo só reafirmaria os dois. Rejeitadas: A por duplicação e por acoplar dois módulos puros num spec; gatilho de revisitação: se o flag mudar de transporte (env/stdin) ou se S2 consolidar a sanitização num único dono.
## Fases verificáveis
1. **F1 — risk map (~30min).** Adicionar os 3 arquivos a `HIGH_RISK_EXACT` (`scripts/lib/test-affected-core.mjs:25-53`); acrescentar os 3 ao loop de `tests/unit/testAffected.unit.spec.ts:22-35`; novo `it` em `tests/unit/ciSkipInvariants.unit.spec.ts` com nome de contrato (“launch/agent-session modules are high-risk — fail-closed blast radius”) pinando membership + presença em disco dos 3. Verificação: `classifyTestScope([{ path: 'scripts/lib/worktree.mjs', status: 'M' }]).mode === 'full'` para cada um dos 3 (coberto pelo loop) e `pnpm test:unit` verde.
2. **F2 — descriptor + wiring (~30min).** Extrair `namespaceLaunchDescriptor` para `scripts/lib/worktree.mjs`; fazer `cmdNamespaceBranch` derivar internamente e remover `noun`/`sessionLabel`/`argument` da assinatura; simplificar os 3 callers para `purpose`+`bag`. Adicionar `describe('namespaceLaunchDescriptor …')` em `tests/unit/worktree.unit.spec.ts` cobrindo plan com/sem bag (argument threaded + label `lote "…"` vs `sequencial`), new com/sem bag (argument sempre `null`), fix com/sem bag, e purpose desconhecido → throw. Verificação: `pnpm test:unit` verde e `tsc --noEmit` limpo (assinatura nova sem sobras).
3. **Gates.** `pnpm gate:fast` (lint + typecheck + unit); push via `pnpm push`. Sem migration e sem e2e novo (Impeccable A, sem superfície de produto).
## Rabbit holes / Não escopo (engenharia)
- Guard `isMain` em `scripts/worktree.mjs` (padrão de `scripts/check-test-locations.mjs:68-70`): só seria preciso na opção B; com o descriptor derivado internamente, não é necessário — se um dia `worktree.mjs` precisar de teste direto, é Issue própria.
- Prefixo `scripts/lib/` inteiro em `HIGH_RISK_EXACT`: rejeitado na intenção (engoliria as ~40 libs `scripts/lib/*.mjs` sem harness e forçaria `full` em diffs triviais). O resíduo “lib-only diff → unit `none`” para as demais libs fica nomeado como follow-up, não implementado.
- Invariante de existência em disco sobre a lista inteira de `HIGH_RISK_EXACT`/`HIGH_RISK_PREFIXES`: escopo maior que os 3 alvos; follow-up.
- S1 (fallback fail-safe de `purposeInvocation:202`), S2 (sanitização duplicada, gatilho 3º call site), S3 (política driverless), prefill-sem-submit upstream: fora, conforme a intenção.
- Cross-pin `purposeInvocation` × descriptor: fora (decisão acima).
## Adiado com gatilho (triage pós-simplify)
- **Generalização do risk map para as demais `scripts/lib/*.mjs` unit-pinadas**
(ex.: `scripts/lib/worktree-env.mjs`, pinada por `tests/unit/worktree.unit.spec.ts:5-16`):
a intenção só pediu os 3 arquivos de launch/agent-session e rejeitou o prefixo
`scripts/lib/` inteiro. A classe remanescente (diff só-lib → unit `none`) virou
a Issue `OPS119++` (#1077, `depends: [OPS119+]`), plano
`docs/plans/escala-dry-pos-scripts-blast-radius.md`.
- **3ª codificação de “purpose carrega bag”** (`entry.carriesBag` em
`namespaceLaunchDescriptor` × `opencodeLaunchDirective` × `purposeInvocation`):
defer. Gatilho: 4º encoding **ou** mudança de transporte do `--argument`
(env/stdin) → consolidar membership+sanitização num dono único.
- **`classifyStaticScope` segue `none` para `scripts/*.mjs`** (lint/typecheck/knip
pulados num diff só-scripts): entrou como fase 2 da Issue `OPS119++`.
## Explicitamente fora (triage pós-simplify)
- Bag em branco → `argument` cru no descriptor × `opencodeHeadlessArgs` lançando
“report vazio”: comportamento pré-existente e intencional (fail-high do headless);
sem reuso bloqueado. Descartado.
- Drift cosmético do docblock/reason de `HIGH_RISK_EXACT` (“schema/lockfile/test
harness”): descartado (cheap_polish score ≤2, sem impacto funcional).
- `noun` como misnomer (`'de planejamento'`/`'neutro'`/`'de correção de bug'` são
qualificadores): rename de pureza mantido por continuidade com o código original.
Descartado.
- Duplicação dos pins de membership entre `testAffected` e `ciSkipInvariants`:
já resolvido/aceito no plano (camadas distintas — classificação `full` ×
membership+existência).
## Riscos e mitigação
- **O descriptor não cobre o caminho `next`.** `cmdNext` não usa `cmdNamespaceBranch`; `namespaceLaunchDescriptor` só recebe `plan`/`new`/`fix`. Mitigação: `purpose` desconhecido lança (fail-high), então um futuro uso indevido falha alto em vez de gerar label errado.
- **Mudança de assinatura de `cmdNamespaceBranch` deixa parâmetro morto.** Mitigação: `tsc --noEmit` e a leitura dos 3 callers; nenhum outro caller existe (verificado — só `cmdPlan`/`cmdNew`/`cmdFix`).
- **Label/id divergirem do texto atual exibido.** Mitigação: os casos do novo `describe` fixam os três labels e o `sequencial` byte a byte, contra os literais de `:765`/`:786`/`:812`.
- **Entrada high-risk morta (path renomeado).** Mitigação: asserção de existência em disco no `it` novo de `ciSkipInvariants` para os 3 alvos.
- **F1 forçar `full` em diffs envolvendo essas libs aumenta custo de CI.** Esperado e desejado: são o harness de launch/agent-session; fail-closed é o outcome.
## Aceite de engenharia
- [ ] Aceite de produto da intenção coberto: diff só-lib de launch/agent-session classifica `full` (F1) e o repasse do bag não pode mais ser dropado por parâmetro (F2).
- [ ] Invariantes AGENTS/engineering-standards: identifiers em inglês, prosa pt-BR, sem comentários de código; nenhuma migration; nenhum acesso/Consent/UI tocado.
- [ ] Testes de domínio previstos: `tests/unit/testAffected.unit.spec.ts`, `tests/unit/ciSkipInvariants.unit.spec.ts`, `tests/unit/worktree.unit.spec.ts` — todos unit, sem harness novo.
- [ ] `pnpm gate:fast` verde; `pnpm push` no fluxo normal de PR.
## Self-score decision-quality
1. Decisões caras têm rejeitadas? **5/5** — dono do descriptor, cru-vs-saneado, fail-high-vs-neutro, corte do pin de F1 e cross-pin registraram Opções/Recomendação/Rejeitadas.
2. Cabe no appetite (~1h)? **5/5** — F1 ~30min (3 literais + 2 pins) e F2 ~30min (extração pequena em lib pura + rewire de 3 callers + 1 describe); sem migration, sem e2e.
3. Rabbit holes nomeados? **5/5** — guard `isMain`, prefixo `scripts/lib/` inteiro, existência repo-wide, S1/S2/S3, cross-pin e prefill.
4. Depth check reusa shells/helpers? **4/5** — reusa o spec que já importa `scripts/lib/worktree.mjs`, os padrões de pin de `HIGH_RISK_EXACT` e a convenção existente do módulo; não cria pass-through (o descriptor é dono real da derivação, não repassa nada).
5. Intenção (aceite de produto) intacta? **5/5** — a engenharia não reescreveu o outcome; o F2 foi feito _mais_ forte que a opção A original (o parâmetro some), sem ampliar escopo.
**Score: 24/25 — aprovado para execução.**