Replace the W4d filename-based convention spec with a sweep of every src/app/(campaign) file that calls mapCampaignFormActionError without runCampaignFormAction/Redirect. Migrate profile, leaderSupporter, and convite-login to the shared wrappers; allowlist auth/password and the existing bespoke formActions exceptions with documented reasons. Closes #241 Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
6.9 KiB
Form-action guard — política na escada, não no filename
Status: entregue (OPS14 #241, 2026-08-02)
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 sobsrc/app/(campaign)que implementa a escada (try +mapCampaignFormActionErrorsem passar porrunCampaign(Redirect)?FormAction) é offender, salvo allowlist com reason por arquivo (ou por símbolo). - Allowlist inicial documenta exceções genuínas hoje (
atividades/formActionsunique-violation,apoiadores/[id]flatten,taskActionsoptimistic, e o que for realmente inexprimível nos wrappers —auth/passwordse ainda forem). - Ladders expressíveis com zero delta semântico (
profileavatar,leaderSupporterse 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.mde fechar/anotar P3-I noTECH-DEBT.md. - Guardrails: sem migration; access control inalterado; sem mudar copy de erro.
- Tracer bullet: spec que flagra um fixture/
actions/*.tscom 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 paraactions/*.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) qualquermapCampaignFormActionError(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/*.tsque só re-exportam / orquestram sem mapError: fora do radar. OK.
Abordagem proposta
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: substituirisGuardedActionFile/ describe W4d pelo sweep acima; allowlistMap<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(espelharnotifications.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— dodgeactions/*.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/mapErrorindireto). - 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 / W4ddocs/TECH-DEBT.md— P3-I (filename guard escapes)docs/GUARDRAILS.md— linha FormActionssrc/utilities/campaignFormActionError.ts— wrapperssrc/app/(campaign)/campanha/actions/{profile,leaderSupporter,password,auth,notifications,webauthn}.ts- AGENTS.md — formActions como cascas sobre
runCampaignFormAction