teqo-backup/docs/plans/worktree-modelo-canonico-deepseek-flash-impl.md
2026-09-12 02:17:37 -03:00

136 lines
16 KiB
Markdown

# Impl: Modelo padrão do worktree → DeepSeek V4.1 Flash (`deepseek/deepseek-flash`)
Status: aprovado (gate humano 2026-09-12)
Atualizado em: 2026-09-12
Issue: #944
Intenção: docs/plans/worktree-modelo-canonico-deepseek-flash.md
Appetite restante: ~0,5 dia eng (herdado; config-only + textos + pins)
## Leitura da intenção
- **Outcome:** o fluxo sem flag (`pnpm worktree next/plan/new/fix` → `launch opencode <dir> --model <preset> --auto [--prompt …]`) nasce em `deepseek/deepseek-flash`; os 4 commands do fluxo deixam de pinar modelo no frontmatter; help/docblocks/comentários citam o nome novo; override e as 7 flags intactos; unit de preset/frontmatter verde com os literais novos.
- **O que NÃO negociar:**
- `OPENCODE_WORKTREE_MODEL` segue como override (precedência env > fallback) e as 7 flags (`--cheap/--pro/--zen/--go/--alibaba/--glm/--free`) + `WORKTREE_MODEL_MAP` intactos — incluindo os aliases legados de outros providers (`cheapestinference/deepseek-v4-flash`, `alibaba-token-plan/deepseek-v4-flash`).
- Nada além do nome do modelo muda: sem `--variant`/variantes/Ctrl+T, sem config global, sem `opencode.json`, sem mudança de lógica no CLI.
- Usos diretos do app (`src/app/(campaign)/campanha/api/ai-chat/route.ts:62`, `src/utilities/ai/campaignDemandTitle.ts:31`) fora — plano irmão Sollinha; docs históricos congelados.
- É future-proofing de nome canônico, **não** economia (o alias legado já é servido pelo V4.1 ao preço Flash).
- **O que reavaliar:**
- Remover o pin `model:` dos 4 commands muda a semântica desejada: o comando passa a **herdar o modelo da sessão** (o `--model` do launch) em vez de forçá-lo por cima — é isso que faz flags/override valerem dentro da sessão. Precisa de guard de teste para não regredir (decisão 1).
- `tests/unit/worktree.unit.spec.ts:46` (`presetInEffect()`) duplica o literal do fallback — sem trocar junto, o unit quebra no CI (env não exportado). Trocar na mesma fase da constante.
- `tests/unit/opencodeCommands.unit.spec.ts:36-38` é o único guard do frontmatter dos 4 commands — tem de mudar na mesma entrega (decisão 1).
- O docblock do preset (`scripts/lib/worktree.mjs:22-35`) cita `.forgejo/worktree.env` como caminho de override por repo — referência stale (nenhum código lê, pós-cutover GitHub). Fica **fora** desta entrega (não é o nome do modelo); registrar como follow-up.
- `scripts/worktree.mjs:877` (help) interpola `${OPENCODE_PRESET_MODEL}` — auto-atualiza; não há literal para editar ali (só o docblock `:30` tem o literal).
## Estado atual verificado (explorador)
- **Fonte executável única:** `OPENCODE_PRESET_MODEL` em `scripts/lib/worktree.mjs:36-37` (`process.env.OPENCODE_WORKTREE_MODEL || 'deepseek/deepseek-v4-flash'`); consumida em `resolveWorktreeModel` (`:74`) e `opencodeLaunchDirective` (`:214`). `WORKTREE_MODEL_MAP:49` (`cheapestinference/...`) e `:53` (`alibaba-token-plan/...`) ficam intactos.
- **Superfícies com o literal legado do preset (fora de docs):** `scripts/lib/worktree.mjs:37` (constante), `scripts/worktree.mjs:30` (docblock; help `:877` interpola a constante, sem literal), `.agents/shell/worktree.sh:22`, `.agents/skills/worktree-next-issue/SKILL.md:34`, `.opencode/commands/work-issue.md:3` / `plan-issue.md:3` / `bug-fix.md:3` / `testing-audit.md:3`, `tests/unit/worktree.unit.spec.ts:46` (espelho) e `:245` (título), `tests/unit/opencodeCommands.unit.spec.ts:37` (regex).
- **Ausência de pin já é padrão do repo:** `.opencode/commands/issue.md` e `worktree.md` não têm `model:` no frontmatter.
- **Diretiva cobre os 4 propósitos:** `cmdNext` (`scripts/worktree.mjs:619`), `cmdNamespaceBranch` (`:706`, usado por `plan`/`new`/`fix` via `cmdFix:762`) chamam `printLaunchDirective` → `opencodeLaunchDirective`.
- **Blast radius:** `scripts/worktree.mjs` está em `HIGH_RISK_EXACT` (`scripts/lib/test-affected-core.mjs:36`) → diff nele = unit/int full + e2e curado no PR (OPS86). `scripts/lib/worktree.mjs` não é high-risk.
- **Fora do fluxo (intocados):** usos do app em `ai-chat/route.ts:62` e `campaignDemandTitle.ts:31`; `opencode.json`, README, `.husky`, workflows e pool sem refs; `deepseek-v4-flash-vision-exp` sem ocorrência viva.
## Abordagem recomendada
```mermaid
flowchart LR
CLI["pnpm worktree next/plan/new/fix\nsem flag"] --> LIB["scripts/lib/worktree.mjs\nOPENCODE_PRESET_MODEL = env || deepseek/deepseek-flash"]
LIB --> DIR["opencodeLaunchDirective\n--model deepseek/deepseek-flash --auto --prompt"]
DIR --> SH[".agents/shell/worktree.sh\ncd + launch"]
SH --> TUI["TUI opencode\nsessão no nome canônico"]
CMD[".opencode/commands/*.md\nfrontmatter SEM model:"] -->|herda a sessão| TUI
GUARD["opencodeCommands.unit.spec.ts\nausência de pin de modelo"] -.->|protege| CMD
TEST["worktree.unit.spec.ts\npresetInEffect canônico"] -.->|protege| LIB
ENV["OPENCODE_WORKTREE_MODEL"] -.->|override intacto| LIB
FLAGS["7 flags do WORKTREE_MODEL_MAP"] -.->|mapa intacto| LIB
```
**Opções consideradas:** A | B | C
**Recomendação:** A — trocar o fallback na constante única + remover o pin `model:` dos 4 commands + trocar o guard do frontmatter para proibir qualquer pin + sincronizar os textos vivos e os pins de teste. É o menor diff que cumpre o aceite integral e ataca a causa (o pin que sobrescreve a sessão), sem tocar mapa, override ou config.
**Rejeitadas:**
- **B — preset canônico + pin trocado para `deepseek/deepseek-flash` nos 4 commands:** cumpre o nome, mas mantém o acoplamento que anula as flags/override dentro da sessão e recria o débito na próxima renomeação; a intenção manda "deixar de pinar".
- **C — preset canônico apenas (commands intocados):** deixaria o frontmatter pinando o legado — falha o aceite (2) e mantém o override do pin sobre a sessão.
### Decisões de engenharia (caras, com rejeitadas)
1. **Guard do frontmatter após remover o pin (decisão cara desta entrega)**
- Hoje: `tests/unit/opencodeCommands.unit.spec.ts:36-38` exige `model: deepseek/deepseek-v4-flash` no frontmatter de cada um dos 4 commands.
- Opções: **A)** remover a linha `model:` e trocar a asserção por **ausência de qualquer pin** (`expect(frontmatter![1]).not.toMatch(/^model:/m)`); **B)** remover a linha e apagar a asserção; **C)** manter o pin, só trocando o valor para o canônico.
- **Recomendação: A** — única que preserva a intenção ("deixam de pinar") **e** evita regressão silenciosa: se alguém reintroduzir um pin (legado ou canônico), o teste falha e obriga a decisão explícita. Ausência de pin é o padrão já praticado no repo (`.opencode/commands/issue.md` e `worktree.md` vivem sem `model:`), e o comando herda o modelo da sessão — que o launch define via `--model` (preset/flag/override). Forma: remover a linha dos 4 arquivos (frontmatter fica só com `description`); a asserção continua dentro do `it.each(commands)` e cobre os 4 de uma vez, com mensagem explicando que o modelo vem da sessão.
- **Rejeitadas:** **B** — apagar a asserção deixa o frontmatter sem guard: um pin reintroduzido passa em silêncio e volta a anular flags/override (exatamente a classe de bug desta entrega). **C** — contradiz o aceite (2) e recria o acoplamento: com pin, `/work-issue` numa sessão `--cheap`/`--pro` roda no modelo pinado, não no da sessão; a próxima renomeação repetiria a OPS101.
2. **Espelho do fallback no teste (`presetInEffect()`)**
- Opções: **A)** trocar o literal nos dois pontos (constante e espelho); **B)** exportar um `DEFAULT_PRESET_MODEL` e importar no teste; **C)** o teste comparar `OPENCODE_PRESET_MODEL` consigo mesmo.
- **Recomendação: A** — o espelho é deliberado (comentário `:41-45`: "o lib resolve exatamente nessa precedência") e é o que detecta deriva; sem env no CI, espelho = fallback e a comparação em `:246` falha se um dos dois ficar para trás. Menor diff, sem superfície nova.
- **Rejeitadas:** **B** — abstração prematura para um literal com 2 usos (engineering-standards: sem abstração especulativa); **C** — tautologia; teste que sempre passa não protege nada.
3. **Escopo da sincronização de textos**
- Opções: **A)** trocar o literal do preset nas superfícies vivas levantadas (docblock de `scripts/lib/worktree.mjs`, `scripts/worktree.mjs:30`, `.agents/shell/worktree.sh:22`, `SKILL.md:34`, título do teste `:245`); **B)** só a constante; **C)** varrer o repo inteiro por `deepseek-v4-flash`.
- **Recomendação: A** — o aceite pede "textos/help/docblocks refletem o nome novo"; B deixa a doc mentindo; C atinge aliases de outros providers e os usos do app (fora de escopo).
- **Rejeitadas:** **B** — os 3 textos fixos ficariam errados (só o help auto-atualiza); **C** — quebra o anti-goal dos aliases e invade o plano irmão do app.
### Componentes / mudanças
- **`OPENCODE_PRESET_MODEL`** (`scripts/lib/worktree.mjs:36-37`): fallback `'deepseek/deepseek-v4-flash'` → `'deepseek/deepseek-flash'`; `process.env.OPENCODE_WORKTREE_MODEL ||` intacto. Docblock `:22-35`: 1 frase registrando o nome canônico (OPS101, V4.1 Flash) — sem tocar no caminho stale `.forgejo/worktree.env` (fora de escopo). `WORKTREE_MODEL_MAP:49/:53` intactos.
- **`scripts/worktree.mjs:30`** (docblock): literal → nome novo. Help `:877` não muda (interpola a constante). Nenhuma mudança de lógica.
- **`.agents/shell/worktree.sh:22`** (comentário): literal → nome novo; aliases `:19`/`:21` intactos.
- **`.agents/skills/worktree-next-issue/SKILL.md:34`**: prosa do preset → nome novo; aliases da mesma linha intactos.
- **`.opencode/commands/work-issue.md:3`, `plan-issue.md:3`, `bug-fix.md:3`, `testing-audit.md:3`**: remover a linha `model: deepseek/deepseek-v4-flash` (frontmatter fica só com `description`).
- **`tests/unit/worktree.unit.spec.ts:46`**: `presetInEffect()` → literal novo; `:245` título do teste → nome novo (cosmético; evita doc mentirosa); `:294`/`:298` aliases intactos.
- **`tests/unit/opencodeCommands.unit.spec.ts:36-38`**: asserção → ausência de pin (decisão 1).
- **Migration:** sem migration — tooling/config-only, nenhuma collection/global.
- **Access / Consent:** N/A — sem coleção, sem PII, sem write path.
- **UI:** Impeccable A — N/A sem UI (verificação = linha de launch + unit).
### Dados → forma (se aplicável)
N/A — config-only de tooling; nenhum dado de negócio modelado (pergunta 3 de data-presentation não se aplica).
## Fases verificáveis
1. **TDD do preset** — `tests/unit/worktree.unit.spec.ts:46/:245` primeiro (vermelho: constante ainda legada), depois `scripts/lib/worktree.mjs` (constante + docblock); `pnpm test:unit -t worktree` verde, com e sem `OPENCODE_WORKTREE_MODEL` exportado. Quota ~30%.
2. **TDD do frontmatter** — trocar o guard em `opencodeCommands.unit.spec.ts:36-38` para ausência de pin (vermelho com os pins atuais), depois remover a linha `model:` dos 4 `.opencode/commands/*.md`; `pnpm test:unit -t opencodeCommands` verde; `grep -n "^model:" .opencode/commands/*.md` vazio. Quota ~30%.
3. **Textos vivos + gates + entrega** — `scripts/worktree.mjs:30`, `.agents/shell/worktree.sh:22`, `SKILL.md:34`; smoke seguro (sem claim) `node -e "import('./scripts/lib/worktree.mjs').then(m => console.log(m.opencodeLaunchDirective({ dir: '/tmp/wt', purpose: 'next', terminal: true })))"` → `--model deepseek/deepseek-flash`; grep final de `deepseek/deepseek-v4-flash` fora de `docs/` deve sobrar só os 2 usos do app (Sollinha); changelog NOVA entrada `docs/changelog/2026-09-12-ops101.md`; `pnpm gate:fast`; `pnpm push` → PR `Closes #944`. Nota: `scripts/worktree.mjs` é `HIGH_RISK_EXACT` (`scripts/lib/test-affected-core.mjs:36`) → PR roda unit/int full + e2e curado (OPS86), mesmo com diff só de docblock. Quota ~40%.
## Rabbit holes / Não escopo (engenharia)
- NÃO trocar o cardápio/mapa (`WORKTREE_MODEL_MAP`, chaves ou values) nem os aliases de outros providers.
- NÃO mexer em variante/thinking/`--variant`/Ctrl+T/config global/`opencode.json` do repo.
- NÃO "vender economia" — registrar como nome canônico/future-proofing.
- NÃO trocar os usos diretos do app (plano irmão Sollinha: `ai-chat/route.ts:62`, `campaignDemandTitle.ts:31`).
- NÃO corrigir o caminho stale `.forgejo/worktree.env` no docblock do preset (follow-up; não é o nome do modelo).
- NÃO editar docs históricos (`docs/plans/*` antigos, `docs/changelog/*`, `docs/CHANGELOG-AGENTS*`).
- NÃO criar constante/abstração nova para o fallback (2 usos; espelho do teste é o guard).
- NÃO varrer o repo inteiro por `deepseek-v4-flash` (atinge app e aliases).
- NÃO usar `pnpm worktree next` como smoke (claimaria Issue real) — o smoke é a chamada direta da lib.
## Triage de débitos (simplify, 2026-09-12)
6 colhidos / 2 já resolvidos na sessão / 2 descartados / 2 deferidos com gatilho / 0 registrados (nenhum score ≥3).
- **Já resolvido (não reabrir):** gramática do docblock (`scripts/lib/worktree.mjs`, "passou a usar o nome canônico") e fragilidade da regex do guard (`opencodeCommands.unit.spec.ts:39` → `/^\s*["']?model["']?\s*:/m`).
- **Descartado:** espelho `presetInEffect()` inerte com `OPENCODE_WORKTREE_MODEL` exportada (deliberado e documentado em `worktree.unit.spec.ts:41-45`; o CI exercita o literal) e docblock stale `.forgejo/worktree.env` (pré-existente, doc-only, parkeado como follow-up).
- **Adiado com gatilho:**
- Literais do preset em comentários vivos (`scripts/worktree.mjs:30`, `.agents/shell/worktree.sh:22`, `.agents/skills/worktree-next-issue/SKILL.md:34`) sem guard de sincronização — **gatilho:** próxima troca de preset deixar um dos textos divergente.
- Guard de frontmatter cobre os 4 commands do array (`opencodeCommands.unit.spec.ts:13`), não os 6 arquivos de `.opencode/commands/` — **gatilho:** novo command de execução (ou pin em command fora do array).
## Riscos e mitigação
- **Sessões manuais que invocam `/work-issue` fora do launch passam a herdar o modelo da sessão** (antes o pin forçava o legado) → é o comportamento desejado (o launch/flags/override mandam); registrar como mudança intencional no PR.
- **HIGH_RISK_EXACT (`scripts/worktree.mjs`)** → PR roda unit/int full + e2e curado; mitigação: tocar só docblock (`:30`), sem claim/provision/PORT/env.
- **Deriva entre constante e `presetInEffect()`** → CI sem env quebra; mitigação: trocar os dois na mesma fase (Fase 1).
- **Pin reintroduzido por engano** → guard de ausência (Fase 2) falha com mensagem explicando a origem do modelo.
- **Varredura ampla apagar aliases do mapa** → mitigação: troca só do literal `deepseek/deepseek-v4-flash` (com namespace), nunca dos aliases; pins `:294/:298` cobrem o mapa.
- **Usos do app trocados por engano** → mitigação: grep final mostra os 2 `deepSeek('deepseek-v4-flash')` intocados.
- **Override `OPENCODE_WORKTREE_MODEL`** → intacto (só o fallback muda); teste cobre a precedência env > fallback.
## Aceite de engenharia
- [ ] Aceite de produto da intenção ainda coberto: preset sem flag = `deepseek/deepseek-flash`; 4 commands sem pin; help/docblocks/comentários com o nome novo; override + 7 flags intactos; unit verde com literais novos.
- [ ] Invariantes AGENTS/engineering-standards: sem collection/global/migration; sem twin (constante única editada no dono); Consent/LGPD intocados; `push:false` preservado; `pnpm gate:fast` verde; identificadores em inglês.
- [ ] Testes de domínio previstos (unit/int) onde access/write paths mudam: N/A (sem access/write path) — unit de preset (`worktree.unit.spec.ts`) e de frontmatter (`opencodeCommands.unit.spec.ts`) atualizados e verdes; smoke da diretiva registrado no PR.
---
Self-score decision-quality: 5/5 — decisão cara (guard do frontmatter) deliberada com rejeitadas e forma exata; cabe no appetite (constante + 4 remoções de linha + 3 textos + 2 specs); rabbit holes nomeados com gatilhos; depth check reusa o dono (`OPENCODE_PRESET_MODEL`) e os guards existentes sem criar twin; outcome da intenção preservado (incluindo o não-ganho de economia).