C13 pos-simplify: fecha os residuos da varredura e estreita os guards

O rename mecanico deixou um defeito silencioso e alguns fosseis: o fixture de
recentVisits perdeu o ${index} (todas as entradas com o mesmo label, invisivel
porque o label nao era asserido) e access/shared.ts guardava "action plan" em
prosa, forma que o guard de vocabulario nao cobria. O padrao agora tolera espaco
e a allowlist encolheu: o comentario de campaignMigrationReconciliation cita
activity e o arquivo saiu da excecao.

Na migration, a entrada action_plan_municipality_idx era inalcancavel (nenhuma
migration jamais criou esse nome) e saiu, deixando up() e down() com os mesmos
48 pares; as duas buscas de FK passam a usar to_regclass, que retorna NULL em vez
de lancar -- a unica consulta que contradizia a invariante do proprio cabecalho.

O detalhe de organizacao para de derivar o rotulo de status a mao e reusa
ActivityStatusBadge, com status tipado como ActivityStatus no loader.

Debitos do /simplify registrados: C8 F4b (o guard de formActions so inspeciona
arquivos chamados exatamente formActions.ts) no plano do C8; defers e descartes
com gatilho no plano do C13.
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-25 21:29:32 -03:00
parent 7b93c232ff
commit 619666dd51
21 changed files with 96 additions and 73 deletions

View file

@ -7,7 +7,7 @@ alwaysApply: false
## Status
- **C13 entregue em código (2026-07-25):** "Plano de Ação" → **"Atividade"** em toda a vertical. Collection `actionPlan` → `activity` (`src/collections/Activity.ts`, labels "Atividade"/"Atividades", hooks internos renomeados), campo `campaignDemand.actionPlan` → `activity`, 7 utilities `actionPlan*` → `activity*`, `access/actionPlans.ts` → `access/activities.ts`, `lib/schemas/actionPlan.ts` → `activity.ts`, `actions/actionPlan.ts` → `activity.ts`, `components/campaign/actionPlan/` → `activity/` (13 componentes), rota `(app)/planos/` → `(app)/atividades/` (criação em `atividades/nova`, feminino como `demandas/nova`) e sidebar "Atividades". **Migration data-preserving escrita à mão** `20260725_213000_rename_action_plan_to_activity`: 4 tabelas, 3 enums, 2 colunas, 39 índices (4 pkeys inclusos), 14 FKs, 2 sequences — lista canônica tirada de `pg_indexes`/`pg_constraint`/`pg_type`/`pg_sequences` porque o snapshot do drizzle não conhece o índice parcial `action_plan_upcoming_start_at_idx` (escrito à mão no C8) e porque o gerador do Payload emitiria DROP+CREATE. O `.json` foi derivado do snapshot anterior por transformação textual (o `migrate:create` é interativo para renames) e validado pelo teste que importa: `pnpm migrate:create __probe` responde "No schema changes detected". **Bônus aprovado pelo usuário nesta sessão:** os **8 índices fósseis `%plaza%`** da remodelagem de 23/07 (que renomeou as colunas `plaza_id` mas não os índices, deixando banco e snapshot em desacordo) foram alinhados na mesma migration; `down()` os restaura, para não inventar um estado que nunca existiu. Decisões de vocabulário: sidebar **"Atividades"** (uma palavra só, a da entidade) e rótulos de campo alinhados — **"Tipo de atividade"**, **"Origem da atividade"**, **"Resultado da atividade"** — porque "ação" ao lado de "atividade" reintroduzia a tradução mental que o item existe para matar; valores de enum `kind`/`status`/`origin` **inalterados**. **Guard:** o describe "banned campaign terminology" de `codebaseConventions.unit.spec.ts` virou tabela de termos varrendo `src`+`tests`+`scripts` (migrations fora — história congelada), banindo os compostos `actionPlan`, a copy "plano de ação" e a rota velha, mas **não** `plan`/`plano` isolado (colidiria com os planos de assinatura da Fase 2 white-label); allowlist: o próprio spec, `scripts/generate-remodel-municipalities-migration.mjs` e o comentário histórico em `campaignMigrationReconciliation.int.spec.ts`. Estendê-lo a `tests/` derrubou 8 fósseis "Praça" em fixtures, limpos no mesmo PR. Gate verde (tsc, lint, format, knip, check:cycles, 46 unit / 48 int / 7 e2e, build); Aikido sem achado. **Deploy:** o `pnpm build` da Vercel aplica a migration enquanto o deployment antigo ainda serve — deployar fora do horário de mesa (mesmo perfil da remodelagem de 23/07). Plano: [`renomear-plano-acao-para-atividade.md`](../../../docs/plans/renomear-plano-acao-para-atividade.md).
- **C13 entregue em código (2026-07-25):** "Plano de Ação" → **"Atividade"** em toda a vertical. Collection `actionPlan` → `activity` (`src/collections/Activity.ts`, labels "Atividade"/"Atividades", hooks internos renomeados), campo `campaignDemand.actionPlan` → `activity`, 7 utilities `actionPlan*` → `activity*`, `access/actionPlans.ts` → `access/activities.ts`, `lib/schemas/actionPlan.ts` → `activity.ts`, `actions/actionPlan.ts` → `activity.ts`, `components/campaign/actionPlan/` → `activity/` (13 componentes), rota `(app)/planos/` → `(app)/atividades/` (criação em `atividades/nova`, feminino como `demandas/nova`) e sidebar "Atividades". **Migration data-preserving escrita à mão** `20260725_213000_rename_action_plan_to_activity`: 4 tabelas, 3 enums, 2 colunas, 39 índices (4 pkeys inclusos), 14 FKs, 2 sequences — lista canônica tirada de `pg_indexes`/`pg_constraint`/`pg_type`/`pg_sequences` porque o snapshot do drizzle não conhece o índice parcial `action_plan_upcoming_start_at_idx` (escrito à mão no C8) e porque o gerador do Payload emitiria DROP+CREATE. O `.json` foi derivado do snapshot anterior por transformação textual (o `migrate:create` é interativo para renames) e validado pelo teste que importa: `pnpm migrate:create __probe` responde "No schema changes detected". **Bônus aprovado pelo usuário nesta sessão:** os **8 índices fósseis `%plaza%`** da remodelagem de 23/07 (que renomeou as colunas `plaza_id` mas não os índices, deixando banco e snapshot em desacordo) foram alinhados na mesma migration; `down()` os restaura, para não inventar um estado que nunca existiu. Decisões de vocabulário: sidebar **"Atividades"** (uma palavra só, a da entidade) e rótulos de campo alinhados — **"Tipo de atividade"**, **"Origem da atividade"**, **"Resultado da atividade"** — porque "ação" ao lado de "atividade" reintroduzia a tradução mental que o item existe para matar; valores de enum `kind`/`status`/`origin` **inalterados**. **Guard:** o describe "banned campaign terminology" de `codebaseConventions.unit.spec.ts` virou tabela de termos varrendo `src`+`tests`+`scripts` (migrations fora — história congelada), banindo os compostos `actionPlan`, a copy "plano de ação" e a rota velha, mas **não** `plan`/`plano` isolado (colidiria com os planos de assinatura da Fase 2 white-label); allowlist: o próprio spec, `scripts/generate-remodel-municipalities-migration.mjs` e o comentário histórico em `campaignMigrationReconciliation.int.spec.ts`. Estendê-lo a `tests/` derrubou 8 fósseis "Praça" em fixtures, limpos no mesmo PR. Gate verde (tsc, lint, format, knip, check:cycles, 46 unit / 48 int / 7 e2e, build); Aikido sem achado. **Deploy:** o `pnpm build` da Vercel aplica a migration enquanto o deployment antigo ainda serve — deployar fora do horário de mesa (mesmo perfil da remodelagem de 23/07). **Pós-`/simplify`:** a varredura mecânica comeu uma interpolação — `tests/unit/recentVisits.unit.spec.ts` ficou com `label: \`Município \`` para todas as entradas, e passava porque o label não era asserido (restaurado, com assertion nova para fechar a brecha, e os slugs `praca-` do fixture alinhados). O guard ganhou tolerância a espaço (`action[\s_-]?plan`), que era o buraco por onde `access/shared.ts` mantinha "action plan" em prosa; com isso a allowlist encolheu — o comentário de `campaignMigrationReconciliation.int.spec.ts` passou a citar `activity` e o arquivo saiu da exceção (uma spec que cresce não fica fora do guard por causa de um comentário). Deliberadamente **não** ampliei o padrão de "Praça" para a forma sem cedilha: cobraria três entradas novas de allowlist (nomes reais de organização na planilha, o teste de entradas legadas `kind: 'nucleus'`) por um fóssil já corrigido. Também: três resíduos do find-and-replace (banner `// Action activities`, `DOSSIER_*_PLAN_LIMIT`, "a activity" em dois comentários), concordância em pt-BR ("Criada por", "atividades apoiadas" minúsculo), o `??` impossível em `ActivityForm`, e as duas buscas de FK da migration agora usam `to_regclass` em vez de `::regclass` (que lança em vez de retornar NULL, contradizendo a invariante do próprio cabeçalho). A entrada morta `action_plan_municipality_idx` saiu da lista de índices da migration (nenhuma migration jamais criou esse nome — só os snapshots o afirmam, e com `push: false` nenhum banco pode carregá-lo), deixando `up()` e `down()` com os mesmos 48 pares; e o detalhe de organização parou de derivar o rótulo de status à mão e passou a reusar `ActivityStatusBadge`, com `OrganizationDetailViewModel.activities[].status` tipado como `ActivityStatus` em vez de `string` (a origem que forçava o re-narrowing no consumidor). Plano: [`renomear-plano-acao-para-atividade.md`](../../../docs/plans/renomear-plano-acao-para-atividade.md).
- **B26 entregue em código (2026-07-25):** registrar sinal a partir da célula "Último sinal" de `/campanha/municipios` — Popover (desktop) / Drawer bottom sheet (mobile) cria `municipalityUpdate` `kind: 'sinal'` (C12) sem sair da fila; submit explícito "Registrar sinal" (exceção do princípio 4); `revalidateMunicipalityListPaths`; sem migration. Plano: [`registrar-sinal-lista-municipios.md`](../../../docs/plans/registrar-sinal-lista-municipios.md).
- **B20 entregue (2026-07-25):** Municípios prioritários no Início — `pickDashboardPriorityMunicipalities` (`src/utilities/dashboardPriorityMunicipalities.ts`) ordena `priority=alta` por déficit E9/`central`, amostra 8; `CampaignDashboard` exibe Badge com `highPriorityCount`, CTA “Ver todas” → `?priority=alta`, chips com `list-none`. Unit `dashboardPriorityMunicipalities.unit.spec.ts`. Plano: [`municipios-prioritarios-inicio.md`](../../../docs/plans/municipios-prioritarios-inicio.md).
- **B28 registrado (2026-07-25):** e-mail + celular na lista `/campanha/liderancas` — colunas com copy-on-click + WhatsApp (`wa.me`) no padrão de `/campanha/assessores` (B19 ✓); `email` no view model da lista; extrair primitivo compartilhado (2º call site); **sem** toggle Editar in-list (Contact update adiado). Appetite ~0,5–0,75d; Janela 1; cortável; Impeccable B. Plano: [`email-celular-lista-liderancas.md`](../../../docs/plans/email-celular-lista-liderancas.md).

View file

@ -1,7 +1,7 @@
# Escala e DRY pós-C6 (apoiadores / import / listas)
Status: F4 (DRY de forms) **entregue** — twin praça 2026-07-21 + hardening 2026-07-23; **o resto do F4 (10 escadas hand-rolled escritas depois) fechou em 2026-07-25 no Pass 2 W4d** (`runCampaignFormAction`, exceções documentadas em comentário); **F3 (parsers das listas de entidade) fechou no Pass 2 W1-D2**; F1–F2 abertos com gatilho (volume real de import/base nominal pós-Onda 0)
Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios + hardening)
Status: F4 (DRY de forms) **entregue** — twin praça 2026-07-21 + hardening 2026-07-23; **o resto do F4 (10 escadas hand-rolled escritas depois) fechou em 2026-07-25 no Pass 2 W4d** (`runCampaignFormAction`, exceções documentadas em comentário); **F3 (parsers das listas de entidade) fechou no Pass 2 W1-D2**; **F4b aberta** (o guard do F4 só inspeciona `formActions.ts`, deixando os arquivos de action irmãos sem verificação — achado do `/simplify` do C13); F1–F2 abertos com gatilho (volume real de import/base nominal pós-Onda 0)
Atualizado em: 2026-07-25 (F4b registrada pelo `capture-review-debts` pós-C13)
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins abertos, item C8)
Responsável: —
@ -88,7 +88,15 @@ flowchart TD
- ~~Migrar `fieldError`/`firstError` local em forms legados~~ — `CampaignInviteForm.tsx` já importa de `campaignFormFields.ts`.
- **Nota pós-remodel:** paths `nucleos/*`/`pracas/*` foram superseded; não reabrir.
**Migration:** Fases 1, 3, 4 sem schema. Fase 2: `pnpm migrate:create add_contact_trgm_index` (extensão + 2 GIN indexes). Sem Consent novo.
### Fase 4b — o guard do F4 só vê `formActions.ts` — **ABERTA** (registrada 2026-07-25 pós-C13)
Appetite: **~2h eng, fill-in.** O `/simplify` do C13 mostrou que a invariante do F4 é menor do que o texto acima sugere: o guard `campaign formActions convention` (`tests/unit/codebaseConventions.unit.spec.ts`) filtra `file.endsWith('/formActions.ts')`, então os arquivos de action irmãos que cada rota escreveu depois — `lifecycleFormActions.ts`, `resultFormActions.ts`, `updateFormActions.ts`, `taskActions.ts` — **não são verificados em nenhuma rota**. A exceção documentada na Revisão 2026-07-24 ("`lifecycleFormActions.ts` só retorna safe-messages fixas — sem ladder para migrar") foi escrita quando o alvo era `mapCampaignFormActionError`; com `runCampaignRedirectFormAction` (Pass 2 W4d) a conta mudou, porque esse arquivo faz exatamente a forma que o wrapper padroniza: dois `catch` nus que descartam o erro + `redirect` no sucesso.
- Ampliar o filtro do guard para os irmãos (`/^\w*[Ff]ormActions\.ts$/` + `taskActions.ts`, ou um sufixo declarado) e allowlistar com racional escrito o que realmente não couber.
- Migrar o que o wrapper cobre; onde o `catch` nu ficar, decidir se o erro deve ser observável (hoje é descartado sem log em ambos os caminhos, então não é um débito diferencial — verificar no PR antes de inflar o escopo).
- Não escopo: mudar a semântica das safe-messages (copy revisada) e tocar `actions/*.ts` (as server actions em si já usam transação/lock).
**Migration:** Fases 1, 3, 4, 4b sem schema. Fase 2: `pnpm migrate:create add_contact_trgm_index` (extensão + 2 GIN indexes). Sem Consent novo.
## Dependências

View file

@ -128,6 +128,25 @@ Componentes:
- **Rótulo "Agenda" no sidebar.** Revisitar se, no onboarding (ou no R6), ≥2 pessoas do time chamarem o destino de "agenda" ao navegar — aí o rail muda de rótulo mantendo a entidade "Atividade".
- **Terceira entrada no guard de vocabulário (extração para módulo próprio).** Revisitar quando existir um 3º rename de termo — antes disso, a tabela no spec basta.
## Simplify (2026-07-25 pós-C13)
Passagem `/simplify` (qualidade + perf + reuso) e `capture-review-debts`. O diff é rename mecânico: nenhuma forma de query mudou, o índice parcial `activity_upcoming_start_at_idx` sobreviveu com o predicado intacto (`EXPLAIN` confirma Index Scan sem Filter residual) e o app segue usando os shells compartilhados.
**Já resolvido (não reabrir):** interpolação `${index}` comida em `tests/unit/recentVisits.unit.spec.ts` (fixture com label único, agora com assertion que fecha a brecha, e slugs `praca-` alinhados); guard tolerante a espaço (`action[\s_-]?plan`), que era o buraco por onde `access/shared.ts` mantinha "action plan" em prosa; allowlist encolhida (o comentário de `campaignMigrationReconciliation.int.spec.ts` cita `activity` e o arquivo saiu da exceção); entrada morta `action_plan_municipality_idx` fora da migration (nenhuma migration jamais criou esse nome — só os snapshots o afirmam), deixando `up()`/`down()` com os mesmos 48 pares; buscas de FK com `to_regclass` em vez de `::regclass`; detalhe de organização reusando `ActivityStatusBadge` com `status` tipado como `ActivityStatus` no loader; resíduos de find-and-replace (banner `// Action activities`, `DOSSIER_*_PLAN_LIMIT`, "a activity", nome de teste "zona Município"); concordância pt-BR ("Criada por", "atividades apoiadas"); `??` impossível em `ActivityForm`.
**Absorvido em outro plano:** o guard de `formActions` só inspeciona arquivos chamados exatamente `formActions.ts`, deixando `lifecycleFormActions.ts`/`resultFormActions.ts`/`updateFormActions.ts`/`taskActions.ts` sem verificação → [escala-dry-pos-c6.md](escala-dry-pos-c6.md) **Fase 4b** (dono da invariante do C8 F4).
**Explicitamente fora (triage `capture-review-debts` 2026-07-25 pós-C13):**
- **Redirect de `/campanha/planos*` → `/campanha/atividades*`** — gatilho: primeiro relato de 404 por bookmark. A rota viveu 7 dias, a navegação é pelo rail, e nada persistido aponta para ela (`recentVisits` só grava páginas de município); um redirect reintroduziria a string que o guard bane.
- **Projeção de activity duplicada em `organizationData.ts`** (em vez de `activityListSelect` + `toActivityListViewModel`) — gatilho: a seção passar a mostrar localidade ou progresso de tarefas. Hoje a forma é subconjunto estrito e o view model completo faria over-fetch numa lista de resumo.
- **Painel de filtros mobile compartilhado entre `ActivityFilters` e `SupporterFilters`** — gatilho: 3º painel com a mesma mecânica de colapso (municípios usa header rico, não este shape).
- **Hoist das listas de rename da migration para constantes + helper local** — recusado: migration é história congelada, o consumidor é único, e a divergência que o helper preveniria está verificada como inexistente (conjuntos idênticos nos dois sentidos).
- **Contadores `renamed` + `RAISE NOTICE` do `up()`** — mantidos: são a única evidência, no log do build da Vercel, de que o rename tocou o número esperado de objetos; a assimetria com `down()` é aceitável porque `down()` só roda local.
- **`payload.create` sequencial dos rascunhos de demanda em `actions/activity.ts`** — pré-existente, roda sobre os rascunhos de um único submit dentro de uma transação; batchear perderia as semânticas de hook por linha.
- **`it.each` do guard re-varrer `src`+`tests`+`scripts` por termo** — 150 ms na suíte unitária; hoisting da leitura é throughput, não duplicação.
- **Padrão de "Praça" tolerante à falta de cedilha** — cobraria três allowlists novas (nomes reais de organização vindos da planilha, teste de entradas legadas `kind: 'nucleus'`) para pegar um fóssil já corrigido à mão.
## Referências
- `docs/roadmap.md` (Trilha C / "Demais itens abertos"; Janela 1; grafo)

File diff suppressed because one or more lines are too long

View file

@ -4,11 +4,11 @@ import Link from 'next/link'
import { notFound, redirect } from 'next/navigation'
import { getPayload } from 'payload'
import { ActivityStatusBadge } from '@/components/campaign/activity/ActivityStatusBadge'
import { OrganizationForm } from '@/components/campaign/organization/OrganizationForm'
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
import { Badge } from '@/components/ui/Badge'
import { Button } from '@/components/ui/button'
import { activityStatusLabels, activityStatuses } from '@/lib/schemas/activity'
import { organizationKindLabels } from '@/lib/schemas/organization'
import { isCampaignStaff } from '@/utilities/campaignAccess'
import { getCampaignUser } from '@/utilities/campaignAuth'
@ -22,11 +22,6 @@ type OrganizationDetailPageProps = {
const dateFormatter = new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short' })
const statusLabel = (status: string): string =>
activityStatuses.includes(status as (typeof activityStatuses)[number])
? activityStatusLabels[status as (typeof activityStatuses)[number]]
: status
export default async function OrganizationDetailPage({ params }: OrganizationDetailPageProps) {
const { slug } = await params
const [user, payload] = await Promise.all([getCampaignUser(), getPayload({ config })])
@ -108,18 +103,20 @@ export default async function OrganizationDetailPage({ params }: OrganizationDet
<ul className="flex flex-col gap-2">
{organization.activities.map((activity) => (
<li key={activity.id} className="flex items-center justify-between gap-2">
<div className="flex min-w-0 flex-col">
<Link
href={`/campanha/atividades/${activity.slug}`}
className="truncate font-medium text-primary underline-offset-4 hover:underline"
>
{activity.title}
</Link>
<div className="flex min-w-0 flex-col gap-1">
<div className="flex flex-wrap items-center gap-2">
<Link
href={`/campanha/atividades/${activity.slug}`}
className="truncate font-medium text-primary underline-offset-4 hover:underline"
>
{activity.title}
</Link>
<ActivityStatusBadge status={activity.status} />
</div>
<span className="text-sm text-muted-foreground">
{statusLabel(activity.status)}
{activity.startAt
? ` · ${dateFormatter.format(new Date(activity.startAt))}`
: ''}
? dateFormatter.format(new Date(activity.startAt))
: 'Sem data definida'}
{activity.deputyPresent ? ' · Deputado presente' : ''}
</span>
</div>

View file

@ -616,7 +616,7 @@ export const Activity: CollectionConfig = {
name: 'createdBy',
type: 'relationship',
relationTo: 'campaignUser',
label: 'Criado por',
label: 'Criada por',
index: true,
admin: {
readOnly: true,

View file

@ -42,7 +42,7 @@ export const Organization: CollectionConfig = {
useAsTitle: 'name',
defaultColumns: ['name', 'kind', 'updatedAt'],
description:
'Sindicatos, associações, movimentos e afins. Concentra lideranças associadas e Atividades apoiadas.',
'Sindicatos, associações, movimentos e afins. Concentra lideranças associadas e atividades apoiadas.',
},
access: {
create: canCreateOrganization,

View file

@ -179,8 +179,8 @@ const ActivityFormFields = ({
</>
) : (
<FieldDescription>
Status atual: {activityStatusLabels[activity?.status ?? ''] ?? activity?.status}
. Use as ações do detalhe para marcar como realizado ou cancelado.
Status atual: {activity ? activityStatusLabels[activity.status] : ''}. Use as
ações do detalhe para marcar como realizado ou cancelado.
</FieldDescription>
)}
</Field>

View file

@ -14,10 +14,11 @@ import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
* Two index names in the lists are fossils from the Município remodel, which
* renamed the `plaza_id` columns but not their indexes: the database holds
* `action_plan_plaza_idx` and `campaign_demand_plaza_idx` while the drizzle
* snapshot expects `*_municipality_idx`. The six remaining `%plaza%` fossils on
* other tables are aligned here too, so the database matches the snapshot in one
* pass. Every rename is guarded on the source object existing, so environments
* that already carry either name converge.
* snapshot expects `*_municipality_idx` — no migration ever created the aligned
* name, so the fossil is the only name any database can hold. The six remaining
* `%plaza%` fossils on other tables are aligned here too, so the database matches
* the snapshot in one pass. Every rename is guarded on the source object existing,
* so a partially converged environment still lands on the same state.
*/
const renameTables = sql`
@ -117,7 +118,6 @@ const renameIndexes = sql`
('action_plan_start_at_idx', 'activity_start_at_idx'),
('action_plan_upcoming_start_at_idx', 'activity_upcoming_start_at_idx'),
('action_plan_plaza_idx', 'activity_municipality_idx'),
('action_plan_municipality_idx', 'activity_municipality_idx'),
('action_plan_responsible_idx', 'activity_responsible_idx'),
('action_plan_leadership_idx', 'activity_leadership_idx'),
('action_plan_result_recorded_by_idx', 'activity_result_recorded_by_idx'),
@ -188,7 +188,7 @@ const renameForeignKeys = sql`
LOOP
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = r.old_name AND conrelid = format('public.%I', r.table_name)::regclass
WHERE conname = r.old_name AND conrelid = to_regclass('public.' || quote_ident(r.table_name))
) THEN
EXECUTE format(
'ALTER TABLE public.%I RENAME CONSTRAINT %I TO %I',
@ -281,7 +281,7 @@ export async function down({ db }: MigrateDownArgs): Promise<void> {
LOOP
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = r.new_name AND conrelid = format('public.%I', r.table_name)::regclass
WHERE conname = r.new_name AND conrelid = to_regclass('public.' || quote_ident(r.table_name))
) THEN
EXECUTE format(
'ALTER TABLE public.%I RENAME CONSTRAINT %I TO %I',

View file

@ -479,7 +479,7 @@ export interface StateDeputy {
createdAt: string;
}
/**
* Sindicatos, associações, movimentos e afins. Concentra lideranças associadas e Atividades apoiadas.
* Sindicatos, associações, movimentos e afins. Concentra lideranças associadas e atividades apoiadas.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "organization".

View file

@ -1,5 +1,5 @@
// ---------------------------------------------------------------------------
// Action activities
// Activities
// ---------------------------------------------------------------------------
import type { Access, FieldAccess, Where } from 'payload'

View file

@ -61,7 +61,7 @@ export const isCampaignStaff = (user: CampaignActor): boolean =>
isCampaignUser(user) && isStaffCampaignRole(user.role)
/**
* Eligible relationship targets for advisor assignments (municipality / action plan).
* Eligible relationship targets for advisor assignments (municipality / activity).
* The candidate is included: the projection sheet lists him as the responsible contact
* for some municipalities (decision 2026-07-24).
*/

View file

@ -51,7 +51,7 @@ const loadActivityUpdateAuthorNames = async (
}
/**
* Display-name lookup for a activity's municipality when the document was loaded at
* Display-name lookup for an activity's municipality when the document was loaded at
* depth 0 (updates tab). The actor already passed row access on the activity
* itself, so reading the municipality's name/slug privileged avoids a second
* per-role access round-trip (established display-name pattern).

View file

@ -21,7 +21,7 @@ const leadershipLabel = (leadership: Leadership): string | null => {
}
/**
* Engaged leaderships visible to the actor (for linking a activity to a leadership).
* Engaged leaderships visible to the actor (for linking an activity to a leadership).
* Uses overrideAccess: false so Payload row-level access applies.
*/
export const searchActivityLeadershipOptions = async (

View file

@ -43,8 +43,8 @@ import {
*/
export const DOSSIER_LEADERSHIP_LIMIT = 8
export const DOSSIER_SIGNAL_LIMIT = 5
const DOSSIER_UPCOMING_PLAN_LIMIT = 3
const DOSSIER_RECENT_PLAN_LIMIT = 2
const DOSSIER_UPCOMING_ACTIVITY_LIMIT = 3
const DOSSIER_RECENT_ACTIVITY_LIMIT = 2
export type MunicipalityDossierData = {
baseline: MunicipalityElectoralBaseline | null
@ -77,8 +77,8 @@ const loadDossierActivities = async (
})
const [upcoming, recent] = await Promise.all([
findActivities('proximos', DOSSIER_UPCOMING_PLAN_LIMIT, 'startAt'),
findActivities('realizados', DOSSIER_RECENT_PLAN_LIMIT, '-startAt'),
findActivities('proximos', DOSSIER_UPCOMING_ACTIVITY_LIMIT, 'startAt'),
findActivities('realizados', DOSSIER_RECENT_ACTIVITY_LIMIT, '-startAt'),
])
return {

View file

@ -5,6 +5,7 @@ import type { Payload } from 'payload'
import type { OrganizationKind } from '@/lib/schemas/organization'
import { organizationKinds } from '@/lib/schemas/organization'
import type { CampaignUser, Organization } from '@/payload-types'
import type { ActivityStatus } from '@/utilities/activityUi'
import {
buildListHref,
firstValue,
@ -160,7 +161,7 @@ export type OrganizationDetailViewModel = {
id: number
title: string
slug: string
status: string
status: ActivityStatus
startAt: string | null
deputyPresent: boolean
}>
@ -239,7 +240,7 @@ export const loadOrganizationDetail = async (
id: activity.id,
title: activity.title,
slug: activity.slug,
status: String(activity.status),
status: activity.status,
startAt: activity.startAt ?? null,
deputyPresent: Boolean(activity.deputyPresent),
})),

View file

@ -160,7 +160,7 @@ describe('campaign migration existing-schema reconciliation', () => {
// NOTE: the pre-remodel "empty path" rollback test (municipalityRemodel.down →
// consolidated.down → original.down) is intentionally absent: the municipality
// remodel down is currently broken — `DROP TABLE "municipality" CASCADE` already
// cascades the supporter/action_plan/locked-documents FKs that the script
// cascades the supporter/activity/locked-documents FKs that the script
// then tries to DROP CONSTRAINT explicitly, so the rollback always aborts.
// Restore that coverage once the migration down uses IF EXISTS (or drops
// constraints before the cascaded tables).

View file

@ -192,7 +192,7 @@ describe('ESLint conventions', () => {
`'use server'\n\nexport type Result = { ok: true }\nexport const save = async () => ({ ok: true as const })\nexport async function load() { return 1 }\n`,
)
await expectValid(
'src/app/(campaign)/campanha/(app)/pracas/municipalityStaffEditMessages.ts',
'src/app/(campaign)/campanha/(app)/municipios/municipalityStaffEditMessages.ts',
`export const municipalityStaffEditSafeMessages = ['x'] as const\n`,
)
})

View file

@ -45,7 +45,7 @@ const canonicalEvidenceRows = (assignments: OfficialEvidence['assignments']): st
describe('Municipality zone neighborhood catalog (Salvador only)', () => {
const zoneMunicipalities = municipalityCatalog.filter((entry) => entry.kind === 'zona')
it('covers every zona Município with a non-empty neighborhood list', () => {
it('covers every zone municipality with a non-empty neighborhood list', () => {
expect(municipalityZoneNeighborhoods).toHaveLength(zoneMunicipalities.length)
expect(new Set(municipalityZoneNeighborhoods.map((entry) => entry.municipalitySlug)).size).toBe(
zoneMunicipalities.length,

View file

@ -80,43 +80,39 @@ describe('banned campaign terminology', () => {
},
{
id: 'Plano de Ação',
pattern:
/actionPlan|ActionPlan|ACTION_PLAN|action_plan|action-plan|planos? de aç[aã]o|\/campanha\/planos/i,
// Accent-tolerant (agents routinely type "plano de acao") and space-tolerant,
// so English prose ("action plan") cannot slip past the identifier forms.
pattern: /action[\s_-]?plan|planos? de a[çc][aã]o|\/campanha\/planos/i,
remedy: 'use "Atividade" / activity (or allowlist frozen history)',
allowlist: [
// Emits the frozen remodel migration, whose SQL predates the rename.
'scripts/generate-remodel-municipalities-migration.mjs',
// Asserts against the frozen migration chain by its historical names.
'tests/int/campaignMigrationReconciliation.int.spec.ts',
],
},
] as const
const searchRoots = ['src', 'tests', 'scripts'] as const
it.each(bannedTerms.map((term) => [term.id, term] as const))(
'keeps %s out of src, tests and scripts',
(_id, term) => {
// This spec quotes the banned literals itself.
const allowlist = new Set<string>([...term.allowlist, repoPath(import.meta.filename)])
const offenders: string[] = []
it.each(bannedTerms)('keeps $id out of src, tests and scripts', (term) => {
// This spec quotes the banned literals itself.
const allowlist = new Set<string>([...term.allowlist, repoPath(import.meta.filename)])
const offenders: string[] = []
for (const root of searchRoots) {
for (const file of walkSourceFiles(resolve(repoRoot, root), ['.ts', '.tsx', '.mjs'])) {
const path = repoPath(file)
if (path.startsWith('src/migrations/')) continue
if (allowlist.has(path)) continue
for (const root of searchRoots) {
for (const file of walkSourceFiles(resolve(repoRoot, root), ['.ts', '.tsx', '.mjs'])) {
const path = repoPath(file)
if (path.startsWith('src/migrations/')) continue
if (allowlist.has(path)) continue
const lines = readFileSync(file, 'utf8').split('\n')
for (const [index, line] of lines.entries()) {
if (term.pattern.test(line)) offenders.push(`${path}:${index + 1}`)
}
const lines = readFileSync(file, 'utf8').split('\n')
for (const [index, line] of lines.entries()) {
if (term.pattern.test(line)) offenders.push(`${path}:${index + 1}`)
}
}
}
expect(offenders, term.remedy).toEqual([])
},
)
expect(offenders, term.remedy).toEqual([])
})
})
describe('eslint legacy ignore lists', () => {

View file

@ -63,8 +63,8 @@ describe('recentVisits storage', () => {
for (let index = 0; index < MAX_ENTRIES + 2; index += 1) {
recordRecentVisit(
sampleEntry({
href: `/campanha/municipios/praca-${index}`,
label: `Município `,
href: `/campanha/municipios/municipio-${index}`,
label: `Município ${index}`,
visitedAt: index,
}),
)
@ -72,8 +72,9 @@ describe('recentVisits storage', () => {
const visits = listRecentVisits()
expect(visits).toHaveLength(MAX_ENTRIES)
expect(visits[0]?.href).toBe(`/campanha/municipios/praca-${MAX_ENTRIES + 1}`)
expect(visits.at(-1)?.href).toBe('/campanha/municipios/praca-2')
expect(visits[0]?.href).toBe(`/campanha/municipios/municipio-${MAX_ENTRIES + 1}`)
expect(visits[0]?.label).toBe(`Município ${MAX_ENTRIES + 1}`)
expect(visits.at(-1)?.href).toBe('/campanha/municipios/municipio-2')
})
it('clears stored visits', () => {