From 16d0803c2ad67df4e0e8cc167b5d5c49e83aad88 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 06:48:37 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20rascunho=20B92=E2=80=93B94=20?= =?UTF-8?q?=E2=80=94=20sugest=C3=B5es=20no=20empty=20do=20wizard=20de=20mu?= =?UTF-8?q?nic=C3=ADpio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../wizard-municipio-sugestoes-chassis.md | 143 ++++++++++++++++++ .../wizard-municipio-sugestoes-continuity.md | 126 +++++++++++++++ docs/plans/wizard-municipio-sugestoes-geo.md | 125 +++++++++++++++ 3 files changed, 394 insertions(+) create mode 100644 docs/plans/wizard-municipio-sugestoes-chassis.md create mode 100644 docs/plans/wizard-municipio-sugestoes-continuity.md create mode 100644 docs/plans/wizard-municipio-sugestoes-geo.md diff --git a/docs/plans/wizard-municipio-sugestoes-chassis.md b/docs/plans/wizard-municipio-sugestoes-chassis.md new file mode 100644 index 00000000..784b76f0 --- /dev/null +++ b/docs/plans/wizard-municipio-sugestoes-chassis.md @@ -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/`) +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/ (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 diff --git a/docs/plans/wizard-municipio-sugestoes-continuity.md b/docs/plans/wizard-municipio-sugestoes-continuity.md new file mode 100644 index 00000000..59a39c29 --- /dev/null +++ b/docs/plans/wizard-municipio-sugestoes-continuity.md @@ -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 <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) diff --git a/docs/plans/wizard-municipio-sugestoes-geo.md b/docs/plans/wizard-municipio-sugestoes-geo.md new file mode 100644 index 00000000..c1ba50f1 --- /dev/null +++ b/docs/plans/wizard-municipio-sugestoes-geo.md @@ -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) From c53add3f8fb37289edfeda5e004d1c8d0e9ce69b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 06:57:30 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20registra=20B92=E2=80=93B94=20(Issue?= =?UTF-8?q?s=20#92=E2=80=93#94)=20p=C3=B3s-gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate 2026-08-01: sem título de seção; geo só se permission granted; fatia em três entregáveis confirmada. Planos ready com Issue #N. --- .../wizard-municipio-sugestoes-chassis.md | 25 +++++++++---------- .../wizard-municipio-sugestoes-continuity.md | 14 +++++------ docs/plans/wizard-municipio-sugestoes-geo.md | 25 ++++++++++--------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/plans/wizard-municipio-sugestoes-chassis.md b/docs/plans/wizard-municipio-sugestoes-chassis.md index 784b76f0..1a8f26ea 100644 --- a/docs/plans/wizard-municipio-sugestoes-chassis.md +++ b/docs/plans/wizard-municipio-sugestoes-chassis.md @@ -1,8 +1,8 @@ # Empty state do wizard — chassis + ranking de esquecidos -Status: rascunho +Status: ready Atualizado em: 2026-08-01 -Issue: — +Issue: #92 Priority: P1 Model: composer-2.5 Impeccable: B — encaixe em `WizardMunicipalitySearchStep` (passo 1 dos wizards `/campanha/acoes/`) @@ -32,13 +32,12 @@ Brief compacto: │ ┌─ 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. + Sem título de seção — mesma região de resultados da busca. + Continuity (B93) e geo (B94) entram depois como linhas prefixadas. ``` ## Dados → decisão → apresentação @@ -61,7 +60,7 @@ Este item é o **chassis + ranking server de esquecidos**. Continuity (visitados ## 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. +- Quando `!query.isActive` no passo de município do wizard: preencher a **mesma região de resultados** com hits sugeridos (sem título de seção — decisão de produto 2026-08-01), 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). @@ -76,14 +75,14 @@ Este item é o **chassis + ranking server de esquecidos**. Continuity (visitados - **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”, …). +- **Lista flat na região de resultados, sem título de seção** (produto 2026-08-01). Reason labels por linha ficam para B93/B94 (neste slice todas as linhas são “esquecidos”). **Rejeitado:** heading “Prováveis”/“Sugestões”; cards; multi-grupo neste appetite. +- **CG = escopo completo frio-primeiro** (não filtrar a `priority === 'alta'`). Prioridade alta continua só como indicador na linha. **Rejeitado:** copiar filtro B68 (política de omnibox, não de ritual). +- **Votos de desempate = nominais 2022 do candidato (artifact).** **Rejeitado:** estimado central (live/cache). +- **i18n / naming:** identificadores `wizardMunicipalitySuggest`, `rankWizardMunicipalitySuggestions`, `loadWizardMunicipalitySuggestions`; copy de empty tipado permanece a de B60. ## 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)_ +- Nenhuma após gate 2026-08-01. ## Abordagem proposta @@ -102,7 +101,7 @@ 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. +- **`WizardMunicipalitySearchStep`**: fetch suggest no idle; cancelar ao digitar; render hits na região de resultados (sem heading); 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. @@ -133,7 +132,7 @@ Componentes: ## Referências -- GitHub Issue — (após register) +- GitHub Issue #92 - [`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) diff --git a/docs/plans/wizard-municipio-sugestoes-continuity.md b/docs/plans/wizard-municipio-sugestoes-continuity.md index 59a39c29..af63e74b 100644 --- a/docs/plans/wizard-municipio-sugestoes-continuity.md +++ b/docs/plans/wizard-municipio-sugestoes-continuity.md @@ -1,8 +1,8 @@ # Empty state do wizard — continuity (visitados + última ação) -Status: rascunho +Status: ready Atualizado em: 2026-08-01 -Issue: — +Issue: #93 Priority: P2 Model: composer-2.5 Impeccable: B — prefixo client no empty de `WizardMunicipalitySearchStep` (após B92) @@ -28,13 +28,12 @@ Brief compacto: ```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. + Sem título de seção. Geo (B94) pode ocupar 1 slot acima. ``` ## Dados → decisão → apresentação @@ -69,12 +68,13 @@ Já existe `teqo:campaign:recent-visits` (`recentVisits.ts`, Quadro). B68 **adio - **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. +- **Sem título de seção** (gate 2026-08-01) — só linhas na região de resultados, com reason na secondary. - **i18n:** `campaignLastActedMunicipality` / `recordLastActedMunicipality` / `listWizardContinuitySlugs`; copy “Última ação”, “Visitado”. +- **Prefixo: 1 last-acted + até 3 visitados**; filtrar fora da carteira. Cap total da lista ~8 com B92/B94. ## 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)_ +- Nenhuma após gate 2026-08-01. ## Abordagem proposta @@ -120,7 +120,7 @@ Componentes: ## Referências -- GitHub Issue — (após register) +- GitHub Issue #93 - [`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) diff --git a/docs/plans/wizard-municipio-sugestoes-geo.md b/docs/plans/wizard-municipio-sugestoes-geo.md index c1ba50f1..66c222a6 100644 --- a/docs/plans/wizard-municipio-sugestoes-geo.md +++ b/docs/plans/wizard-municipio-sugestoes-geo.md @@ -1,8 +1,8 @@ # Empty state do wizard — município mais próximo (geo) -Status: rascunho +Status: ready Atualizado em: 2026-08-01 -Issue: — +Issue: #94 Priority: P2 Model: composer-2.5 Impeccable: B — 1 linha prefixada no empty de `WizardMunicipalitySearchStep` (após B92) @@ -28,13 +28,13 @@ Brief compacto: ```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. + Sem título de seção. Só resolve se permission já granted + (prompt de geo já ocorre ao abrir o Quadro — produto 2026-08-01). + Sem permissão / fora da BA / unsupported: linha some. ``` ## Dados → decisão → apresentação @@ -55,8 +55,8 @@ O pedido (2026-08-01) inclui o mesmo sinal no empty do **wizard**. O passo 1 hoj - 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. +- **Só resolve se a permissão já está `granted`** (gate 2026-08-01): o prompt de geo já ocorre ao abrir (Quadro B14). Wizard **não** dispara `requestCurrentPosition` em `prompt`/`denied`/`unknown` — zero surpresa no ritual. +- Falhas / não-granted / fora da BA / outOfScope sem nearest: **silêncio** — não montar linha de erro nem CTA; usuário digita. - `kind: 'inScope'` → uma hit row com reason; select = auto-avanço. - Sem migration / Consent / POST de coordenadas. @@ -64,14 +64,15 @@ O pedido (2026-08-01) inclui o mesmo sinal no empty do **wizard**. O passo 1 hoj - **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). +- **Geo no wizard = só `granted` (produto 2026-08-01).** Prompt já foi dado ao abrir o app/Quadro; não re-pedir nem CTA no ritual. **Rejeitado:** prompt automático no mount do wizard; CTA “Usar localização” neste item. - **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`. +- **Uma linha, não card; sem título de seção.** **Rejeitado:** embutir `NearestMunicipalityCard`; heading “Prováveis”. +- **Salvador multi-ZE `zoneCity` / ambíguo: omitir a linha** — wizard exige `municipality` atômico. **Rejeitado:** deep-link para lista filtrada (sai do ritual). - **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)_ +- Nenhuma após gate 2026-08-01. ## Abordagem proposta @@ -113,12 +114,12 @@ Componentes: ## 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. +- **CTA “Usar localização” quando permission ≠ granted.** Revisitar se sessão observada pedir botão explícito no wizard (hoje o prompt vive só no Quadro). - **Centroid artifact** só se o lazy da malha pesar no mid-tier Android em campo. ## Referências -- GitHub Issue — (após register) +- GitHub Issue #94 - [`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)