docs(plans): register OPS13–15 guardrail hygiene chores

Classname/token pin removal, form-action ladder guard, kill TooltipProvider JSX nesting pin.
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-08-02 13:40:25 -03:00
parent 8c6b838797
commit 8a60ab1ec8
3 changed files with 302 additions and 0 deletions

View file

@ -0,0 +1,103 @@
# Form-action guard — política na escada, não no filename
Status: registrado
Atualizado em: 2026-08-02
Issue: #241 (OPS14)
Priority: P2
Model: composer-2.5
Impeccable: A — N/A (sem superfície UI; convention spec)
Appetite: ~1 dia eng; reescrever guarda em `codebaseConventions` (+ migrar ladders expressíveis se couber no mesmo PR); sem migration
Responsável: —
## Dados → decisão → apresentação
Dados: N/A — higiene de guardrail; sem métrica de produto.
## Contexto
Pass 2 W4d / C8 F4b: `runCampaignFormAction` / `runCampaignRedirectFormAction` unificam a escada try/catch → `mapCampaignFormActionError` → return/redirect. A guarda em `tests/unit/codebaseConventions.unit.spec.ts` só varre arquivos cujo **filename** casa `*FormActions.ts` ou `taskActions.ts`.
**Dodge ledgerado (P3-I / `docs/TECH-DEBT.md` / `docs/GUARDRAILS.md`):** ladders hand-rolled em `src/app/(campaign)/campanha/actions/*.ts` são invisíveis à guarda. Baseline medido no Pass 4: ~7 ladders assim (`profile` avatar×2, `leaderSupporter`, mais `password`/`auth` parcialmente bespoke). Actions novas que **usam** o wrapper (`notifications`, `webauthn`, …) passam por disciplina, não por mecanismo.
Agentes otimizam o gate: colocar mutação nova em `actions/foo.ts` com try/catch + `mapCampaignFormActionError` literal **não quebra o build**, embora viole a política.
Pedido (review 2026-08-02, item 2): **melhorar o pin** — falhar o build quando a **escada** aparece fora do wrapper, independentemente do path.
## Objetivos
- A guarda deixa de filtrar por filename `*FormActions.ts` / `taskActions.ts`.
- Critério positivo: export/`'use server'` action sob `src/app/(campaign)` que implementa a escada (try + `mapCampaignFormActionError` **sem** passar por `runCampaign(Redirect)?FormAction`) é offender, salvo allowlist **com reason** por arquivo (ou por símbolo).
- Allowlist inicial documenta exceções genuínas hoje (`atividades/formActions` unique-violation, `apoiadores/[id]` flatten, `taskActions` optimistic, e o que for realmente inexprimível nos wrappers — `auth`/`password` se ainda forem).
- Ladders **expressíveis com zero delta semântico** (`profile` avatar, `leaderSupporter` se ainda raw) migram para o wrapper **neste item** ou ficam como offenders até migrar no mesmo PR — não expandir allowlist “porque o filename mudou”.
- Atualizar linha em `docs/GUARDRAILS.md` e fechar/anotar P3-I no `TECH-DEBT.md`.
- Guardrails: sem migration; access control inalterado; sem mudar copy de erro.
- **Tracer bullet:** spec que flagra um fixture/`actions/*.ts` com try+mapError sem wrapper → verde após migrate ou allowlist justificada → `pnpm gate:fast`.
## Decisões travadas
- **Detecção = presença da escada (try + `mapCampaignFormActionError`), não do filename.** Opções: A) sweep AST/regex em `(campaign)/**/*.ts` | B) ESLint rule local | C) só ampliar filename para `actions/*.ts`. **Recomendação: A (convention spec)** — mesmo mecanismo da guarda atual, um PR, alinhado a P3-I. **Rejeitado:** C (ainda é proxy de path; `lib/` ou helper colocalizado escapa); B como v1 (appetite maior; Adiado com gatilho se A for dodgeable).
- **Chamada real a `runCampaignFormAction(` / `runCampaignRedirectFormAction(` continua obrigatória** onde a escada existia — comentário nomeando o wrapper **não** passa (já é invariante atual).
- **Não crescer os wrappers** para absorver políticas únicas (unique-violation→fieldErrors, flatten de fieldErrors, optimistic checklist). Essas ficam allowlisted com reason — precedente W4d.
- **Migrar o que for expressível no mesmo PR** quando o custo for mecânico (<~3 call sites citados no ledger). Se um arquivo for grande/bespoke, allowlist + defer com gatilho “próxima edição do arquivo”.
- **Kind: `chore` (OPS14).**
- **i18n:** ids de teste em inglês.
## Questões em aberto
- **Incluir `throw` + mapError fora de try no mesmo padrão?** **Opções:** A) só try+mapError (escada clássica) | B) qualquer `mapCampaignFormActionError(` fora de arquivo que já chama o wrapper. **Recomendação:** **B** — o helper de map é o cheiro da escada hand-rolled; wrappers encapsulam o map. Verificar que nenhum call site legítimo chama mapError direto após a migração. _(assumido)_
- **`actions/*.ts` que só re-exportam / orquestram sem mapError:** fora do radar. OK.
## Abordagem proposta
```mermaid
flowchart LR
walk["walk src/app/(campaign)/**/*.ts"] --> has{"mapCampaignFormActionError( ?"}
has -->|não| ok["ok"]
has -->|sim| wrap{"runCampaign(Redirect)?FormAction( ?"}
wrap -->|sim| ok
wrap -->|não| allow{"allowlist + reason?"}
allow -->|sim| ok
allow -->|não| fail["build fail"]
```
Componentes:
- **`tests/unit/codebaseConventions.unit.spec.ts`:** substituir `isGuardedActionFile` / describe W4d pelo sweep acima; allowlist `Map<path, reason>` (ou Set + comentário adjacente); manter require de call site real do wrapper.
- **Migrações mecânicas (se couber):** `profile.ts` / `leaderSupporter.ts` → `runCampaignFormAction` (espelhar `notifications.ts` / `webauthn.ts`).
- **Allowlist residual:** `atividades/formActions.ts`, `apoiadores/[id]/formActions.ts`, `taskActions.ts`, e auth/password se ainda inexprimíveis — reasons copiados do bloco atual + TECH-DEBT.
- **`docs/GUARDRAILS.md`:** atualizar a linha “filename `*FormActions.ts` — dodge `actions/*.ts`”.
- **`docs/TECH-DEBT.md`:** fechar/anotar P3-I remaining open sobre o dodge de filename.
- **Migration:** nenhuma.
## Dependências
- Nenhuma dura. Independente de OPS13/OPS15.
- Soft: P3-I ledger row.
## Não escopo
- Unificar políticas bespoke (unique violation, flatten) dentro dos wrappers.
- Rotas JSON (`campaignJsonMutationRoute`) — já têm guarda própria.
- OPS13 classname / OPS15 TooltipProvider.
## Rabbit holes
- **"Já que detecto a escada, reescrevo todos os actions do domínio."** Explode. **Mitigação:** migrar só expressíveis; allowlist o resto com gatilho.
- **False positive em testes/helpers.** **Mitigação:** sweep só `src/app/(campaign)`; excluir `*.spec.ts`.
- **AST completo vs regex.** Regex no estilo das outras convention specs basta; se surgir dodge por rename de mapError, ESLint depois.
## Adiado com gatilho
- **ESLint local `require-run-campaign-form-action`.** Revisitar se A for dodgeada ≥1× (rename/`mapError` indireto).
- **Absorver unique-violation / flatten nos wrappers.** Revisitar no 3º call site da mesma política.
## Referências
- GitHub Issue #241 (OPS14)
- Review de guardrails (chat 2026-08-02) — item 2
- `tests/unit/codebaseConventions.unit.spec.ts` — bloco formActions / W4d
- `docs/TECH-DEBT.md` — P3-I (filename guard escapes)
- `docs/GUARDRAILS.md` — linha FormActions
- `src/utilities/campaignFormActionError.ts` — wrappers
- `src/app/(campaign)/campanha/actions/{profile,leaderSupporter,password,auth,notifications,webauthn}.ts`
- AGENTS.md — formActions como cascas sobre `runCampaignFormAction`

View file

@ -0,0 +1,96 @@
# Matar guarda JSX de nesting `TooltipProvider` (B102)
Status: registrado
Atualizado em: 2026-08-02
Issue: #242 (OPS15)
Priority: P2
Model: composer-2.5
Impeccable: A — N/A (sem superfície UI; remover convention spec)
Appetite: ~0,25 dia eng; apagar um `describe` + atualizar GUARDRAILS; sem migration
Responsável: —
## Dados → decisão → apresentação
Dados: N/A — remoção de guardrail frágil; sem métrica de produto.
## Contexto
B102 (#129 / miss #133/#135): o drawer de ações rápidas monta como **irmão** do scrollport dentro de `CampaignAppScrollChrome`. `TooltipProvider` só em volta de `{children}` deixava focus→suggest (hits `priority:alta` → `CampaignHoverTooltip`) sem provider → crash da página.
**Fix real:** elevar o provider para envolver `CampaignAppScrollChrome` no layout `(app)`.
**Guarda adicionada** em `tests/unit/codebaseConventions.unit.spec.ts` (`campaign TooltipProvider wraps quick-actions chrome (B102)`): regex que exige
```tsx
<TooltipProvider>…<CampaignAppScrollChrome>…</CampaignAppScrollChrome>…</TooltipProvider>
```
e proíbe o nesting invertido **no source** de `layout.tsx`.
Problema (review 2026-08-02, item 5): o pin congela **forma JSX incidental**. Agentes satisfazem a regex com wrappers estranhos; um refactor legítimo (provider dentro de um shell component que já envolve o chrome) quebra o build sem regressão; a proteção real do crash já está (ou deve estar) nos **pins comportamentais** de focus→suggest / type→search com hits prioritários (`campaignQuickActionsDrawer` unit + e2e da miss).
Pedido: **matar esta guarda** — sem substituto source-shape.
## Objetivos
- Remover o `describe('campaign TooltipProvider wraps quick-actions chrome (B102)', …)` de `codebaseConventions.unit.spec.ts` por completo.
- Confirmar que os pins comportamentais da regressão B102/miss (#133/#135) **permanecem** (unit drawer focus→suggest com hit prioritário; e2e se existir). Se algum tiver sido apagado, **restaurar** nesse PR — a remoção da guarda source só é segura com a rede comportamental viva.
- Atualizar `docs/GUARDRAILS.md` se a guarda estiver listada; senão nota no PR + referência à Issue.
- Guardrails: sem migration; **não** mover o `TooltipProvider` neste item (layout fica como está).
- **Tracer bullet:** delete do describe → `pnpm gate:fast` verde → checklist dos pins comportamentais no body do PR.
## Decisões travadas
- **Delete da guarda source-shape; não “afrouxar” a regex.** Opções: A) delete | B) mover o pin para outro arquivo/componente | C) ESLint react-tree. **Recomendação: A.** O valor está no pin de crash (comportamento), não na árvore textual do layout. **Rejeitado:** B/C (mesmo cheiro / appetite).
- **Não adicionar judgment-only substituto obrigatório** além de uma linha em GUARDRAILS/CHANGELOG se útil — o layout comentado + pins de miss bastam.
- **Kind: `chore` (OPS15).**
- **i18n:** N/A.
## Questões em aberto
- **Onde documentar “provider deve envolver o chrome do drawer”?** **Opções:** A) só comentário no layout | B) GUARDRAILS judgment-only | C) nada. **Recomendação:** **A** (já deve existir contexto B102 no layout); B só se a linha já existir e precise de “removido”. _(assumido)_
## Abordagem proposta
```mermaid
flowchart LR
check["Ver pins unit/e2e B102"] --> del["Apagar describe nesting"]
del --> docs["GUARDRAILS se listado"]
docs --> gate["gate:fast"]
```
Componentes:
- **`tests/unit/codebaseConventions.unit.spec.ts`:** remover o bloco B102 (~L671–688).
- **Verificação:** `rg TooltipProvider|priority:alta|CampaignHoverTooltip tests/unit tests/e2e` — garantir cobertura comportamental; restaurar se faltar.
- **`docs/GUARDRAILS.md`:** remover/ajustar linha se houver.
- **Migration:** nenhuma.
## Dependências
- Nenhuma dura. Independente de OPS13/OPS14.
- Soft: Issues #129 / #133 / #135 (histórico da miss) — só referência.
## Não escopo
- Mudar posição do `TooltipProvider` no layout.
- Reabrir investigação do crash.
- OPS13/OPS14.
## Rabbit holes
- **"Substituir por teste de árvore React renderizada do layout."** Layout é RSC/async — painel de dor; os pins do drawer já exercitam o caminho. **Mitigação:** não.
- **Aproveitar para “limpar” outros describes do conventions.** Fora de escopo.
## Adiado com gatilho
- **Pin e2e dedicado se unit do drawer for flaky.** Revisitar só com evidência de regressão pós-remoção.
## Referências
- GitHub Issue #242 (OPS15)
- Review de guardrails (chat 2026-08-02) — item 5
- `tests/unit/codebaseConventions.unit.spec.ts` — describe B102
- `src/app/(campaign)/campanha/(app)/layout.tsx` — TooltipProvider atual
- Issues #129 (B102), #133, #135 (miss)
- `tests/unit/campaignQuickActionsDrawer.unit.spec.tsx` — pins comportamentais esperados

View file

@ -0,0 +1,103 @@
# Remover pins de className / tokens Tailwind literais
Status: registrado
Atualizado em: 2026-08-02
Issue: #240 (OPS13)
Priority: P2
Model: composer-2.5
Impeccable: A — N/A (sem superfície UI; só testes de convenção / pin unitário)
Appetite: ~0,5–1 dia eng; editar specs unitárias; sem migration; sem mudança de produto obrigatória
Responsável: —
## Dados → decisão → apresentação
Dados: N/A — higiene de guardrails de teste; sem métrica de produto.
## Contexto
Review de guardrails (2026-08-02): vários unit specs congelam **strings completas de `className`**, sequências Tailwind e tokens hex de CSS. Exemplos vivos:
- `tests/unit/campaignComponents.unit.spec.ts` — `className="h-svh min-h-0 overflow-hidden print:h-auto print:overflow-visible"`; sticky `sticky left-0 z-20…min-w-56…`; `--sidebar: #fafaf9` / foreground hex
- `tests/unit/campaignHomeActionButton.unit.spec.tsx` — `w-[5.5rem]`, `gap-0`, `px-4`
- `tests/unit/campaignHomeLayout.unit.spec.tsx` — `-mx-4`, `w-[calc(100%+2rem)]`, `grid-rows-[0fr]`, `opacity-0`
- `tests/unit/campaignWizardShell.unit.spec.tsx` — `pt-2` vs `pt-3`, `pb-6`
- `tests/unit/campaignQuickActionsDrawer.unit.spec.tsx` — `-mx-4`, `w-[calc(100%+2rem)]`
- `tests/unit/homeSearchResultsLayout.unit.spec.tsx` — `flex-col` / `md:grid-cols-2` / `lg:grid-cols-3`
O intent original (shell fixa + scroll no content pane; coluna Município sticky; touch target; contraste da sidebar) é legítimo. O **mecanismo** (literal de utilitários) empurra agentes a: (a) não mexer no layout com bug real; (b) satisfazer a string e colocar o comportamento em outro arquivo; (c) “passar” o gate sem preservar a invariante.
Pedido: **remover** esses pins de classname/token e, onde a invariante ainda importa, **substituir por pin comportamental** (`data-slot`, role/ARIA, presença de região de scroll, não-presença de `title=` onde deve ser texto acessível, etc.).
## Objetivos
- Nenhum unit spec sob `tests/unit/` exige igualdade/substring de uma **lista completa de utilitários Tailwind** (ex. string multilasse colada do JSX) nem hex de token CSS como prova de layout.
- Invariantes que ainda valem ficam pinadas por **contrato estável**: `data-slot`, `data-*` de domínio, roles/ARIA, uso de constantes exportadas (`HOME_SEARCH_*_CLASS` **como símbolo**, sem reassertar o conteúdo da constante), ou comportamento renderizado (texto acessível, overflow scrollport presente).
- Specs que só existiam para congelar spacing/sizing cosmético (`pt-2` vs `pt-3`, largura exata do botão) **somem** ou viram assert do contrato exportado, sem re-listar utilities.
- Atualizar `docs/GUARDRAILS.md` / nota curta se alguma linha citar esses pins; entrada no ledger se P4-H-adjunto não se aplicar.
- Guardrails: sem migration; sem mudança de UI obrigatória neste PR (só testes/docs). Se um pin comportamental exigir `data-slot` novo no shell, é permitido e mínimo.
- **Tracer bullet:** apagar o `toContain('className="h-svh…')` em `campaignComponents` → substituir por assert em `data-slot="campaign-content-scroll"` + overflow no scrollport (já parcialmente presente) → `pnpm gate:fast` verde.
## Decisões travadas
- **Remover literais de className/token; não “afrouxar” para regex parcial da mesma string.** Opções: A) delete puro | B) delete + pin comportamental onde havia invariante de produto | C) manter e documentar judgment-only. **Recomendação: B.** A sozinho perde cobertura real (shell scroll / sticky / a11y). C perpetua o incentivo a shortcut. **Rejeitado:** C; “só comentar o expect”.
- **Constantes exportadas (`HOME_SEARCH_*_CLASS`) podem continuar sendo referenciadas por identidade (`toBe(CONST)`), sem `toContain('rounded-md')` adjacente que recongele o conteúdo.** Se o spec hoje compara `className === CONST` **e** ainda lista utilities, manter só a igualdade ao símbolo. **Rejeitado:** expandir pins para o valor string da constante.
- **Hex de `--sidebar*`:** provar contraste/tema via token name (`text-sidebar-foreground`) ou snapshot de contrato de tema se existir; não o valor `#fafaf9`. **Rejeitado:** pin de palette hex em unit (design tokens mudam sem bug).
- **Sticky Município:** pinar `data-slot` / sticky via atributo estável ou teste de presença da célula sticky sem a sequência `z-20…min-w-56…`. **Rejeitado:** regex da class completa.
- **Kind: `chore` (OPS13).** Engenharia de guardrail, não feature de `/campanha`.
- **i18n:** ids de teste/data-slot em inglês; copy pt-BR intocada.
## Questões em aberto
- **Incluir também asserts `min-h-11` / `min-w-11` (touch target)?** **Opções:** A) remover junto (são classname) | B) manter como proxy de touch target até haver pin computed-style/e2e. **Recomendação:** **A neste item** — o produto já tem regra em `campanha-edit-where-you-see`; o literal Tailwind é o mesmo cheiro. Se regressão de touch aparecer, e2e/comportamental depois. _(assumido)_
- **`campaignCompositionCleanup` `w-full` no submit?** **Opções:** A) no escopo | B) fora (é um assert pontual de form). **Recomendação:** **A** se for `className` literal; fora se for só layout smoke sem lista de utilities. _(assumido: incluir se for toContain de utility)_
## Abordagem proposta
```mermaid
flowchart LR
audit["Listar expects className/token"] --> classify{"Invariante de produto?"}
classify -->|não| delete["Apagar expect"]
classify -->|sim| replace["Pin data-slot / ARIA / símbolo"]
delete --> gate["gate:fast"]
replace --> gate
```
Componentes:
- **Auditoria mecânica:** `rg "className|toMatch\\(/sticky|--sidebar: #" tests/unit` — checklist no PR; cada hit classificado delete vs replace.
- **`tests/unit/campaignComponents.unit.spec.ts`:** remover strings de layout/sidebar hex/sticky utilities; conservar `data-slot="campaign-content-scroll"`, `data-scope`, badges semânticos, a11y de prioridade/classe territorial (`sr-only` vs `title` — esse pin é de **conteúdo acessível**, não de classname de layout; **manter**).
- **Home / wizard / drawer / search layout specs:** cortar expects de spacing/bleed/grid utilities; onde B115/B116 etc. dependem de contrato, apontar para constante exportada ou `data-slot` já existente.
- **`docs/GUARDRAILS.md`:** se alguma linha referenciar pin de classname, atualizar; senão só o PR das Issues.
- **Migration:** nenhuma.
## Dependências
- Nenhuma dura. Independente de OPS14/OPS15.
- Soft: issues de UI mobile (B115/B116) podem conflitar em specs de home — rebase trivial; não bloqueia registro.
## Não escopo
- Reescrever o shell visual / redesign.
- Matar pins de **comportamento** (`data-pending`, `aria-busy`, `data-slot` semântico, texto acessível vs `title`).
- OPS14 (form-action por política) e OPS15 (TooltipProvider nesting).
- ESLint banindo Tailwind arbitrary values.
## Rabbit holes
- **"Já que mexo no spec, refatoro o layout".** Explode appetite. **Mitigação:** só testes (+ `data-slot` mínimo se faltar âncora).
- **Substituir classname por snapshot HTML enorme.** Mesmo cheiro. **Mitigação:** asserts pontuais de contrato.
- **Computed style em jsdom.** Frágil e barulhento. **Mitigação:** não neste item; data-slot/ARIA bastam.
## Adiado com gatilho
- **Pin e2e de scroll/sticky real.** Revisitar se regressão de shell voltar após remoção dos literais.
- **Lint `no-restricted-syntax` contra `toContain('min-h-` em tests.** Só se agentes reintroduzirem o padrão ≥2×.
## Referências
- GitHub Issue #240 (OPS13)
- Review de guardrails (chat 2026-08-02) — item 1
- `tests/unit/campaignComponents.unit.spec.ts` (shell + sticky + sidebar)
- `tests/unit/campaignHomeLayout.unit.spec.tsx`, `campaignHomeActionButton.unit.spec.tsx`, `campaignWizardShell.unit.spec.tsx`, `homeSearch*.unit.spec.tsx`, `campaignQuickActionsDrawer.unit.spec.tsx`
- `docs/GUARDRAILS.md` — escada de determinismo
- AGENTS.md / engineering-standards — gates; não congelar incidental syntax