10 KiB
Escala e DRY pós-C9 (apoiadores / access / forms)
Status: registrado no roadmap (fases pendentes) Atualizado em: 2026-07-19 Item do roadmap: docs/roadmap.md (Trilha C, item C10) Responsável: —
Contexto
O C9 (escala-dry-pos-c8.md) entregou filtro unificado Payload↔SQL (supporterListFilters.ts), contactSearchQuery no aggregate/lista, loadSupportersPageData com prefetch de getCoordinatorNucleusIds, apoiadores/[id]/formActions no mapper compartilhado e fieldError nos componentes de form restantes. Uma passagem /simplify (2026-07-19) aplicou limpezas pontuais: normalizeContactSearchQuery nos termos de busca, paralelismo no loader, spy de teste corrigido, KPI aggregate isolado por searchTag, aria-describedby com hasError no LeadershipPrimaryContactAction.
Os revisores (quality / reuse / performance) marcaram como importantes e maiores que cleanup os follow-ups abaixo. Sem registro, o coordenador continua pagando lookups redundantes por navegação em /campanha/apoiadores e o DRY de acessibilidade nos forms grandes fica pela metade.
- Lista de apoiadores ainda re-dispara access do coordenador.
loadSupportersPageDataprefetchagetCoordinatorNucleusIdspara overview e options, masloadSupporterListPageDatachamapayload.findcomoverrideAccess: false→canReadSupporter→getAccessibleNucleusIds→ segunda chamada agetCoordinatorNucleusIdsna mesma renderização (semreqcompartilhado / contexto pré-populado). - Dois round-trips de
electoralNucleuspara o coordenador.getCoordinatorNucleusIdsseleciona só{ id: true }; em seguidaloadAccessibleNucleusOptionsfaz outrofindcomname/slugno mesmo conjunto de IDs — poderia ser uma query comselect: { id, name, slug }. errorPropsnão migrado nos forms grandes. C9 adotoufieldError+ alias localerrorFor, masNucleusForm,ActionPlanForm,NucleusTerritoryFields,NucleusTerritoryAndZonesFields,VoteEstimateDialog,ActionPlanUpdateForm,LeadershipPrimaryContactActioneNucleusIntelligenceDialogainda repetem manualmentearia-invalid/aria-describedbyem vez deerrorPropsdecampaignFormFields.ts(precedente:CampaignInviteForm/NucleusUpdateFormno C8).- Prefetch de escopo não estende à página de criação.
loadSupporterCreatePageDataainda pode repetir o mesmo padrão de access/options sem o escopo pré-resolvido do coordenador.
Explicitamente fora (revisores pediram skip no simplify / decisão C9): remover o alias errorFor (reduz repetição em forms grandes); AST neutro único Payload↔SQL (adaptadores paralelos são o desenho do C9); waterfall lista→overview (precisa de totalDocs da lista); unificar lista+aggregate numa query SQL (só se latency reclamar); retirar coordinatorNucleusIds do retorno da página (útil para testes int); hoist de toMessageOnlyState para campaignFormActionError.ts (só quando houver segundo consumidor).
Objetivos
- Navegação em
/campanha/apoiadorescomocoordenadorresolve escopo de núcleos uma vez por request — lista, overview e options compartilham o mesmo prefetch (viareq.contextou constraint injetado), sem segunda ida agetCoordinatorNucleusIds. - Opções de núcleo do coordenador saem de uma query
electoralNucleuscomid+name+slug. - Forms grandes da campanha usam
errorPropspara wiring dearia-*consistente comCampaignInviteForm. - Guardrails: sem novo
Consent; sem migration;overrideAccess: falsecomusernas leituras de apoiador; não alterar semântica de filtros (supporterListFilters).
Decisões travadas
- Um item C10, três fases ordenadas. Mesmo racional de A7/B5/C8/C9: um ID de roadmap, PRs por fase. Ordem: dedup de access na lista (ganho real por page view) → query única de núcleos (I/O) →
errorPropsnos forms (mecânico, UI). - Dependência dura de C9. Só faz sentido com filtro unificado e
loadSupportersPageDatajá no código; não reabre AST de filtros nem mapper do detalhe. - Fase 1 prefere
req.contextao bypass de access.getAccessibleNucleusIdsjá cacheia porreq(ACCESSIBLE_NUCLEUS_IDS_CONTEXT_KEY); pré-popular o contexto no loader Local API é menos invasivo queoverrideAccess: truena lista ou duplicar constraint decanReadSupporter. - Manter alias
errorFor. O/simplifyconcluiu que o wrapper local reduz ruído nos forms grandes; Fase 3 migra paraerrorProps(fieldErrors, name, idPrefix)dentro doerrorFor, não remove o alias. - Cortável se a base nominal permanecer pequena e poucos coordenadores usarem
/apoiadoresdiariamente. Fase 3 é a mais cortável (só a11y/DRY); Fase 1 evita drift de perf sob carga. - i18n e naming (AGENTS.md): identificadores em inglês (
loadCoordinatorNucleusScope,prefillAccessibleNucleusContext), strings visíveis em pt-BR inalteradas.
Questões em aberto
- Fase 1:
getPayloadcomreqsintético vspayload.find({ req })do loader? Recomendação: construirreqmínimo reutilizável noloadSupportersPageData(comuser,payload,contextpré-preenchido) e passá-lo às três leituras (list,overviewaggregate já bypassa access;optionspode continuaroverrideAccess: truecom IDs pré-resolvidos). Teste int asserta contagem degetCoordinatorNucleusIds= 1 e degetAccessibleNucleusIdssem cache miss na lista. - Fase 2: generalizar
getCoordinatorNucleusIdsou helper novo? Recomendação: novoloadCoordinatorNucleusScope(payload, coordinatorID, req?)retornandoSupporterNucleusOption[];getCoordinatorNucleusIdspermanece fino (idsderivados do scope) para não quebrar aggregate/access existentes. - Fase 3: prefixo de
idpor form? Recomendação: umidPrefixestável por componente (nucleus-form,action-plan-form, …) espelhando o padrão doCampaignInviteForm; não unificar prefixos entre forms distintos na mesma página.
Abordagem proposta
flowchart TD
C9["C9 Filtros + loader ✓"] --> F1
F1["Fase 1 — req.context / access dedup na lista"]
F1 --> F2["Fase 2 — loadCoordinatorNucleusScope<br/>(1× electoralNucleus)"]
F1 --> F3["Fase 3 — errorProps nos 8 forms"]
F2 -.options + ids.-> F4["Fase 4 opcional — create page prefetch"]
Fase 1 — Dedup de access na lista de apoiadores
- Em
src/utilities/supporterPageData.ts: ao montarloadSupportersPageData, após resolvercoordinatorNucleusIds, pré-popularreq.contextcom a chave usada porgetAccessibleNucleusIdsou extrair helperprefillAccessibleNucleusContext(req, user, ids). - Passar esse
reqaloadSupporterListPageData→payload.find({ ..., req, overrideAccess: false }). - Garantir que
canReadSupporter/getAccessibleNucleusIdsleem o cache e não chamamgetCoordinatorNucleusIdsde novo. - Teste int: expandir o spy existente em
campaignSupporter.int.spec.tspara cobrir também a lista (payload.findpath), não só o loader agregado.
Fase 2 — Query única de núcleos do coordenador
- Novo helper em
src/utilities/supporterPageData.tsoucampaignAccess.ts:loadCoordinatorNucleusScopecomwhere: { coordinators: { contains } },status: ativo,select: { id, name, slug }. loadSupportersPageDatausa o scope paranucleusOptionse derivacoordinatorNucleusIdsvia.map(n => n.id).getCoordinatorNucleusIdspode delegar ao scope (só IDs) para manter uma fonte de query.- Remover o segundo
finddeloadAccessibleNucleusOptionsquandocoordinatorNucleusIdsvier do scope completo.
Fase 3 — errorProps nos forms grandes
- Migrar os 8 componentes listados no Contexto: substituir pares manuais
errorFor+aria-*por desestruturação deerrorProps(fieldErrors, field, idPrefix). - Manter
const errorFor = (name) => fieldError(...)onde simplificarerrorno JSX; ouconst field = (name) => errorProps(fieldErrors, name, idPrefix)se reduzir linhas. - Sem mudança de copy nem de contrato
formActions.
Fase 4 (opcional) — Prefetch na criação de apoiador
loadSupporterCreatePageDatareutilizaloadCoordinatorNucleusScope/ context prefill do coordenador.- Só entra se a Fase 1–2 já estiverem estáveis; pode ser cortada.
Migration: nenhuma. Sem collection, sem Consent, sem server action nova.
Dependências
- Dura: C9 Escala e DRY pós-C8 — implementado (
supporterListFilters,loadSupportersPageData, forms C9). - Suave: C8
getCoordinatorNucleusIds+ context cache emgetAccessibleNucleusIds— já no código. - Reuso:
src/utilities/campaignAccess.ts,src/utilities/supporterPageData.ts,src/utilities/campaignFormFields.ts(errorProps),src/app/(campaign)/campanha/(app)/apoiadores/page.tsx.
Não escopo
- AST neutro único para filtros Payload↔SQL — decisão fechada no C9; mudanças futuras continuam em
supporterListFilters.ts. - Unificar lista + overview KPI numa query — escala-dry-pos-c8.md / C8 explicitamente fora; reavaliar só com evidência de latency.
- Remover waterfall lista→overview — tradeoff intencional (
totalDocsantes do aggregate). - Migrar
formActionsrestantes fora da vertical de apoiadores — coberto por C7/C8; não repetir aqui. - Hoist de
toMessageOnlyState— fill-in quando surgir segundo consumidor message-only.
Referências
docs/roadmap.md— Trilha C, item C10; sequência Janela 2- escala-dry-pos-c8.md — C9 entregue; simplify 2026-07-19
- escala-dry-pos-c6.md — precedente C8→C9
src/utilities/supporterPageData.ts—loadSupportersPageData,loadAccessibleNucleusOptionssrc/utilities/campaignAccess.ts—getCoordinatorNucleusIds,getAccessibleNucleusIds, context cachesrc/collections/Supporter.ts—read: canReadSupportersrc/utilities/campaignFormFields.ts—fieldError,errorPropstests/int/campaignSupporter.int.spec.ts— spy de lookup único- AGENTS.md —
overrideAccess: false, naming inglês, campaign auth
Simplify (2026-07-19)
Passagem /simplify sobre o diff do C9 aplicou limpezas pontuais sem mudar comportamento (ver Contexto). Débitos maiores registrados neste plano como item C10 do roadmap.