diff --git a/docs/plans/form-action-guard-escada.md b/docs/plans/form-action-guard-escada.md new file mode 100644 index 00000000..fc3e5d5f --- /dev/null +++ b/docs/plans/form-action-guard-escada.md @@ -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` (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` diff --git a/docs/plans/matar-guarda-tooltip-provider.md b/docs/plans/matar-guarda-tooltip-provider.md new file mode 100644 index 00000000..50eadb08 --- /dev/null +++ b/docs/plans/matar-guarda-tooltip-provider.md @@ -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 +……… +``` + +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 diff --git a/docs/plans/remover-pins-classname.md b/docs/plans/remover-pins-classname.md new file mode 100644 index 00000000..db17b341 --- /dev/null +++ b/docs/plans/remover-pins-classname.md @@ -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