11 KiB
Escala e DRY pós-C8 (apoiadores / filtros / forms)
Status: implementado (Fases 1–4, 2026-07-19) Atualizado em: 2026-07-19 Item do roadmap: docs/roadmap.md (Trilha C, item C9) Responsável: —
Contexto
O C8 (escala-dry-pos-c6.md) entregou a segunda camada de escala/DRY do cadastro nominal: locks advisory em um round-trip, bulk import com .returning() e leituras drizzle na txn, overview sem COUNT(*) redundante, migration pg_trgm em contact, helper drizzleBulk.ts, variante getCoordinatorNucleusIds, e migração da maioria dos formActions para mapCampaignFormActionError + campaignFormFields. Duas passagens /simplify (pré- e pós-rebase em main com C7) aplicaram limpezas adicionais (select: { id: true } via DynamicFind, prefetch de núcleos do coordenador para o overview, chunk de IN no import, drizzleResultRows no campaignInviteRepository, errorProps no CampaignInviteForm, safeMessages opcional no mapper).
Os revisores (quality / reuse / performance) marcaram como importantes e maiores que cleanup os follow-ups abaixo. Sem registro, a lista de apoiadores pode divergir entre Payload e SQL agregado, o coordenador paga 2–3 lookups idênticos por navegação, e o DRY de forms fica incompleto na borda da vertical.
- Filtros de apoiadores duplicados.
buildSupporterListWhere(src/utilities/supporterUi.ts, PayloadWhere) ebuildAggregateSql(src/utilities/supporterListOverviewAggregate.ts, SQL cru) reimplementam a mesma semântica (telefone normalizado,q,voteIntention,city,nucleus). O C8 derivoutotalda lista, mas os dois caminhos ainda podem divergir em qualquer mudança futura. contactSearchQuerynão wireado no aggregate. C7 entregouisContactSearchQueryReady/CONTACT_SEARCH_MIN_LENGTHemsrc/lib/contactSearchQuery.ts(mín. 2 chars) para comboboxes; o aggregate SQL do overview ainda aplicaILIKE '%q%'paraqde 1 caractere — desperdiça opg_trgmdo C8.- Lookup de núcleos do coordenador repetido na página de apoiadores.
getCoordinatorNucleusIdsroda no prefetch do overview, masloadSupporterListPageDataeloadAccessibleNucleusOptionsdisparam access separado (getAccessibleNucleusIds→ mesma query) semreq.contextcompartilhado em RSC — até 3× por navegação paracoordenador. apoiadores/[id]/formActions.tsfora do mapper compartilhado. C8 migrou ~8formActions, mas o detalhe do apoiador mantém ladder inline com contrato de erro diferente (messageem validação Zod, nãofieldErrors) — drift e risco de regressão se migrado sem desenho.- Componentes de form com
fieldErrorlocal.NucleusForm,ActionPlanForm,NucleusTerritoryFields,VoteEstimateDialog,ActionPlanUpdateForm, etc. ainda usamerrorForoufieldErrors?.[0]em vez decampaignFormFields.ts— fora do escopo mínimo do C8 Fase 4 (sóNucleusUpdateForm+CampaignInviteForm).
Explicitamente fora (revisores pediram skip no simplify): remover o waterfall lista→overview (tradeoff intencional para totalDocs correto); unificar lista+aggregate numa query só (só se latency reclamar); migrar collectLockParams do teste de locks; widen de PostgresTransactionDatabase (follow-up de plataforma, coberto parcialmente por assertDrizzleColumns).
Objetivos
- Um único núcleo de intenção de filtro de apoiadores alimenta Payload (
Where) e SQL agregado — sem drift entre KPIs e lista. - Busca
qno overview (e, se ainda faltar, na lista Payload) respeitacontactSearchQuery(mín. 2 chars ou dígitos suficientes), alinhado ao C7. - Página
/campanha/apoiadoresresolve escopo de núcleos do coordenador uma vez por render e repassa para list/overview/options. - Forms restantes da campanha usam
mapCampaignFormActionError/fieldError/errorPropssem quebrar contratos de UI existentes. - Guardrails: sem novo
Consent; sem migration salvo se unificar filtros exigir índice novo (improvável);overrideAccess: falsecomuser; locks advisory xact-level inalterados.
Decisões travadas
- Um item C9, quatro fases ordenadas. Mesmo racional do C6/C7/C8: um ID de roadmap, PRs por fase. Ordem: filtros (correção) →
contactSearchQuery+ loader da página → forms restantes (detalhe + componentes). - Dependência dura de C8 (merge). O código otimizado é o do C8; não reabre decisões de v1 do C6 (token HMAC,
supporterImportBatch,skipContactPhoneInvariant). - Dependência suave de C7 para
contactSearchQuery— já emmain; C9 só wireia nos paths de apoiadores que ainda ignoram o guard. - Cortável se a base nominal permanecer pequena. Fases 1–2 só rendem com volume real ou coordenadores ativos; Fases 3–4 (DRY de forms) são baratas e reduzem drift — manter se houver folga.
apoiadores/[id]/formActions: migrar só comresolveBoundaryMessage/ branch pré-mapper que preservemessageonde a UI espera texto global (não field error).- i18n e naming (AGENTS.md): identificadores em inglês (
buildSupporterListFilterConditions,loadSupportersPageData), strings visíveis em pt-BR.
Questões em aberto
- Filtro unificado: AST neutro vs helper de termos de busca? Resolvido (2026-07-19): módulo
supporterListFilters.tscombuildSupporterSearchTerms+ adaptadorestoPayloadWhere/toAggregateSqlConditions— sem AST completo; access SQL permanece no aggregate. - Memo de núcleos:
React.cache(getCoordinatorNucleusIds)vsloadSupportersPageData? Resolvido (2026-07-19): loader únicoloadSupportersPageDataretorna{ result, state, redirectHref, nucleusOptions, overview, coordinatorNucleusIds };loadAccessibleNucleusOptionsaceitacoordinatorNucleusIdspré-resolvidos para coordenador. - Migrar componentes de form fora do diff do C8? Resolvido (2026-07-19): Fase 4 migrou os 8 componentes listados para
fieldErrordecampaignFormFields.ts.
Abordagem proposta
flowchart TD
C8["C8 apoiadores ✓"] --> F1
F1["Fase 1 — Filtro unificado<br/>supporterListFilters"]
F1 --> F2["Fase 2 — contactSearchQuery + loader página<br/>loadSupportersPageData"]
F2 --> F3["Fase 3 — formActions detalhe apoiador<br/>message-only mapper"]
F3 --> F4["Fase 4 — fieldError nos componentes restantes"]
C7["C7 contactSearchQuery ✓"] -.-> F2
Fase 1 — Núcleo de filtros de apoiadores ✓
src/utilities/supporterListFilters.ts:buildSupporterSearchTerms,toPayloadWhere,toAggregateSqlConditions.buildSupporterListWhereebuildAggregateSqldelegam ao módulo compartilhado.- Testes:
tests/int/supporterListFilters.int.spec.ts+ paridade lista/overview emcampaignSupporter.int.spec.ts.
Fase 2 — contactSearchQuery + loader da página ✓
- Filtros ignoram
qquando!isContactSearchQueryReady(q). loadSupportersPageDataemsupporterPageData.ts;apoiadores/page.tsxusa loader único.- Int: 1×
getCoordinatorNucleusIdspor render de coordenador.
Fase 3 — apoiadores/[id]/formActions.ts ✓
mapCampaignFormActionError+toMessageOnlyStatepreservando contratomessageda UI.
Fase 4 — Componentes de form restantes ✓
fieldErrorem:NucleusForm,ActionPlanForm,NucleusTerritoryFields,NucleusTerritoryAndZonesFields,VoteEstimateDialog,ActionPlanUpdateForm,LeadershipPrimaryContactAction,NucleusIntelligenceDialog.
Migration: nenhuma. Sem Consent novo.
Dependências
- Dura: C8 Escala e DRY pós-C6 (merge) — código em
supporterListOverviewAggregate.ts,supporterPageData.ts,supporterUi.ts,campaignFormActionError.ts. - Suave: C7
contactSearchQuery— já emmain; Fase 2 wireia. - Reusa:
src/lib/contactSearchQuery.ts,getCoordinatorNucleusIds,campaignFormFields.ts, padrãoloadSupporterListPageData, plano C8 escala-dry-pos-c6.md.
Não escopo
- Reabrir C6/C8 (import bulk, token HMAC,
pg_trgmmigration, locks batch). - Unificar lista+overview numa única query SQL (só se medição de latency justificar).
- Widen
PostgresTransactionDatabase— follow-up de plataforma (assertion runtime do C8 cobre drift de colunas no bulk). - Helpers FormData/URL de território do actionPlan — C7 Fase 3 / escala-dry-pos-c3.md.
- GOTV / dia D — C5.
Referências
docs/roadmap.md— Trilha C, item C9; sequência Janela 2- escala-dry-pos-c6.md — C8 entregue; precedente do formato
- escala-dry-pos-c3.md —
contactSearchQuery(C7) - escala-dry-pos-c2.md — cadastro nominal C2/C6
src/utilities/supporterListFilters.ts— núcleo de filtros unificadosrc/utilities/supporterUi.ts—buildSupporterListWheresrc/utilities/supporterListOverviewAggregate.ts— aggregate SQL +resolveAccessConstraintsrc/app/(campaign)/campanha/(app)/apoiadores/page.tsx—loadSupportersPageDatasrc/lib/contactSearchQuery.ts—isContactSearchQueryReadysrc/app/(campaign)/campanha/(app)/apoiadores/[id]/formActions.ts— mapper compartilhadosrc/utilities/campaignFormFields.ts—fieldError,errorProps- AGENTS.md —
Contact,overrideAccess: false, naming inglês
Revisão (2026-07-19)
Implementação das Fases 1–4 conforme plano de implementação C9: supporterListFilters.ts, loadSupportersPageData, contactSearchQuery nos filtros, formActions do detalhe no mapper, fieldError nos 8 componentes restantes. Testes unit + int adicionados. Roadmap atualizado (C8 mesclado, C9 entregue).
Simplify (2026-07-19)
Passagem /simplify sobre o diff do C9 aplicou limpezas pontuais: normalizeContactSearchQuery em buildSupporterSearchTerms, paralelismo no loader, spy de teste corrigido, KPI aggregate isolado por searchTag, aria-describedby com hasError no LeadershipPrimaryContactAction. Validado com testes int + scan Aikido.
Os débitos que os revisores marcaram como importantes e maiores que cleanup foram registrados como item C10 — escala-dry-pos-c9.md:
- Lista ainda re-dispara access —
payload.findcomoverrideAccess: falsechamagetAccessibleNucleusIds→ segunda ida agetCoordinatorNucleusIdsna mesma renderização. - Dois round-trips de
electoralNucleus—getCoordinatorNucleusIds(só IDs) +loadAccessibleNucleusOptions(name/slug) no mesmo escopo. errorPropsnão migrado — forms grandes ainda repetem wiring manual dearia-*apesar defieldError.- Prefetch não estende à página de criação —
loadSupporterCreatePageDatapode repetir o padrão.
Explicitamente fora (tradeoffs aceitos no C9/simplify): AST neutro único Payload↔SQL; waterfall lista→overview; unificar lista+aggregate; remover alias errorFor; hoist de toMessageOnlyState.