diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index de5ceb0b..eeca2d4f 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- +**Recently resolved (2026-07-31):** **Consolidação de Issues (project-status)** — merge **B68+** (#39) → **O0+** (#35) (mesmo `model: kimi-k3-low`); **não** merge D3/D4/D5 (modelos mistos kimi vs composer); **não** merge B53–B55 (já `done`); fatia **B79** → chassis (#15) + **B91** busca no drawer (#76, `depends: [B79]`). Planos: `escala-dry-pos-onda0.md`, `busca-global-bottom-drawer.md`, `chassis-bottom-drawer-acoes-rapidas.md`. + ## Known Gaps (as of 2026-07-24 — resolve before relying on this file for onboarding new devs) Backlog consolidado (bloqueadores, site, campanha, white-label): [`docs/roadmap.md`](docs/roadmap.md). diff --git a/docs/plans/busca-global-bottom-drawer.md b/docs/plans/busca-global-bottom-drawer.md new file mode 100644 index 00000000..4d0ed4e6 --- /dev/null +++ b/docs/plans/busca-global-bottom-drawer.md @@ -0,0 +1,84 @@ +# Busca global no bottom drawer (mobile) + +Status: rascunho +Atualizado em: 2026-07-31 +Issue: #76 (B91) +Priority: P1 +Model: composer-2.5 +Impeccable: C — chrome do drawer (B79); reuso da busca do Início dentro do snap expandido +Appetite: ~0,75–1 dia eng; extrair acoplamento HomeSearch do Início; sem migration +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` / `DESIGN.md` · tema `data-theme='campaign'` · shell do drawer em [chassis-bottom-drawer-acoes-rapidas.md](chassis-bottom-drawer-acoes-rapidas.md) (B79). + +Na implementação (`work-issue`): craft compacto → critique → polish (superfície já moldada pelo chassis). + +Brief compacto: + +- **Persona / contexto:** staff no celular, drawer expandido em rota ≠ Início. +- **Job principal:** a mesma busca global do Início (`POST /campanha/home-search`, providers B48–B55) sem sair da página. +- **Estratégia de cor:** Restrained — chrome do drawer. +- **Edit where you see:** não — busca/navegação. +- **Anti-goals:** segunda API de busca; command palette desktop; reinventar debounce/contrato; drawer no Início. + +### Wireframe (texto) + +```text +┌─ CampaignQuickActionsDrawer (expanded) ─────────────┐ +│ ═ handle ═ │ +│ [ações B80+ …] │ +│ ┌─ Buscar na campanha ────────────────────────────┐ │ +│ │ input + results (HomeSearch* reusado) │ │ +│ └─────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────┘ + Fora: página opaca/interativa (B79); Início sem este item. +``` + +## Dados → decisão → apresentação + +Dados: N/A — hits = providers B48+ já existentes; este item só monta a UI no drawer. + +## Contexto + +Fatia de **B79** (2026-07-31): o chassis (drawer não-modal + snap + registry + padding) desbloqueia **B80–B90**; a busca embutida era a metade que inchava o guarda-chuva. B79 permanece o dependente duro dos catálogos; este item (B91) depende de B79 e não bloqueia B80–B90. + +## Objetivos + +- Extrair o mínimo de `CampaignHomeSearch` / `HomeSearchProvider` / results shell para montar fora de `CampaignHomeStaffChrome`. +- No drawer expandido: mesma API, debounce e modo focado do Início. +- Guardrails: sem migration, sem collection, sem Consent, sem server action de escrita. + +## Decisões travadas + +- **Busca = contrato do Início** (`home-search`). **Rejeitado:** segunda API; omnibox desktop neste item. +- **Depende de B79 (chassis).** Sem drawer montado, não há onde embutir. **Rejeitado:** busca flutuante fora do drawer. +- **Não bloqueia B80–B90.** Catálogos usam o slot de ações do chassis; busca é independente. + +## Abordagem proposta + +- Depth check: reusar `HomeSearch*` — extrair provider montável no drawer; não duplicar debounce. +- Wire no `CampaignQuickActionsDrawer` (região expandida). +- Pins: unit/int existentes da home-search; smoke mobile do drawer com query. + +## Dependências + +- Dura: **B79** (chassis + mount). Soft: B47 ✓ / B48–B55 (providers). + +## Não escopo + +- Catálogos por vertical → B80–B90. +- Chassis / snap / padding → B79. +- Desktop omnibox. + +## Rabbit holes + +- **Teclado virtual cobrindo a busca no snap.** Mitigação: v1 = padding fixo; Adiado com gatilho iOS. +- **Levantar strip do Início sem medir bundle.** Mitigação: só a busca neste item. + +## Referências + +- [chassis-bottom-drawer-acoes-rapidas.md](chassis-bottom-drawer-acoes-rapidas.md) (B79) +- `src/components/campaign/dashboard/HomeSearchResultsContext.tsx` +- `src/lib/campaignHomeActions.ts` + `CampaignHomeSearch` diff --git a/docs/plans/chassis-bottom-drawer-acoes-rapidas.md b/docs/plans/chassis-bottom-drawer-acoes-rapidas.md index 54a28083..a45e5b75 100644 --- a/docs/plans/chassis-bottom-drawer-acoes-rapidas.md +++ b/docs/plans/chassis-bottom-drawer-acoes-rapidas.md @@ -1,10 +1,13 @@ # Chassis do bottom drawer de ações rápidas + busca (mobile) -Status: rascunho -Atualizado em: 2026-07-30 +Status: in-progress (Issue #15) — **fatiado 2026-07-31:** busca embutida → **B91** +Atualizado em: 2026-07-31 +Issue: #15 (B79) +Priority: P1 +Model: composer-2.5 Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item **B79** — UX-1 chrome pós-B73) -Impeccable: C — chrome novo em toda rota `(app)` exceto Início e wizards; gesto drag + busca embutida -Appetite: ~1,5–2 dias eng; shell não-modal + snap/drag + slot de ações + reuso da busca global; sem migration +Impeccable: C — chrome novo em toda rota `(app)` exceto Início e wizards; gesto drag + slot de ações +Appetite: ~1–1,5 dia eng; shell não-modal + snap/drag + registry/slot de ações + padding; **sem** busca (B91); sem migration Responsável: — ## Design (Impeccable) @@ -16,10 +19,10 @@ Na implementação (`implement-roadmap-item`): shape → craft → critique → Brief compacto: - **Persona / contexto:** CG/assessor no celular, **fora** do Início — após **B73** a bottom nav some; o ritual de ação+busca só existia no Início; nas outras páginas sobra só o sidebar fechado. -- **Job principal:** ter ações rápidas do contexto da página + a mesma busca global do Início, sem escurecer nem bloquear a página atrás; poder recolher/expandir o drawer para ver mais conteúdo. +- **Job principal:** ter ações rápidas do contexto da página sem escurecer nem bloquear a página atrás; poder recolher/expandir o drawer para ver mais conteúdo. (Busca global no drawer = **B91**.) - **Estratégia de cor:** Restrained — superfície de chrome (fundo campaign), não modal dramático. - **Edit where you see:** não neste item — launchers; escrita nos wizards/listas. -- **Anti-goals:** overlay que muda opacidade/`pointer-events` da página; segunda bottom nav de destinos de domínio; FAB flutuante; drawer no Início (já tem dock B65); drawer em `/campanha/acoes/*` (ritual B59/B75); reinventar busca (reusar B47+). +- **Anti-goals:** overlay que muda opacidade/`pointer-events` da página; segunda bottom nav de destinos de domínio; FAB flutuante; drawer no Início (já tem dock B65); drawer em `/campanha/acoes/*` (ritual B59/B75); embutir busca neste item (→ **B91**). ## Dados → decisão → apresentação @@ -30,8 +33,8 @@ Dados: N/A — chrome de navegação/ação; hits da busca = providers B48+ já Decisão de produto (2026-07-30): com a remoção da bottom nav (**B73**), a navegação mobile frequente fica no Início (ações + busca) e no sidebar (Sheet fechado por padrão, B38 ✓). Nas **demais** páginas falta um atalho de intenção. Solução: **bottom drawer** sempre montado no viewport <`md` (desktop: fora — sidebar + chrome amplo bastam _(assumido)_), com: 1. Strip/catálogo de ações **contextualizadas** pela página atual (catálogos = **B80–B90**). -2. Barra de busca com o **mesmo escopo global** do Início (`POST /campanha/home-search`, providers B48–B55). -3. Gesto: arrastar para cima/baixo para expandir/recolher; tap na borda/handle superior também alterna. +2. Gesto: arrastar para cima/baixo para expandir/recolher; tap na borda/handle superior também alterna. +3. **Busca no drawer** ficou em **B91** (fatia 2026-07-31) — não bloqueia B80–B90. O `Drawer` shadcn/base-ui já expõe `modal={false}` (não renderiza `DrawerOverlay`) e `snapPoints` — profundidade certa; não inventar segundo sheet. @@ -39,11 +42,11 @@ O `Drawer` shadcn/base-ui já expõe `modal={false}` (não renderiza `DrawerOver - Componente shell (ex. `CampaignQuickActionsDrawer`) montado no layout `(app)` **só** quando: viewport mobile, actor staff (ou leader nas rotas que o plano B89 pedir), pathname **não** é Início exato e **não** está sob `/campanha/acoes`. - **Não-modal:** página atrás permanece 100% opaca, scrollável e interativa; o drawer **ocupa** faixa inferior (layout shift / padding do scroll) em vez de cobrir com scrim. -- Snap points mínimos: **collapsed** (handle + talvez 1 linha de peek) e **expanded** (ações + busca); drag e tap no handle/borda superior alternam. Persistência de snap na sessão _(recomendado: `sessionStorage`)_ — Adiado se estourar appetite. -- Slot `actions` (lista já resolvida com `href`s) + slot/região de busca reusando `CampaignHomeSearch` / `HomeSearchContext` / results shell — extrair o necessário para não acoplar ao `CampaignHomeLayout`. -- Contrato de contexto client-safe (ex. `CampaignQuickActionContext`: `municipalitySlug?`, `leadershipId?`, …) + helper que resolve `href` via `wizardActionHref` / atalhos de lista — **B80+** só preenchem o contexto e o catálogo. +- Snap points mínimos: **collapsed** (handle + talvez 1 linha de peek) e **expanded** (ações; região de busca reservada/vazia até B91); drag e tap no handle/borda superior alternam. Persistência de snap na sessão _(recomendado: `sessionStorage`)_ — Adiado se estourar appetite. +- Slot `actions` (lista já resolvida com `href`s). Contrato de contexto client-safe (ex. `CampaignQuickActionContext`: `municipalitySlug?`, `leadershipId?`, …) + helper que resolve `href` via `wizardActionHref` / atalhos de lista — **B80+** só preenchem o contexto e o catálogo. - Ajuste de padding do `campaign-content-scroll` para a altura do peek (substitui o antigo `pb-24` da bottom nav após B73). -- Guardrails: sem migration, sem collection, sem Consent, sem server action nova de escrita; leader lockdown respeitado (sem omnibox estadual no drawer staff). +- Guardrails: sem migration, sem collection, sem Consent, sem server action nova de escrita; leader lockdown respeitado. +- **Tracer bullet:** drawer montado + registry vazio/stub + padding → merge — isso **desbloqueia B80–B90** sem esperar B91. ## Decisões travadas @@ -51,8 +54,8 @@ O `Drawer` shadcn/base-ui já expõe `modal={false}` (não renderiza `DrawerOver - **Fora do Início e fora de `/acoes/*`.** Início já tem dock; wizard tem chrome próprio (B75). **Rejeitado:** unificar Início no mesmo drawer (dois docks); drawer dentro do wizard. - **Só mobile (<`md`).** Desktop/tablet mantêm sidebar + busca do Início quando o usuário volta. **Rejeitado:** drawer em `md+` (ruído; compete com sidebar). - **Catálogo por página = itens B80–B90**, não hardcode no chassis. O chassis só renderiza o que a rota fornece (via registry pathname→provider ou prop do layout segment). **Rejeitado:** um único catálogo global idêntico ao Início em toda rota (perde o prefill). -- **Busca = mesmo contrato do Início** (`home-search`, debounce, modo focado **dentro do drawer expandido**). **Rejeitado:** segunda API; command palette desktop neste item. -- **i18n:** ids `CampaignQuickActionsDrawer`, `quickActionContext`, `snap`; copy pt-BR (“Buscar na campanha”, aria do handle “Mostrar ações rápidas” / “Ocultar…”). +- **Busca neste item = fora.** Contrato `home-search` no drawer → **B91**. **Rejeitado:** manter busca no escopo do chassis (guarda-chuva que bloqueava 11 P1). +- **i18n:** ids `CampaignQuickActionsDrawer`, `quickActionContext`, `snap`; copy pt-BR (aria do handle “Mostrar ações rápidas” / “Ocultar…”). ## Questões em aberto @@ -69,27 +72,27 @@ flowchart TB gate -->|não| drawer["CampaignQuickActionsDrawer modal=false"] drawer --> handle["handle + snap"] drawer --> actions["ações do registry B80+"] - drawer --> search["HomeSearch* reusado"] + drawer -.->|"B91"| search["HomeSearch* no drawer"] actions --> wizards["wizardActionHref + context"] - search --> api["POST /campanha/home-search"] ``` Componentes: -- **`CampaignQuickActionsDrawer`** (`src/components/campaign/shell/`): `Drawer` `modal={false}` + `snapPoints` + `showSwipeHandle`; região de ações (reuso visual de `CampaignHomeActionStrip` / botões B44 se couberem no peek) + busca. +- **`CampaignQuickActionsDrawer`** (`src/components/campaign/shell/`): `Drawer` `modal={false}` + `snapPoints` + `showSwipeHandle`; região de ações (reuso visual de `CampaignHomeActionStrip` / botões B44 se couberem no peek); slot/região de busca **opcional** (preenchida em B91). - **`campaignQuickActionRegistry` / `resolveQuickActionsForPath`** (`src/lib/` client-safe): pathname + role + context → `ResolvedCampaignHomeAction[]` (ou tipo irmão). Contextos ricos entram pelos planos B80+. -- **Extrair busca do acoplamento ao Início** o mínimo necessário (`HomeSearchProvider` montável fora de `CampaignHomeStaffChrome`) — depth check: não duplicar debounce/contrato. - **`(app)/layout.tsx`**: mount condicional; padding inferior do scroll = altura do peek. - **Migration:** Sem migration, sem collection, sem server action de escrita. ## Dependências - Dura: **B73** (remover bottom nav — senão dois chromes inferiores). Soft: **B47 ✓**/B48+ (busca); **B45 ✓**/B44 ✓ (strip/botão); **B60 ✓** (`wizardActionHref` + `?municipio=`). -- Desbloqueia **B80–B90**. +- Desbloqueia **B80–B90** (não exige B91). +- Soft: **B91** (busca no drawer) depende deste chassis. ## Não escopo - Catálogos e prefills por vertical → **B80–B90**. +- Busca global embutida no drawer → **B91** ([busca-global-bottom-drawer.md](busca-global-bottom-drawer.md)). - Novos wizards / novas actions de escrita. - Desktop omnibox global. - Quadro: **fora do inventário** (decisão de produto 2026-07-30). @@ -108,9 +111,14 @@ Componentes: ## Referências - `docs/roadmap.md` (UX-1, B73, B79–B90) +- [busca-global-bottom-drawer.md](busca-global-bottom-drawer.md) (B91 — fatia busca) - `src/components/ui/Drawer.tsx` — `modal={false}` sem overlay - `src/lib/campaignActionRoutes.ts` — `wizardActionHref` -- `src/lib/campaignHomeActions.ts` + `CampaignHomeActionStrip` / `CampaignHomeSearch` +- `src/lib/campaignHomeActions.ts` + `CampaignHomeActionStrip` - `src/app/(campaign)/campanha/(app)/layout.tsx` — scroll + chrome - [remover-bottom-nav-mobile.md](remover-bottom-nav-mobile.md) · [fluxos-acao-primeiro-inicio.md](fluxos-acao-primeiro-inicio.md) - `PRODUCT.md` / `DESIGN.md` — Field Desk mobile + +## Revisões + +- **2026-07-31:** Fatia humana — busca → B91; chassis desbloqueia B80–B90 sozinho; tracer bullet = drawer + registry + padding. diff --git a/docs/plans/escala-dry-pos-b68.md b/docs/plans/escala-dry-pos-b68.md index 86a3320d..c0c72b0b 100644 --- a/docs/plans/escala-dry-pos-b68.md +++ b/docs/plans/escala-dry-pos-b68.md @@ -1,58 +1,19 @@ # Escala e DRY pós-B68 (sugestões na busca aberta) -Status: rascunho (débito pós-`/simplify` 2026-07-29) -Atualizado em: 2026-07-29 -Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins abertos, **B68+**) -Impeccable: A — perf/estrutura de loader; sem pixel novo -Appetite: ~0,5–0,75 dia eng (F1); F2 cortável -Responsável: — +Status: **absorvido em O0+ (#35)** — 2026-07-31 +Atualizado em: 2026-07-31 +Issue: #39 (fechada) → fases vivas em [escala-dry-pos-onda0.md](escala-dry-pos-onda0.md) (§ Absorvido de B68+) +Priority: P2 +Model: kimi-k3-low -## Dados → decisão → apresentação +## Motivo da absorção -- **Vou apresentar dados?** Não (N/A) — mesmos ~8 hits já mostrados; só muda de onde vêm no primeiro focus. +Consolidação humana (project-status 2026-07-31): mesmo domínio/prio heurístico (escala dry/P2) e **mesmo `model: kimi-k3-low`**. A regra de merge era: só consolidar se a complexidade de modelo não subir — aqui permanece Kimi K3 (simplify/DRY). -## Contexto +Não reabrir este plano; trabalhar as fases B68-F1/F2 no plano consolidado O0+. -**B68 ✓ (2026-07-29)** entrega sugestões de municípios no empty state da busca do Início (`mode: 'suggest'` em `POST /campanha/home-search`). O `/simplify` do fechamento mediu que, para **coordenador/candidato**, cada focus dispara um POST que repete `loadMunicipalityScope` + `loadMunicipalityGoalCoverageBundle` — trabalho que a mesma página já pagou em `loadCampaignHomeSummary` no RSC. `React cache()` não cruza a fronteira RSC ↔ route handler. +## Referência histórica (pré-merge) -O simplify da sessão já aplicou mitigação parcial: o bundle E8 roda só em municípios `priority === 'alta'` (não nas 435 linhas) e `toHomeSearchMunicipalityHit` unificou o shaping dos hits. +O `/simplify` de B68 mediu que, no Início staff (coordenador/candidato), cada focus do empty-state dispara POST que repete `loadMunicipalityScope` + `loadMunicipalityGoalCoverageBundle` já pagos no RSC. Mitigação: embed `initialSuggest` no mesmo request da página. -## Objetivos - -- **F1:** primeiro focus no Início staff **não** refaz scope+coverage no POST quando o RSC da página já carregou os insumos — embed `initialSuggest` (ou loader irmão no mesmo `Promise.all` da página) e hidratar `useHomeSearchResultsState` com TTL curto / invalidação ao navegar. -- Sem mudar a política de ranking (assessor = frescor; CG = déficit + frescor). -- Sem migration/collection/Consent. - -## Já resolvido no simplify/critique (não reabrir) - -- `aria-busy` duplicado (`resultsBusy ?? isDebouncing`). -- Shell de erro sem `errorMessage` nullable intermediário. -- `id` removido do tipo puro do ranker. -- Factory `toHomeSearchMunicipalityHit` nos dois loaders. -- Bundle de cobertura restrito a `priority === 'alta'`. - -## Fases - -### F1 — RSC embed do suggest (P1 perf) - -- Estender `loadCampaignHomeSummary` ou extrair `loadHomeSearchSuggestHits` reutilizável no mesmo request da página (`page.tsx`). -- Passar `initialSuggest` para `CampaignHomeStaffChrome` → `useHomeSearchResultsState({ initialSuggest })`. -- Client: usar payload inicial quando `uiFocused && !query.isActive` e não houver stale/error; POST só para revalidação explícita ou após TTL (opcional v1: só primeiro paint). - -### F2 — Comparator compartilhado de déficit (cortável) - -- Extrair `compareNullableNumberDesc` + tiebreak hook para `lib/municipalitySort.ts` quando houver 3º call site com política diferente de tiebreak (hoje: B20 nome, B68 frescor, E9 lista). - -## Explicitamente fora / Adiado com gatilho - -- **Cache client-side / stale-while-revalidate no refocus** — aceito no plano B68 (fetch ao focus); revisitar se sessão reclamar de flash de loading. -- **Unificar constante `8`** (`HOME_SEARCH_SUGGEST_LIMIT` vs `DASHBOARD_PRIORITY_SAMPLE_LIMIT`) — cosmético; mover para `lib/` só se um dos dois mudar. -- **`loadTerritoryOverview` em todo keystroke da busca** — débito adjacente B48 (search path), não B68; gatilho: latência perceptível ao digitar após B49+. -- **Mesclar `loadHomeSearchSuggestions` com `searchHomeMunicipalities`** — dois call sites; extrair só o mapper (já feito). - -## Referências - -- [sugestoes-busca-vazia-inicio.md](sugestoes-busca-vazia-inicio.md) (B68) -- [`loadHomeSearchSuggestions.ts`](../src/utilities/homeSearch/loadHomeSearchSuggestions.ts) -- [`campaignDashboardData.ts`](../src/utilities/campaignDashboardData.ts) (`loadCampaignHomeSummary`) -- [`HomeSearchResultsContext.tsx`](../src/components/campaign/dashboard/HomeSearchResultsContext.tsx) +Detalhe completo das fases e rabbit holes: ver § Absorvido de B68+ em [escala-dry-pos-onda0.md](escala-dry-pos-onda0.md). diff --git a/docs/plans/escala-dry-pos-onda0.md b/docs/plans/escala-dry-pos-onda0.md index 0717f1ef..235668fb 100644 --- a/docs/plans/escala-dry-pos-onda0.md +++ b/docs/plans/escala-dry-pos-onda0.md @@ -1,8 +1,13 @@ -# Escala e DRY pós-Onda 0 (Consent / privacidade / cache) +# Escala e DRY P2 — pós-Onda 0 + pós-B68 (consolidado) -Status: Fase 1 entregue; fases 2–5 pendentes -Atualizado em: 2026-07-19 -Item do roadmap: [docs/roadmap.md](../roadmap.md) (Onda 0+, fill-in de engenharia pós-MVP) +Status: Fase 1 (Onda 0 revalidate) entregue; fases 2–5 O0 + F1–F2 B68 pendentes +Atualizado em: 2026-07-31 +Issue: #35 (O0+) — absorveu **B68+** (#39) em 2026-07-31 (mesmo `model: kimi-k3-low`) +Priority: P2 +Model: kimi-k3-low +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Onda 0+ + Fill-in B68+) +Impeccable: A — perf/estrutura + DRY Consent/cache; sem pixel novo +Appetite: ~1–1,5 dia eng (fases O0 restantes + F1 B68; F2 B68 cortável) Responsável: — ## Contexto @@ -116,6 +121,24 @@ flowchart TD - `tests/int/onda0Provision.int.spec.ts` / `campaignMigrationReconciliation.int.spec.ts` - AGENTS.md — Consent por chave estável, `revalidateGlobal`, naming +## Absorvido de B68+ (2026-07-31) + +Plano original: [escala-dry-pos-b68.md](escala-dry-pos-b68.md) (Issue #39 fechada como absorvida). Mesma classe de tarefa (simplify/escala-dry → `kimi-k3-low`); domínio distinto (busca do Início) mas consolidado sob O0+ por decisão humana 2026-07-31. + +### Fase B68-F1 — RSC embed do suggest (P1 perf) + +- Estender `loadCampaignHomeSummary` ou extrair `loadHomeSearchSuggestHits` reutilizável no mesmo request da página (`page.tsx`). +- Passar `initialSuggest` para `CampaignHomeStaffChrome` → `useHomeSearchResultsState({ initialSuggest })`. +- Client: usar payload inicial quando `uiFocused && !query.isActive` e não houver stale/error; POST só para revalidação explícita ou após TTL (opcional v1: só primeiro paint). +- Sem mudar política de ranking (assessor = frescor; CG = déficit + frescor). Sem migration/collection/Consent. + +### Fase B68-F2 — Comparator compartilhado de déficit (cortável) + +- Extrair `compareNullableNumberDesc` + tiebreak hook para `lib/municipalitySort.ts` quando houver 3º call site com política diferente de tiebreak (hoje: B20 nome, B68 frescor, E9 lista). + +**Já resolvido no simplify B68 (não reabrir):** `aria-busy` duplicado; shell de erro; `id` no ranker; factory `toHomeSearchMunicipalityHit`; bundle E8 só em `priority === 'alta'`. + ## Revisões +- **2026-07-31:** Consolidação com B68+ (#39) — mesmo modelo `kimi-k3-low`; fases B68-F1/F2 absorvidas neste plano; Issue #39 fechada. - **2026-07-19:** Fase 1 entregue — endpoint allowlisted (`posts`, `global_privacy-policy`), `getGlobalCacheTag` exportado, curl no runbook Onda 0 e lembrete no `seed:consent`.