docs: rascunho B92–B94 — sugestões no empty do wizard de município

Quebra o empty state do passo 1 dos wizards (/campanha/acoes) em três
entregáveis: chassis+esquecidos (B92), continuity visitados/última ação (B93),
geo mais próximo (B94). Aguarda gate de confirmação antes do agent:register.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-08-01 06:48:37 +00:00
parent d9a2414656
commit 16d0803c2a
No known key found for this signature in database
3 changed files with 394 additions and 0 deletions

View file

@ -0,0 +1,143 @@
# Empty state do wizard — chassis + ranking de esquecidos
Status: rascunho
Atualizado em: 2026-08-01
Issue: —
Priority: P1
Model: composer-2.5
Impeccable: B — encaixe em `WizardMunicipalitySearchStep` (passo 1 dos wizards `/campanha/acoes/<slug>`)
Appetite: ~1–1,5 dia eng; modo suggest no home-search + rank puro + lista no idle; sem migration
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` (Clarity under pressure; ação→local) / `DESIGN.md` (register `product`) · tema `data-theme='campaign'` · B60 `WizardMunicipalitySearchStep` · B68 `HomeSearchHitRow` / suggest · B59 `CampaignWizardShell`.
Na implementação (`work-issue`): craft compacto → critique → polish.
Brief compacto:
- **Persona / contexto:** assessor ou CG abre “Ajustar votos” / “Registrar sinal” no Início e cai no passo “Em qual município?” ainda sem digitar — hoje a região abaixo do input é deserto.
- **Job principal:** com zero teclas, ver municípios **prováveis** (esquecidos / frios no escopo) e avançar com um toque; digitar continua refinando via B60.
- **Estratégia de cor:** Restrained — mesmos hits do wizard; título de seção discreto; sem card.
- **Edit where you see:** não — descoberta/navegação de fluxo (select = auto-avanço B60).
- **Anti-goals:** mapa no empty; command palette; misturar hits de busca tipada com sugestões na mesma lista; motor E11 / `allocationDecision` neste passo; hero de KPI.
### Wireframe (texto)
```text
┌─ /campanha/acoes/<slug> (sem ?municipio=) ──────────────┐
│ [←] Ajustar votos [✕] │
│ Em qual município? │
│ ┌─ Buscar município ─────────────────────────────────┐ │
│ │ Nome do município… │ │
│ └────────────────────────────────────────────────────┘ │
│ Prováveis │
│ · Cairu · Baixo Sul · 2022 … [alta?] │
│ · Valença · … │
│ · … (até ~8; select = avança com ?municipio=) │
└────────────────────────────────────────────────────────┘
Fora do frame: strip de ações do Início; Continuity (B93)
e geo (B94) entram depois como linhas prefixadas.
```
## Dados → decisão → apresentação
- **Vou apresentar dados?** Sim — lista curta de municípios do escopo do ator, ranqueados por frescor + desempates operacionais.
- **Decisões desbloqueadas:**
- Assessor/CG: “em qual município desta carteira/estado eu executo **esta** ação agora, sem digitar o nome?”
- Implicação: atacar primeiro quem está **frio** (sem sinal) e, empate, quem a campanha já investiu mais / tem tendência / classe / volume de votos mais relevante.
- **Forma escolhida:** lista ranqueada (mesmo degrau B48/B60/B68 — `HomeSearchHitRow`) — **por quê** a decisão é “qual abrir”. **Rejeitado:** mapa; `CampaignMetricStrip`; chart; chips sem readout (perda de contexto 2022 já no hit).
- **Profile:** até ~8 itens (`HOME_SEARCH_SUGGEST_LIMIT` / paridade B68); scoped ao access; relativo/local (nome + TI + readout 2022 opcional); **dias sem sinal** como chave primária (não % estadual).
- **Anti-goals de dado:** sem % estadual absoluto; sem gauge; sem inventar métrica nova além de reusar `lastSignalAt`, E14, tendência, E10, votos 2022 já no scope/artifact.
## Contexto
**B60 ✓** entrega a etapa de busca de município nos wizards (`WizardMunicipalitySearchStep` + `POST /campanha/home-search` `mode: 'wizard-municipality'`). Com query inativa a região fica **idle vazia** — B60 adiou explicitamente “chips de recentes/prioritários”.
**B68 ✓** resolveu o empty da busca do **Início** (`mode: 'suggest'`, rank por papel: assessor = carteira fria; CG = prioritários `alta` × déficit). O wizard **não** consome esse modo: precisa de política própria (ação-primeiro, não omnibox) e de desempates que o pedido de produto (2026-08-01) nomeou: envolvimento (E14), tendência política, classe territorial (E10), número de votos.
Este item é o **chassis + ranking server de esquecidos**. Continuity (visitados / última ação) e geo ficam em **B93** / **B94**.
## Objetivos
- Quando `!query.isActive` no passo de município do wizard: mostrar seção **Prováveis** (copy craft; default “Prováveis”), não o deserto.
- Novo modo tipado no `home-search` (recomendação: `mode: 'wizard-municipality-suggest'`) distinto de `suggest` (B68) e de `wizard-municipality` (B60 search) — política e payload próprios; staff-only (layout `acoes/` já gateia).
- Rank puro client-safe em `src/lib/` (irmão de `homeSearchSuggest.ts`), pinado em unit:
1. **Primário:** mais dias sem sinal primeiro (`lastSignalAt` via `resolveMunicipalityLastSignalAt`; `null` = mais frio).
2. **Desempates (nesta ordem):** envolvimento E14 (maior `engagementLevelRank` primeiro; `null`/`Sem nível` por último) → tendência (`desfavoravel` > `neutra` > `favoravel` > `null`) → classe E10 (`territorialClassSortWeight` desc; `sem_base`/`null` por último) → votos 2022 do candidato no município (maior primeiro; sem série = por último) → nome `pt-BR`.
- Cap ~8; access `overrideAccess: false`; assessor nunca vê fora da carteira.
- Digitar (≥ limiar B47) **substitui** sugestões pelos hits B60 (não misturar).
- Select = `wizardActionHref` (auto-avanço intacto).
- Sem migration / Consent / collection nova.
## Decisões travadas
- **Modo dedicado `wizard-municipality-suggest`, não reusar `suggest` do Início.** Wizard é ritual ação→local; B68 filtra CG a `priority === 'alta'` e usa déficit — aqui o pedido é **esquecidos do escopo inteiro** com desempates E14/tendência/classe/votos. **Rejeitado:** chamar `loadHomeSearchSuggestions` no wizard (política errada); search com `q=""` (contradiz B66/B68).
- **Primário = frescor (`lastSignalAt`), não só `lastUpdateAt`.** Mesma semântica E9/B68 (max update × pledge). **Rejeitado:** só `municipality.updatedAt` do Payload (ruído de admin); só `lastUpdateAt` (ignora pledges).
- **Desempates na ordem do pedido de produto** (envolvimento → tendência → classe → votos), com pesos já existentes (`engagementLevelRank`, ordem de tendência acima, `territorialClassSortWeight`, votos nominais 2022 do artifact/scope). **Rejeitado:** déficit de meta como desempate neste item (B68 já cobre no Início; misturar duas “inteligências” no mesmo empty confunde); LQ contínuo no lugar da classe.
- **Lista flat com título de seção; reason labels por linha ficam para B93/B94** (neste slice todas as linhas são “esquecidos”). **Rejeitado:** cards; multi-grupo neste appetite.
- **i18n / naming:** identificadores `wizardMunicipalitySuggest`, `rankWizardMunicipalitySuggestions`, `loadWizardMunicipalitySuggestions`; strings pt-BR (“Prováveis”, …).
## Questões em aberto
- **Título da seção?** **Opções:** A “Prováveis” | B “Sugestões” (paridade B68) | C “Para esta ação”. **Recomendação:** **A** — distingue do omnibox do Início. _(assumido — craft)_
- **CG inclui municípios `priority !== 'alta'`?** **Opções:** A sim, escopo completo frio-primeiro | B só `alta` como B68. **Recomendação:** **A** — o pedido é esquecidos + desempates de investimento; prioridade alta já é sinal na linha (`showPriority`). _(assumido — validar com produto)_
- **Votos = série Solla 2022 no município (artifact) vs estimativa central de pledges?** **Opções:** A votos 2022 nominais | B estimado central. **Recomendação:** **A** — estável, sem live cache, já no readout do hit. _(assumido)_
## Abordagem proposta
```mermaid
flowchart LR
Idle["WizardMunicipalitySearchStep !query.isActive"] --> Suggest["POST home-search mode=wizard-municipality-suggest"]
Suggest --> Load["loadWizardMunicipalitySuggestions"]
Load --> Scope["loadMunicipalityScope + extraSelect"]
Scope --> Rank["rankWizardMunicipalitySuggestions"]
Rank --> Rows["HomeSearchHitRow + wizardActionHref"]
Type["query.isActive"] --> Search["mode=wizard-municipality B60"]
```
Componentes:
- **`src/lib/wizardMunicipalitySuggest.ts`** (puro): input tipado (`slug`, `name`, `lastSignalAt`, `engagementLevel`, `politicalTrend`, `territorialClass`, `votes2022`) + `rankWizardMunicipalitySuggestions(inputs, limit)` + constantes de ordem de tendência; unit-pinned.
- **`src/utilities/homeSearch/loadWizardMunicipalitySuggestions.ts`** (`server-only`): `loadMunicipalityScope` com `extraSelect` (`lastUpdateAt`, `engagementLevel`, `politicalTrend`, …); classe via helper E10 já usado na lista/map; votos 2022 do artifact/`bahiaElectionAggregates` (ou campo já no scope se existir); `overrideAccess: false`.
- **Rota** `home-search/route.ts` + schema zod discriminado: ramificar o novo mode; resposta no shape `WizardMunicipalitySearchSuccessResponse` (ou irmão com `resultKind: 'wizard-suggest'`).
- **`WizardMunicipalitySearchStep`**: fetch suggest no idle; cancelar ao digitar; render seção + hits; empty tipado só quando search ativo e 0 hits.
- **Reuse:** `HomeSearchHitRow`, `HomeSearchMunicipalityVoteTrailing`, `postCampaignJson`, `useHomeSearchQuery`, copy em `campaignWizardCopy.ts`.
- **Migration:** Sem migration, sem collection, sem Consent.
## Dependências
- Duras: nenhuma aberta (B59 ✓, B60 ✓).
- Soft: B68 ✓ (padrão suggest ≠ search); E9/E10/E14 ✓ (sinais de desempate); artifact TSE para votos 2022.
## Não escopo
- Visitados recentes / última ação do ator → **B93**.
- Município mais próximo (geo) → **B94**.
- Empty da busca do Início (já **B68**); unificar políticas B68↔wizard.
- Motor E11 / fila de alocação no wizard.
- Leader (wizards são staff-only).
## Rabbit holes
- **Unificar `rankHomeSearchSuggestMunicipalities` e o rank do wizard num “suggest engine” genérico.** Se alguém “só completar”: API parametrizada prematura. **Mitigação:** dois módulos puros até 3º call site (precedente depth check).
- **Carregar coverage/déficit/E11 no loader do wizard.** Explode latência do idle. **Mitigação:** só campos de desempate listados; déficit fica no B68.
- **Reason chips + multi-fonte neste PR.** Mistura continuity/geo sem contrato de merge. **Mitigação:** B93/B94.
## Adiado com gatilho
- **Comparator compartilhado de frescor** entre B68 e wizard. Revisitar no 3º call site (ledger O0+/DRY).
- **RSC embed do suggest** (evitar POST no mount do passo). Revisitar se latência idle for P1 em campo.
- **Reason label “Sem atualização há N dias” na linha.** Revisitar se craft/B93 pedir diferenciação visual além da ordem.
## Referências
- GitHub Issue — (após register)
- [`WizardMunicipalitySearchStep.tsx`](../../src/components/campaign/shared/WizardMunicipalitySearchStep.tsx)
- [`homeSearchSuggest.ts`](../../src/lib/homeSearchSuggest.ts) / [`loadHomeSearchSuggestions.ts`](../../src/utilities/homeSearch/loadHomeSearchSuggestions.ts)
- [`busca-municipio-wizard.md`](busca-municipio-wizard.md) (B60 — adiados)
- [`sugestoes-busca-vazia-inicio.md`](sugestoes-busca-vazia-inicio.md) (B68)
- [`engagementLevel.ts`](../../src/lib/engagementLevel.ts) · [`territorialClassSortWeight.ts`](../../src/lib/territorialClassSortWeight.ts)
- AGENTS.md — access advisor; `overrideAccess: false`; naming EN / copy pt-BR
- `PRODUCT.md` / `DESIGN.md` — Clarity under pressure; Field Desk

View file

@ -0,0 +1,126 @@
# Empty state do wizard — continuity (visitados + última ação)
Status: rascunho
Atualizado em: 2026-08-01
Issue: —
Priority: P2
Model: composer-2.5
Impeccable: B — prefixo client no empty de `WizardMunicipalitySearchStep` (após B92)
Appetite: ~0,75–1 dia eng; merge client de recentVisits + last-acted localStorage; sem migration
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` (Feel the action; Clarity under pressure) / `DESIGN.md` · tema `campaign` · B92 chassis · `recentVisits.ts` · Quadro `RecentlyVisitedCard`.
Na implementação: craft compacto → critique → polish.
Brief compacto:
- **Persona:** staff que **acabou** de mexer num município ou voltou ao ritual no mesmo aparelho — quer repetir o local sem re-digitar.
- **Job principal:** no idle do passo 1, ver no topo “onde eu estava / onde eu agi” antes dos esquecidos server.
- **Estratégia de cor:** Restrained; secondary discreto na linha (“Visitado”, “Última ação”).
- **Edit where you see:** não.
- **Anti-goals:** sync multi-dispositivo; POST de histórico ao servidor; misturar continuity com hits tipados; redesign do card do Quadro.
### Wireframe (texto)
```text
┌─ Wizard passo município (idle, pós-B92) ───────────────┐
│ [input busca…] │
│ Prováveis │
│ · Itabuna · … · Última ação │
│ · Cairu · … · Visitado │
│ · Valença · … · Visitado │
│ · … (fill B92 esquecidos, dedup) │
└────────────────────────────────────────────────────────┘
Geo (B94) pode ocupar 1 slot acima de Continuity.
```
## Dados → decisão → apresentação
- **Vou apresentar dados?** Sim — atalhos de continuidade (client) mesclados à lista server B92.
- **Decisões:** “volto ao município em que eu **agi** ou que **abri** há pouco?”
- **Forma:** mesmas linhas `HomeSearchHitRow` + secondary/reason curto. **Rejeitado:** seção separada com cards; timeline.
- **Profile:** 1 última ação + até ~3–5 visitados município; dedup com geo/esquecidos; cap total da lista ~8.
- **Anti-goals:** sem enviar `localStorage` ao servidor; sem PII além do que já vive no device (padrão visitados).
## Contexto
B92 entrega o idle com esquecidos server. O pedido de produto (2026-08-01) também pede: (a) **últimos municípios acessados**; (b) **último município em que o usuário executou qualquer ação**.
Já existe `teqo:campaign:recent-visits` (`recentVisits.ts`, Quadro). B68 **adiou** visitados no suggest do Início por hidratação — no wizard o idle já é client (`WizardMunicipalitySearchStep`), então o merge é natural **depois** do mount.
“Última ação” **não** tem índice server hoje (autores espalhados: `municipalityUpdate.author`, `votePledge.declaredBy`/`estimatedBy`, `campaignDemand.createdBy`, …). Índice unificado = migration/query cara. Neste item: **localStorage device-local**, escrito nos success paths dos wizards/actions de município.
## Objetivos
- Após mount, com `!query.isActive`: mesclar no topo da lista B92:
1. **Última ação** (0–1) — slug gravado em storage dedicado.
2. **Visitados recentes** (`kind === 'municipality'`) — extrair slug do `href`, filtrar ao escopo se o server já devolveu o universo **ou** validar contra hits suggest / lista acessível passada como prop.
- Dedup estável: geo (B94) > última ação > visitados > esquecidos B92; uma linha por slug.
- Reason labels pt-BR curtos na secondary ou trailing textual.
- Gravar `lastActedMunicipality` (nome final no craft) ao **sucesso** de writes do ritual (ajuste votos, sinal, tendência, liderança, demanda — paths que já fecham wizard com município). Fail-soft se storage bloquear.
- Sem migration / Consent / envio de lat-lng ou histórico ao server.
## Decisões travadas
- **Continuity 100% client (localStorage), não query server “last modified by me”.** Barato, mesmo padrão de visitados; cross-device fica Adiado. **Rejeitado:** UNION multi-collection por `author`/`declaredBy` neste appetite (latência + índices + access); collection `municipalityActorTouch` (migration cara sem evidência).
- **Gravar última ação no success do write, não no select do passo 1.** Select sem commit não é “executou ação”. **Rejeitado:** gravar no `router.push(?municipio=)` (falso positivo se abandonar).
- **Reusar `recentVisits` para acessos; storage separado para last-acted.** Semânticas diferentes (dwell 2s vs write). **Rejeitado:** sobrecarregar `RecentVisitEntry` com `kind: 'acted'`.
- **Merge só no idle; search tipado ignora continuity.** Paridade B68/B92.
- **i18n:** `campaignLastActedMunicipality` / `recordLastActedMunicipality` / `listWizardContinuitySlugs`; copy “Última ação”, “Visitado”.
## Questões em aberto
- **Quantos visitados no prefixo?** **Opções:** A 3 | B 5 | C todos os 8 do storage. **Recomendação:** **A** (3) — deixa espaço para esquecidos/geo no cap 8. _(assumido)_
- **Escopo assessor:** visitado fora da carteira (mudança de advisors)? **Opções:** A ocultar | B mostrar e falhar no select. **Recomendação:** **A** — filtrar contra slugs do payload suggest ou prop `accessibleSlugs`. _(assumido)_
## Abordagem proposta
```mermaid
flowchart LR
Mount["client mount idle"] --> Server["hits B92"]
Mount --> Local["recentVisits + lastActed"]
Local --> Merge["mergeWizardMunicipalitySuggestions"]
Server --> Merge
Merge --> Rows["HomeSearchHitRow + reason"]
Write["wizard action success"] --> Record["recordLastActedMunicipality(slug)"]
```
Componentes:
- **`src/lib/campaignLastActedMunicipality.ts`** (client-safe storage helpers, irmão fino de `recentVisits.ts`).
- **`src/lib/wizardMunicipalitySuggestMerge.ts`** (puro): ordena/dedup fontes `{ source, slug }[]` + hits server → lista final com `reason`.
- **`WizardMunicipalitySearchStep`**: após fetch B92, merge client; skeleton/idle sem flash errado (não render continuity no SSR).
- **Hooks de gravação:** pontos de sucesso já existentes nos formActions/wizards (votos, sinal, tendência, liderança, demanda) — uma chamada `recordLastActedMunicipality(slug)`; sem abstração genérica se &lt;3 sites usar helper inline até o 3º.
- **Migration:** nenhuma.
## Dependências
- Dura: **B92**.
- Soft: visitados ✓; wizards A1–A5 ✓.
## Não escopo
- Geo → **B94**.
- Last-acted server / multi-device.
- Continuity no empty do Início (B68 adiou; gatilho separado).
- Alterar card do Quadro.
## Rabbit holes
- **Helper genérico “campaignLocalShortcuts”.** **Mitigação:** dois módulos pequenos até 3º consumidor.
- **Gravar last-acted em toda mutation Payload via hook global.** Blast radius. **Mitigação:** só success paths do ritual de ação.
## Adiado com gatilho
- **Last-acted server (cross-device).** Revisitar se o mesmo CG reportar “no celular não aparece o que fiz no notebook” em sessão observada.
- **Visitados no suggest do Início (B68 adiado).** Revisitar com evidência de sessão; não neste item.
## Referências
- GitHub Issue — (após register)
- [`recentVisits.ts`](../../src/utilities/recentVisits.ts) · [`visitados-recentemente.md`](visitados-recentemente.md)
- [`wizard-municipio-sugestoes-chassis.md`](wizard-municipio-sugestoes-chassis.md) (B92)
- AGENTS.md — sem Consent para storage local de UX autenticada (precedente B14/visitados)

View file

@ -0,0 +1,125 @@
# Empty state do wizard — município mais próximo (geo)
Status: rascunho
Atualizado em: 2026-08-01
Issue: —
Priority: P2
Model: composer-2.5
Impeccable: B — 1 linha prefixada no empty de `WizardMunicipalitySearchStep` (após B92)
Appetite: ~1 dia eng; reuso B14 (`campaignGeolocation` + `municipalityProximity`) no idle do wizard; sem migration / Consent
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` (Feel the action) / `DESIGN.md` · tema `campaign` · B14 `NearestMunicipalityCard` · B92 chassis · B93 merge order.
Na implementação: craft compacto → critique → polish. Gatilho `harden` se permission/prompt edge cases vazarem copy inconsistente com B14.
Brief compacto:
- **Persona:** staff em deslocamento (carro/praça) abre uma ação no celular e quer o município **onde está**.
- **Job principal:** no idle, um toque no “mais próximo” avança o wizard com o município resolvido.
- **Estratégia de cor:** Restrained; reason “Perto de você” / distância se B14 já formata.
- **Edit where you see:** não.
- **Anti-goals:** mapa embutido no wizard; POST de lat/lng; Consent novo; prompt agressivo a cada abertura de ação; card GPS gamificado.
### Wireframe (texto)
```text
┌─ Wizard passo município (idle) ────────────────────────┐
│ [input…] │
│ Prováveis │
│ · Salvador — ZE 5 · … · Perto de você (~1,2 km) │
│ · … (B93 continuity) │
│ · … (B92 esquecidos) │
└────────────────────────────────────────────────────────┘
Sem permissão / fora da BA / unsupported: linha some;
lista B92/B93 permanece.
```
## Dados → decisão → apresentação
- **Vou apresentar dados?** Sim — 0–1 município resolvido por geo no escopo do ator.
- **Decisões:** “executo a ação **aqui** (onde estou)?”
- **Forma:** uma linha na lista (pobre). **Rejeitado:** card “Onde estou” duplicando o Quadro; mapa Leaflet no passo.
- **Profile:** 1 item; client-only position; match contra malha já usada em B14.
- **Anti-goals:** sem transmitir posição ao servidor; sem reverse-geocode vendor.
## Contexto
B14 ✓ no **Quadro** (`NearestMunicipalityCard`): posição no browser, containment na malha municipal (+ ZE Salvador), fallback Haversine, prompt 1× por sessão de aba, sem Consent.
O pedido (2026-08-01) inclui o mesmo sinal no empty do **wizard**. O passo 1 hoje não carrega geometrias nem lista `AccessibleMunicipality` — B92 traz hits server; este item acrescenta a resolução client e o slot no merge (ordem: geo → continuity B93 → esquecidos).
## Objetivos
- No idle do wizard, tentar resolver município mais próximo **no escopo do ator** reusando `requestCurrentPosition` / `resolveNearbyMunicipality` / loaders lazy de geometria.
- Página `acoes/[slug]` (passo busca) passa prop `accessibleMunicipalities: AccessibleMunicipality[]` (slug/name/ibgeCode) a partir do scope — ou o client usa só os slugs do suggest B92 **mais** um payload mínimo RSC (recomendação: prop RSC, porque suggest é top-8 e o nearest pode estar fora do top frio).
- Prompt automático: **respeitar** `hasPromptedThisSession` de B14 (não re-assaltar se o Quadro já pediu; se wizard é a 1ª superfície da sessão, um prompt basta).
- Falhas (`denied` / `timeout` / `unsupported` / fora da BA / outOfScope sem nearest): **silêncio** — não montar linha de erro no ritual (diferente do card do Quadro que explica); usuário digita.
- `kind: 'inScope'` → uma hit row com reason; select = auto-avanço.
- Sem migration / Consent / POST de coordenadas.
## Decisões travadas
- **Client-only matching (paridade B14).** **Rejeitado:** POST lat/lng; PostGIS; artefato de centroides commitado (já revertido em B14).
- **Prop RSC de municípios acessíveis** para o matcher (não só top-8 do suggest). **Rejeitado:** resolver só dentro dos 8 esquecidos (falso “perto” se o município real ficou de fora do rank frio).
- **Fail soft sem UI de erro no wizard.** Ritual não deve virar troubleshooting de GPS. **Rejeitado:** copiar o card B14 inteiro para dentro do shell.
- **Uma linha, não card.** **Rejeitado:** embutir `NearestMunicipalityCard`.
- **i18n:** reusar `formatDistanceKm`; reason “Perto de você”; helpers `useWizardNearestMunicipality` se o efeito merecer nome (senão lógica no step até extrair no 2º call site).
## Questões em aberto
- **Prompt automático no mount do wizard se a sessão ainda não pediu?** **Opções:** A sim (paridade Quadro) | B só se permissão já `granted` | C CTA explícito “Usar minha localização”. **Recomendação:** **B** no v1 do wizard (zero surpresa no meio do ritual) + se `granted`, resolve quieto; CTA curto se `prompt`/`denied` e craft quiser. _(assumido — validar com produto; se campo pedir paridade total com Quadro, subir para A)_
- **Salvador multi-ZE outOfScope / zoneCity:** seguir B14 (lista filtrada no Quadro). No wizard: **Opções:** A omitir linha | B deep-link impossível sem município. **Recomendação:** **A** omitir — wizard exige `municipality` atômico. _(assumido)_
## Abordagem proposta
```mermaid
flowchart LR
Page["acoes/slug page RSC"] --> Prop["accessibleMunicipalities"]
Prop --> Step["WizardMunicipalitySearchStep"]
Step --> Geo["permission + position B14 utils"]
Geo --> Mesh["lazy bahiaGeometries + zones"]
Mesh --> Resolve["resolveNearbyMunicipality"]
Resolve --> Merge["slot geo no merge B93/B92"]
```
Componentes:
- **`acoes/[slug]/page.tsx`**: ao renderizar o search step, carregar scope enxuto (id/slug/name/ibgeCode) e passar como prop.
- **Ilha no step / hook:** permission + optional locate; dynamic import geometria; resolver; emitir `{ slug, reason, distanceKm? }` ou `null`.
- **Merge** (contrato com B93): fonte `geo` tem precedência máxima de ordenação.
- **Reuse:** `campaignGeolocation.ts`, `municipalityProximity.ts`, `loadMunicipalityGeometryModule`, `formatDistanceKm`.
- **Migration:** nenhuma. **Consent:** nenhum.
## Dependências
- Dura: **B92**.
- Soft: **B93** (merge order — se B94 landar antes, step aceita geo sem continuity; merge helper pode nascer em qualquer um e o outro consome).
- Soft: B14 ✓.
## Não escopo
- Continuity → **B93**.
- Ranking esquecidos → **B92**.
- Mudanças no card do Quadro.
- Geo para `leader`.
## Rabbit holes
- **Extrair `NearestMunicipalityCard` → package compartilhado pesado.** **Mitigação:** reusar utils puros; UI do wizard é uma hit row.
- **Prefetch da malha no Início para o wizard.** **Mitigação:** lazy no mount do step; memo do módulo já ajuda se Quadro carregou antes.
## Adiado com gatilho
- **CTA “Usar localização” quando permission ≠ granted.** Revisitar se sessão observada mostrar staff recusando o prompt silencioso e pedindo botão.
- **Centroid artifact** só se o lazy da malha pesar no mid-tier Android em campo.
## Referências
- GitHub Issue — (após register)
- [`municipio-mais-proximo.md`](municipio-mais-proximo.md) (B14)
- [`campaignGeolocation.ts`](../../src/utilities/campaignGeolocation.ts) · [`municipalityProximity.ts`](../../src/lib/municipalityProximity.ts)
- [`wizard-municipio-sugestoes-chassis.md`](wizard-municipio-sugestoes-chassis.md) (B92)
- AGENTS.md — sem Consent para geo local autenticada (B14)