The three /simplify reviewers reported after the merge, so their findings
became a fill-in instead of a fix. The lot is anchored on a measurement that
contradicts "small mesh, small artifact": the 19 Salvador zones carry 9,120 arc
points against 9,139 for ALL 417 Bahia municipalities, because both simplify
constants are relative to the file's own bbox and were copied verbatim from the
sibling script -- the same "1e4 grid" is ~103 m statewide and ~3.9 m in a city
26x narrower, on a map that stops at maxZoom 10.
That measurement inverts one reviewer's recommendation, which is why it is
written down rather than left to be re-derived: extracting the shared constants
as they stand would cement the defect and give it the authority of a policy, so
what gets shared between the two build scripts is the ground scale.
The other three: nothing runs featureMapKey over a real feature outside the
e2e, so the three spellings of the map key can drift and paint grey with no
error anywhere; Promise.all over both meshes plus the memoized rejection turned
a 19-polygon chunk into a single point of failure for the whole map, where the
municipal mesh alone used to suffice; and the "approximate, not the official TSE
boundary" caveat hangs off the zone LIST's render condition, so it vanishes in
comparison mode while the approximate mesh is still painted.
Also records what this delivery orphaned (getTerritoryFeature lost its last
production caller when fitMapToHighlights stopped consulting geometry modules,
and knip cannot see it because specs are entry points), and bumps B5 F2's
call-site counter to five.
Records the Salvador zone delivery where the next agent will look for it —
roadmap, plan as-built, notebook and the geometry/map sections of AGENTS.md —
including the two measurements that are easy to re-derive wrongly (the bay
water in Salvador's municipal polygon, and ZE 16 wrapping ZE 14, which is why
neither an area ratio nor a centroid check is a valid overlap test).
Simplify fixes: the build script re-indexes simplified geometries by a unique
id instead of neighbourhood name, so a repeated name in a future mesh cannot
silently collapse two polygons inside a committed artifact; a stale comment
claiming zone polygons do not exist; and two int assertions that the Fase 0
catalog hydration turned tautological.
Ledgers the Aikido outage: the scanner returns Opengrep exit 2 for any input,
so the mandatory per-change scan was skipped, not passed.
Salvador era um polígono só carregando os votos da cidade inteira, então o
coroplético não conseguia responder "qual pedaço de Salvador está fraco / é
meu". As 19 ZE ganham polígono próprio, dissolvido da malha de bairros do IBGE
(Censo 2022) pela lista de circunscrição da RA 02/2017 do TRE-BA, e o mapa passa
a pintar por map key (slug para as zonas, ibgeCode para o resto) em vez de
codarea. O polígono municipal de Salvador continua desenhado por baixo, sem dado
e sem interação, para não abrir buraco no contorno do estado.
Aproximação declarada na copy e no cabeçalho do script: o TRE publica a
circunscrição como lista de bairros, então o bairro é o átomo geométrico —
Periperi, que a resolução corta na Rua das Pedrinhas, vai inteiro para a ZE 17.
competitiveRank segue por cidade (o artefato TSE só tem federalRankByIbgeCode).
Sem migration, sem collection, sem action, sem Consent.
Espelha a aresta leadership.stateDeputies do B31 do lado do deputado, com célula compartilhada bidirecional, loadLeadershipOptions e casca própria de formActions.
As funções rodavam no default iad1 (Virgínia) enquanto o Neon fica em
sa-east-1 (São Paulo): cada query do Payload atravessava o equador duas
vezes (~120 ms por ida-e-volta) e uma página do /campanha faz uma dúzia.
Medido em produção: 410-470 ms de TTFB para um 307 que não toca o banco.
- vercel.json fixa regions: [gru1], mesma região AWS do banco (Hobby
permite uma região, mas permite escolher qual).
- loadStatewideSuggestedGoals passa a ser exportada e é disparada dentro
do Promise.all que já lê o escopo, na lista e no dashboard: o read do
global campaignGoals saía sozinho no fim da requisição.
- loadMunicipalityScope lê os pledges em paralelo quando o ator é
irrestrito e não há filtro (aí o IN de 435 ids não exclui nada);
qualquer escopo mais estreito continua esperando os ids.
Débitos medidos no caminho e registrados em TECH-DEBT: o fan-out de
leituras do ator em getFreshCampaignUser (~5 por render) e a leitura
dupla em authenticateCampaignToken.
Staff gera convite de autopreenchimento na coluna de ações da lista, reutilizando createCampaignInvite e alinhando validação de celular ao link de contato do B28.
CampaignCopyableCell is a Client Component; passing formatDisplay as a
function from the leaderships RSC page broke serialization in production.
Use a preformatted displayValue string at the call sites instead.
Coluna "Dobradinhas" em /campanha/liderancas com chips clicáveis
para /campanha/dobradinhas/<slug> e edição via Popover+Command
(padrão B27, escrita via server action delta + lock, como B19).
- LeadershipRowViewModel.stateDeputies (StateDeputySummary[]) via
loadStateDeputySummaries, substituindo stateDeputyNames.
- setLeadershipStateDeputyMembership(Record): delta com
withPayloadTransaction + acquireTextAdvisoryLocks + revalidatePath.
- nextStateDeputyIdsAfterMembership (lib/leadershipStateDeputyMembership.ts),
espelha nextAdvisorIdsAfterMembership.
- loadStateDeputyOptions estendido com plainName/party/slug.
- Catálogo entregue uma vez por tabela via leadershipColumns(options)
(columns factory, mesmo precedente de municipalityListColumns).
/simplify (1ª rodada): corrige revert otimista por delta (não pelo
snapshot inteiro), remove fallback silencioso na adição, extrai
sameIdSet compartilhado com MunicipalityListAdvisorsControl (B27), e
evita leitura+revalidação supérfluas no caminho no-op da action.
/simplify (2ª rodada, pós-rebase): troca o StateDeputyOptionsProvider
(Context) inicial pela columns factory acima — a 1ª rodada tinha
validado o Context por falta da evidência de que MunicipalityList.tsx
já resolve o mesmo problema sem Context; corrige revalidatePath para
o id concreto em vez do template dinâmico '/campanha/liderancas/[id]'
(que invalidava toda ficha a cada toggle); remove non-null assertion
movendo a guarda para dentro do setCurrent updater.
Gate verde: tsc, lint --max-warnings=0, format:check, unit 530/530,
int 406/406 (incl. os 6 de campaignLeadershipStateDeputyMembership),
build. Aikido: 0 achados nos arquivos novos/editados.
Conflito unico em docs/roadmap.md, nas cinco superficies em que B14 aparece
(linha "Atualizado em", bullet do item, no do grafo, Paralelizaveis/Janela 1 e
Cortes seguros): main tinha B14 pendente mais os registros de hoje (B38 e o
fill-in de limitar o mapa a Bahia), a feature tinha B14 entregue sem eles.
Resolvido preservando os dois lados em todas elas.
Registro de roadmap-item de dois pedidos de produto de 2026-07-26, commitado
como estava na arvore de trabalho do checkout principal:
- B38 sidebar recolhido em tablet (`collapsible="offcanvas"`, trigger em `md+`,
default por faixa de viewport com o cookie `sidebar_state` vencendo apos o
toggle) + entrada no grafo de dependencias.
- Fill-in: limitar pan/zoom do mapa da Bahia a `BAHIA_BOUNDS` (`maxBounds` com
folga de 10%, `maxBoundsViscosity: 1` e `minZoom` calculado no
`ResizeObserver` que ja existe), so em `BahiaMap.tsx`.
- Nota de dependencia soft do B18 para o B38: com o rail recolhido, o submenu
de filtros salvos precisa tratar collapsed como "abrir rail".
Terceira rodada de /simplify sobre o B14, com dois enunciados errados corrigidos:
- A propriedade da fronteira compartilhada NAO vem da ordem do `.find()`, e sim
da convencao meio-aberta do ray casting (`point.lng < crossingLng`): a aresta
pertence ao poligono cujo interior fica a leste (ao norte, se horizontal). Tres
comentarios diziam o contrario, e o do unit test dizia "oeste" enquanto
afirmava `east`.
- O plano dizia que o `Promise.all` esconde o decode da malha atras do fix de
GPS. Nao esconde: o decode roda no topo do modulo, quando o chunk executa. O
paralelo continua certo por outro motivo (decode ~2 ms; sequencial jogaria os
42 KB no caminho critico quando o fix vem do cache).
Simplificacoes: `unsupported` virou estado proprio, entao `failureCopy` deixa de
ser anulavel e caem duas leituras defensivas inalcancaveis; "Voce esta em " era
escrito duas vezes (frase sr-only + JSX), o que anulava justamente a invariante
que o comentario alegava; `zoneCityHrefs` virou `Partial` porque a busca falha
para os 416 municipios sem zona e o guard existia sem o tipo exigir;
`MunicipalityForIbgeIndex` era duplicata campo-a-campo de `AccessibleMunicipality`;
`kmFormatter` duplicava `oneDecimalFormatter`; `RecentlyVisitedCard` carregava
tres props que nenhum caller passa (dez condicionais mortas); e o bbox de feature
estava escrito em dois specs (agora `tests/helpers/featureBounds.ts`, compartilhado
por vitest e Playwright).
Copy: fora da Bahia sem nada por perto dizia a mesma coisa em duas frases e nunca
dizia que voce estava fora do estado. Agora o headline afirma o fato e o detalhe
reusa "Nenhum municipio da sua carteira por perto.".
Tambem limpa um achado de knip herdado do main (`MunicipalityPoliticalTrendViewModel`
exportado sem uso) e registra no plano o caminho afiado do debito de payload RSC
mais dois adiados com gatilho.
Card "Onde estou" no dashboard staff: point-in-polygon contra a malha
municipal que o painel do mapa ja carrega, sem artefato de centroides,
sem migration, sem collection, sem server action e sem Consent -- a
posicao e lida por navigator.geolocation e casada no browser, nunca sai
do aparelho.
- src/lib/municipalityProximity.ts: haversine, ray casting, centroide
ponderado por area e uniao discriminada
(inScope | zoneCity | outOfScope | outsideBahia)
- src/utilities/campaignGeolocation.ts: flag de sessao, leitura guardada
de permissao e requestCurrentPosition com falha tipada
- Salvador nao ganha ZE chutada: link para a lista filtrada, com href
serializado no servidor (importar o serializador canonico no cliente
custava 21 kB de First Load JS)
- fallback fora da carteira ordenado por centroide, teto de 150 km
- fix grosseiro (>10 km) mostra o resultado com ressalva de precisao
/campanha First Load JS: 265 -> 268 kB.
Fecha a entrega do B33 registrando o achado do /simplify que sobrou maior
que cleanup: StateDeputyFilters é o 3º call site do mesmo scaffold de
navegação/debounce de TerritoryFilters/MunicipalityFilters. Novo fill-in
B33+ (plano escala-dry-pos-b33.md) extrai useCampaignListFilterNavigation.
Agregado SQL do facet de partido formalizado como Adiado com gatilho no
plano-pai; referência cruzada obsoleta em escala-dry-pos-e10.md corrigida
(party é texto livre, não enum fechado — o gatilho ali não disparou).
- stateDeputyListUrl.ts / stateDeputyListFilters.ts: contrato de URL/filtro
próprio (sort/dir, filtro de Partido multi + sentinela "Sem partido"),
espelhando municipalityListUrl.ts (paginação/resolveListUrl) e
territoryListUrl.ts (sort keys/labels)
- stateDeputyData.ts: where/sort resolvidos no banco + facet de partido
(loadStateDeputyPartyFacet), select estreito na query principal
- StateDeputySortableHead / StateDeputyHeaderFilter / StateDeputyFilters:
wrappers finos sobre o chrome compartilhado (shared/CampaignSortableHead,
CampaignHeaderFilterPopover), paridade mobile incluída
- dobradinhas/page.tsx: redirect canônico em duas fases, CampaignTable
sempre montada com caption + empty state, resumo de ordenação aria-live
- Correção pós-entrega (design review): "Sem partido" no mobile + caption
da tabela + copy plural do empty state
- /simplify (1ª rodada): buildStateDeputyPartyOptions compartilhado, remove
discriminante filterParam redundante, dedupe/if mortos em
stateDeputyListUrl.ts
- /simplify (2ª rodada, pós-rebase): remove cast/type morto em
stateDeputyData.ts, stateDeputyColumns usa StateDeputyFilterOption em vez
de duplicar a forma, inlinea parsePartiesParam; extrai truncatedNamesLabel
compartilhado em campaignListUrl.ts (3º call site — municipalityListFilters
e territoryListFilters tinham a mesma função com nomes distintos)
Gate verde (tsc, lint, format, check:cycles, 396 testes, build); Aikido
0 achados nos 7 arquivos tocados nesta rodada. Docs:
docs/plans/ordenacao-filtros-lista-dobradinhas.md, docs/roadmap.md,
.cursor/rules/projects/nucleos-eleitorais.mdc.
- botão WhatsApp por linha duplicado (2 call sites) — gatilho: 3º site
- copyText vs copyMessage em SupporterShareKit — gatilho: 3º site de copy-to-clipboard
O /simplify da escala relativa do mapa achou um bug semantico real em
computeAggregateTerritorialClass — somava o headroom dos slugs e comparava
contra a mediana de UMA unidade, classificando um grupo grande como expansao
quando ele e marginal. A mediana agora escala por slugs.length, com teste.
As bolhas sairam do centro do bounding box para o centroide real e ganharam
pane proprio (z-index 450), porque o bringToFront do hover as escondia atras
do poligono. Legenda e pintura passaram a compartilhar o mesmo predicado,
a copy dos quantis parou de prometer faixas de igual largura, e o subtitulo
do painel parou de dizer "em 2026" com 2022 selecionado (defeito do B11).
Fonte unica onde havia duas: NO_DATA_FILL, o raio das bolhas e
formatPlacementOrdinal (ex-formatMunicipalityVoteRank, agora client-safe em
electionFormat.ts).
Os quatro debitos maiores que o cleanup viraram o fill-in B13+
(docs/plans/escala-dry-pos-b13.md); a flakiness dos e2e, reproduzida em main
limpo, foi para o ledger com a causa afinada.
O coropleto do Inicio era quase monocromatico porque 0-100% dos validos comprime
uma disputa cujo topo estadual e 4,71%. Tres escalas relativas ao proprio
candidato, todas em classes discretas: quantis (novo default), padrao proprio
(LQ) e posicao no municipio, ao lado de "% dos validos" (B11) e "Total (votos)".
Artefato v3: federalRankByIbgeCode com a colocacao entre todos os candidatos com
votos > 0 na geografia (2014/2018/2022), chaveado por ibgeCode porque e o que o
mapa desenha. Fica no artefato commitado, nao em query de request, que varreria
~100 mil linhas por ano para um dado imutavel. 534 -> 623 KB (orcamento 700 KB).
LQ virou 5 classes nas ancoras do E10, nao diverging: o plano pedia as duas
coisas e se contradizia. Vermelho x azul continua significando "Solla x
adversario" e segue exclusivo do compare.
computeAggregateTerritorialClass classifica um grupo de slugs como um territorio
so, somando insumos e rodando o mesmo classificador (LQ e razao; a media de
razoes nao e a razao das somas) - o E12 herda o helper. TERRITORIAL_CLASS_ANCHORS
mudou para src/lib/territorialClassAnchors.ts para nao arrastar o artefato TSE
ao bundle do cliente.
Quantis sobre o escopo de acesso do usuario, degradando para 3 classes abaixo de
10 municipios com a legenda declarando a contagem efetiva. Rank indisponivel em
2026 (dado TSE); LQ em 2026 reusa o padrao estadual de 2022.
Bolhas opcionais por validos projetados (area proporcional ao valor, cor da
classe E10, interactive: false). 417 marcadores medidos antes de otimizar - nao
derrubaram o frame, entao o corte para top-K nao foi feito.
Sem migration, sem collection, sem server action; bundle client de /campanha
byte a byte igual.
- MunicipalityListAdvisorsControl: stop advancing lastPropsIDsRef from a
successful delta response — it exists only to detect a genuine external
prop change, and writing it there made a later stale re-render of the
pre-edit content look external and clobber optimistic state back to it.
Also reconcile with the latest confirmed server set on the error path
once every in-flight delta settles (previously only the success path did),
and consolidate chip/tooltip name resolution onto the shared
advisorEntriesFromIds helper instead of a bespoke lookup with an
'#id' placeholder.
- Extract campaignJsonMutationErrorResponse/parseCampaignJsonRequestBody
(src/utilities/campaignJsonMutationRoute.ts) and adopt them in both
municipios/advisors/route.ts and municipios/expected-votes/route.ts,
removing the duplicated body-parse + error-mapping ladder.
- Municipality.ts's validateMunicipalityAdvisors now uses
uniqueRelationshipIds instead of re-inlining the Set+map+filter dance
the helper was extracted for.
Verified: tsc --noEmit, lint, format:check, check:cycles all clean;
full unit suite (450 tests) and the municipality/advisor int suites pass.
knip's pre-existing payload.config.ts load error is unrelated (P3).
Playwright's spawned webServer failed to boot in this sandbox (unrelated
module-resolution environment issue, reproduced even before this diff);
manual pnpm dev boots fine and the change is covered by unit/int tests.
Replaces the checkbox-list + "Salvar assessores" button in the
Assessors popover (/campanha/municipios) with removable chips + a
Command combobox, delta-based auto-save (one advisor add/remove per
request, never the whole array), and optimistic UI with request
sequencing to resolve out-of-order responses correctly.
- New POST /campanha/municipios/advisors JSON route (mirrors
expected-votes/), backed by setMunicipalityAdvisorMembership(Record)
in actions/municipality.ts (text advisory lock + transaction).
- nextAdvisorIdsAfterMembership moved to
src/lib/municipalityAdvisorMembership.ts with unit tests.
- Shared isSameOriginRequest (src/utilities/sameOriginRequest.ts) and
uniqueRelationshipIds (src/utilities/relationship.ts) helpers,
de-duplicating the same-origin CSRF check and the
Set+map(relationshipId)+filter pattern across advisor call sites.
- MunicipalityListAdvisorsControl.tsx rewritten: chips, Command
combobox, per-item pending affordance, rollback on error, and a
request-sequence guard so only the most recently sent delta's
response is adopted when concurrent optimistic updates settle.
- e2e coverage for the combobox auto-save flow; setup prewarms the new
POST-only route to avoid Playwright dev-server HMR aborting the
first request.
- Plan and roadmap marked as delivered (B27).
- openOnTouch/disabled em CampaignHoverTooltip para conviver com Popover
sem roubar o tap nem sobreviver atras dele; falsy content agora renderiza
o trigger sem wrapper (colapsa o ternario nos dois consumidores)
- formatAdvisorNamesTooltip + advisorEntriesFromIds compartilhado
(MunicipalityAdvisorAvatarStack) para dedupe entre lista e controle
- MunicipalityListAdvisorsControl: hover mostra nomes completos (coordenador)
- MunicipalityListTrendControl: hover mostra a justificativa (politicalTrend.note,
E4R), com whitespace-pre-wrap preservado; leitura tambem dentro do Popover em toque
- testes unit dos wrappers/no-op e do helper
/simplify (pos-rebase): corrigido HEADER_TOOLTIP_COUNT (4->10) no teste da
celula de assessores, que ficou obsoleto depois do rebase absorver o B22
(10 tooltips de header no thead staff) - o teste passava a comparar 5 vs 11
sem essa correcao; formatAdvisorNamesTooltip agora tipa ReactElement | null
em vez de ReactNode, removendo os casts 'as React.ReactElement' no teste;
comentarios de baixa informacao removidos/enxugados em CampaignHoverTooltip
e nos dois controles; docs: plano, roadmap e notebook do projeto marcados
como entregues.
Gate verde: tsc, lint (0 warnings), format, 446 unit, build anterior verde.
Aikido: 0 findings.
/simplify: extrai campaignHoverExplanationClassName/campaignHoverTooltipAlign para CampaignHoverTooltip.tsx (elimina duplicacao entre CampaignTableHead, MunicipalitySortableHead e MunicipalityGoalAccountCard), troca non-null assertion de campaignConceptOneLiner por lookup em Record, corrige copy da coluna Assessores (coordenador, nao candidato) e restaura comentario de invariante do sort de goalCoverage