Replace all project-local .cursor/ path references with .agents/ across rules, skills, environment config, hooks, and ignore files. Keep Cursor editor paths (~/.cursor/, window.cursor, CURSOR_API_KEY) and vendored Impeccable editor-hook paths unchanged.
32 KiB
Escala e DRY pós-B8 F2 (polígonos das ZE de Salvador)
Status: entregue 2026-07-27 (as quatro fases; ver "Como foi entregue")
Atualizado em: 2026-07-27
Item do roadmap: docs/roadmap.md (B8+ ✓, fill-in de engenharia)
Impeccable: A — N/A (nenhuma superfície nova; F3 muda um parágrafo de copy e um estado de falha, o resto preserva o comportamento visível do B8 F2)
Appetite: ~0,75–1 dia eng; quatro fases independentes, nenhuma com migration, collection ou Consent; só F1 reescreve o artefato commitado
Responsável: —
Como foi entregue (as-built, 2026-07-27)
As quatro fases entraram numa sessão, F1 primeiro. Duas correções ao plano vieram de medição, não de preferência, e estão registradas abaixo porque mudam o que a próxima pessoa deve acreditar.
F1 — a alavanca era o peso absoluto, não os dígitos
O plano tratava o alvo como resolução de chão implementada via dígitos de quantização. Medindo o re-snap do artefato commitado para grades mais grossas, quantização sozinha quase não corta: ~12 m → 8.990 pontos (99% do original), ~25 m → 8.164 (90%), ~50 m → 6.219 (68%), e mesmo na grade da malha estadual (~103 m) → 3.849 pontos, ainda 203/feature — ~9× a densidade do irmão. A alavanca dominante é o limiar absoluto de Visvalingam, que o plano mencionava entre parênteses. A política inverteu a ordem: o peso absoluto (em m² de chão) é a política; os dígitos são derivados do bbox de cada topologia por consistência.
scripts/lib/topology.mjs (primeiro módulo de scripts/lib/ — B5 F2 deve pousar nele, não abrir um segundo) declara SIMPLIFY_TOLERANCE_M2 por malha, GROUND_GRID_METRES = 102.82, simplifyToGroundScale, quantizeToGroundGrid e describeGroundScale. SIMPLIFY_QUANTILE e QUANTIZE_DIGITS saíram dos dois scripts.
| artefato | antes | depois |
|---|---|---|
bahia-municipalities |
132.162 B · 9.139 pts · 21,9/ft | byte-idêntico (política semeada nele) |
bahia-identity-territories |
14.862 B · 1.210 pts · 44,8/ft | 14.861 B · 1.210 pts · 44,8/ft (−1 B) |
bahia-municipality-zones |
72.310 B · 9.120 pts · 480/ft | 7.687 B · 574 pts · 30,2/ft (−89% bytes) |
Três coisas que a implementação aprendeu e deixou no comentário do módulo, para ninguém re-derivar:
- Tolerâncias são por malha de propósito, agora lado a lado em unidades de chão. A tolerância municipal (1,78 km²) até chega a 17,9 pontos/feature nas zonas, mas come 5% da área de Salvador (306 → 291 km²) — e a soma de áreas é justamente a asserção que prova que nenhum bairro foi descartado nem contado duas vezes. As zonas ficaram em 0,06 km²; os territórios em 13,6 km² (são dissolve da malha municipal, então já vêm generalizados).
- A ordem importa: quantizar antes de simplificar colapsa vértices em duplicatas e o
mergeposterior produz anéis quebrados — medido: Salvador dissolveu para 24 km² em vez de 307. - O 1 byte dos territórios é a grade indo de 102,730 m para 102,822 m: os dígitos passaram a ser derivados do bbox para a grade pedida em vez de um
1e4fixo.
A área das zonas saiu em 306,3 km² (era ~324), dentro da janela de 280–360 km² que o int já asseverava — medida, não presumida, como o plano exigia. O tripwire de bytes ficou e ganhou ao lado MAX_POINTS_PER_FEATURE = 60, a grandeza que a política controla. zoneNumber saiu do artefato e do tipo.
F2 — o invariante valia; caiu o teste sintético
featureMapKey agora roda sobre features reais das três malhas commitadas, cada uma no laço que já as varre por inteiro: 417 municípios respondem codarea, 27 TIs respondem code, 19 zonas respondem slug mesmo carregando o ibgeCode da cidade. A primeira escrita asseria a cidade dentro do laço das zonas, e o /simplify pegou o furo: as 19 entradas compartilham o código de Salvador, então aquilo era uma feature municipal asserida 19 vezes enquanto 416 municípios e os 27 territórios nunca passavam por featureMapKey. O caso de precedência pinado no unit usava a forma { municipalitySlug, codarea }, que nenhuma malha produz: caiu, e o comentário passou a declarar o invariante real (as três propriedades de chave são mutuamente exclusivas; ibgeCode na malha de zonas deliberadamente não é chave, porque as 19 o compartilham). Sem resolvedor único, como travado.
F3 — degrada e admite a aproximação em todo modo
loadLayerFeatures faz .catch() na metade das zonas e segue com a malha municipal. A precisão que o plano deixava otimista: como o bundle é keyed por slug, Salvador não volta a pintar agregada — volta como polígono sem dado e sem interação, porque não está em scopedKeys. A memoização da rejeição fica (é o que dá ao B14 um estado de falha acionável) e status === 'error' segue reservado à falha da malha municipal. A ressalva de aproximação saiu do gate !comparisonActive e passou a ser um <p> condicionado a haver ZE no escopo. A primeira escrita derivava isso no cliente com um isZoneMapKey (um município é keyed por 7 dígitos; uma zona, pelo slug) e o /simplify a rejeitou: era uma segunda codificação da regra de mapKeyForMunicipality, agora em espaço negativo (qualquer chave sem 7 dígitos responde "zona"), e o servidor já conhece a resposta por kind. O gate é bundle.hasZoneMunicipalities, campo próprio do contrato preenchido no mesmo passe que monta zoneBreakdown — e não zoneBreakdown.length > 0, que recriaria justamente o acoplamento que o fill-in remover-lista-zonas-mapa-inicio.md vai deletar; assim ele só apaga o <ul> + o campo do bundle.
F4 — deletado, colapsado e um item rejeitado
Saíram getTerritoryFeature e getMunicipalityZoneFeature (os int specs montam o índice em uma linha). MunicipalityMapNavigation virou string | undefined e MunicipalitiesByMapKey virou Record<string, string> — com o tipo, morreram o helper, o useMemo que só existia porque o helper alocava por chamada, o | null do MapFeatureReadout e 435 strings no payload RSC de toda visita ao Início. polygonRingsOf foi exportado (3º call site) e os helpers de geometria passaram a declarar o requisito real com PolygonalFeature, mais BahiaFeature<P> para as quatro grafias de Feature<Polygon | MultiPolygon, P> e BahiaMeshFeature para o que a camada do mapa recebe. As limpezas do script entraram (chave por alias num Map acima do passe 1, polygonCount sem branches inalcançáveis, [await shp(buffer)].flat(), o url que ninguém desestrutura, push em vez de copiar o array).
Rejeitado por medição, contra o plano: deletar city/zoneNumber de MunicipalityZoneNeighborhoodEntry. É verdade que nenhum runtime os lê, mas os dois entram nas linhas hasheadas por canonicalEvidenceRows e são colunas do fixture de proveniência da RA 02/2017 — uma transcrição independente da resolução. Deletar enfraqueceria a verificação ou forçaria regerar um fixture de proveniência por ganho zero. Ficaram, e com eles o filtro entry.city === 'Salvador' no int. Só o zoneNumber do artefato TopoJSON saiu (F1 reescreveu o arquivo).
O que continua verdade
Sem migration, collection, server action ou Consent. Contrato de URL, escalas e catálogo TRE inalterados. O e2e segue em 436 paths / 435 interativos (o não-interativo é a base municipal de Salvador). Os dois débitos de produto do B8 F2 (rank por ZE, ZE exata no "Onde estou") continuam abertos com gatilho no plano do B8, e a decisão sobre o modo territory morto do loadLayerFeatures continua pertencendo ao E12.
Dados → decisão → apresentação
- Vou apresentar dados? Não (N/A) — o lote é custo, coerência e honestidade de dados já entregues pelo B8 F2. F3 muda um parágrafo (a ressalva de aproximação, que hoje desaparece no modo comparação enquanto a malha aproximada continua pintada), não a métrica nem a forma.
- Anti-goals de dado: nenhuma escala nova, nenhuma métrica nova, nenhum rank por ZE (esse tem gatilho próprio no plano do B8). Se uma fase começar a propor métrica, ela virou outro item.
Contexto
B8 F2 (poligonos-pracas-zona.md, entregue 2026-07-26) deu polígono próprio às 19 ZE de Salvador e re-keyed o coroplético por map key (municipalitySlug na zona, ibgeCode no resto). O /simplify da entrega (três revisores em paralelo — quality / performance / reuse) devolveu relatório depois do merge; o cleanup barato entrou antes (id no re-index do script, comentário obsoleto, asserções tautológicas), e os achados maiores que cleanup estão aqui.
O lote abre com custo porque o achado de performance é o único medido e já commitado: o artefato novo tem quase exatamente a mesma contagem de vértices que a malha de todos os 417 municípios da Bahia, gasta em 3,8% da largura do mapa. Verificado nos artefatos deste repo, não estimado:
| artefato | features | pontos de arco | pontos/feature | bbox | passo da grade |
|---|---|---|---|---|---|
bahia-municipalities |
417 | 9.139 | 21,9 | 9,236° × 9,804° | ~103 m × 108 m |
bahia-identity-territories |
27 | 1.210 | 44,8 | 9,227° × 9,804° | ~103 m × 108 m |
bahia-municipality-zones |
19 | 9.120 | 480,0 | 0,353° × 0,275° | ~3,9 m × 3,0 m |
A comparação que fecha o caso: Salvador é um Polygon de 35 pontos na malha municipal e 9.120 pontos na malha de zonas. A causa não é a malha de bairros ser detalhada — é que QUANTIZE_DIGITS = 1e4 e SIMPLIFY_QUANTILE = 0.35 foram copiados verbatim do script irmão, e os dois são relativos à própria topologia: a quantização é uma grade sobre o bbox do arquivo (o de Salvador é ~26× mais estreito que o da Bahia, daí 30× mais resolução no chão) e o quantil de topojson-simplify é um percentil dos pesos de triângulo daquele arquivo (manter 65% num mosaico de bairros corta um limiar de área absoluta muito menor que num mosaico municipal estadual). O detalhe só se torna visível por volta do zoom 15; fitMapToHighlights para em maxZoom: 10.
Já resolvido no simplify (não reabrir): o re-index das geometrias simplificadas passou de properties.name para um id único (um nome repetido em malha futura colapsaria dois polígonos em silêncio dentro de um artefato commitado — o irmão usa codarea, único por construção), com rebuild byte-idêntico como prova; o comentário de zoneCity que afirmava não existirem polígonos de zona; e as duas asserções que a hidratação da Fase 0 tornou tautológicas (comparavam city/zoneNumber com o catálogo de onde foram copiados). Os revisores também confirmaram que o commit acertou três coisas que não devem regredir: fitMapToHighlights ficou mais rápido que antes (lê o _bounds já cacheado dos paths desenhados em vez de construir um L.geoJSON descartável sobre os 417 features), o desenho do B6 sobreviveu intacto (efeito de geometria só em [mode], restyleFeature O(2), featureMapKey roda 2× por feature na construção e nunca por frame), e o re-keying deixou o servidor mais barato (computeMunicipalityTerritorialClass é memoizado, computeAggregateTerritorialClass não era e re-somava as 19 baselines de Salvador por request).
Objetivos
- As duas malhas commitadas da Bahia são simplificadas na mesma escala de chão, e um teste falha se uma delas voltar a ser cortada na sua própria escala relativa.
- A regra da chave do mapa é exercida ponta a ponta pelo menos uma vez fora do e2e: artefato →
featureMapKey→mapKeyForMunicipality. - Uma falha de chunk nas 19 zonas de Salvador não derruba o mapa inteiro da sessão.
- Nenhum número/rótulo aproximado aparece sem a ressalva, em nenhum modo do painel.
- O que a entrega orfanou está deletado (regra "dead code dies immediately" do
engineering-standards.mdc), inclusive o que o knip não vê. - Guardrails: sem migration, sem collection, sem
Consent, sem server action; contrato de URL, escalas e o catálogo TRE inalterados;overrideAccess: falsepreservado em toda leitura comuser.
Decisões travadas
- F1 calibra por resolução de chão, e é isso que fica compartilhado entre os dois scripts — não a contagem de dígitos. O revisor de reuse pediu extrair
simplifyTopology+ as duas constantes (hoje byte-idênticas nos dois scripts) para um módulo comum; a medição acima mostra que compartilhar os dígitos é exatamente o defeito, porquequantize(1e4)significa 103 m na Bahia e 3,9 m em Salvador. O que merece nome é a política — "as duas malhas commitadas da Bahia são cortadas na mesma escala de chão" —, então o módulo compartilhado expõe um alvo em metros (ou umminWeightabsoluto em graus²) e cada script deriva seus dígitos do próprio bbox. Fonte:/simplifyB8 F2 (2026-07-26), medido nos artefatos. Rejeitado: extrair as constantes como estão (cimenta o bug e dá a ele a autoridade de uma política); baixar só oQUANTIZE_DIGITSdas zonas e deixar os dois scripts divergindo em silêncio (a próxima pessoa "conserta" de volta para o valor do irmão por simetria); manter como está porque "só aparece no zoom 15" (o custo não é visual, é vértice reprojetado a cadazoomend/pan e +19 KB gzip no chunk do mapa). - F1 troca o tripwire de bytes por um de densidade.
MAX_TOPO_BYTES = 600 * 1024foi calibrado para pegar uma malha estadual sem simplificação; uma malha de 19 features a 3,8 KB/feature passa folgada. O guard que teria pego isso é pontos-por-feature (ou bytes-por-feature), que é a grandeza que a política de F1 controla. Rejeitado: apertar o teto de bytes (um número global não distingue "19 features densas" de "435 features magras"); confiar na revisão humana (foi ela que deixou passar). - F2 pina o contrato com duas asserções no laço que já existe, não com uma abstração. A regra da chave está escrita em três formas —
mapKeyForMunicipality(registro do catálogo),featureMapKey/MAP_KEY_PROPERTIES(saco de propriedades GeoJSON) e obaseKeys = zones.map(z => z.properties.ibgeCode)doBahiaMap(a identidade entre as duas). As três precisam concordar ou o coroplético pinta cinza sem erro em lugar nenhum. Cada ponta já está pinada contra omunicipalityCatalog, mas nada rodafeatureMapKeysobre um feature real: o unit novo alimenta literais escritos à mão, então o artefato pode deixar de casar com o literal e os dois testes continuam verdes. Só o e2e fecha o laço, e ele pede dev server + banco. Rejeitado: unificar as três num helper (o server lê Payload/catálogo e o cliente lê GeoJSON — um helper que aceite os dois é um wrapper raso, reprovado pelo depth check); confiar no e2e (a suíte mais lenta e mais flaky do repo como única rede de um modo de falha silencioso). - F3 degrada, não esconde.
loadLayerFeaturesexige as duas malhas numPromise.all, eloadMunicipalityZoneGeometryModulememoiza a rejeição (comportamento deliberado, documentado no B14). Somados, um ChunkLoadError nos 19 polígonos de Salvador mata o mapa inteiro pelo resto da sessão — antes do B8 F2 a malha municipal sozinha bastava. Isto é um ponto único de falha novo, introduzido pela entrega. Rejeitado: remover a memoização da rejeição (é ela que dá ao B14 um estado de falha acionável em vez de um spinner eterno); deixar como está porque chunk raramente falha (a regressão é de disponibilidade e o custo do.catché três linhas). - A ressalva de aproximação pertence à malha, não à lista. O parágrafo "é aproximado, não é o limite oficial do TSE" estava dentro do gate
bundle.zoneBreakdown.length > 0 && !comparisonActive, mas o mapa divergente da comparação pinta Salvador zona por zona a partir da mesma malha aproximada. A ressalva foi pendurada na condição de render da lista em vez de na presença da malha na tela — e o rabbit hole que o próprio plano do B8 nomeia é "tratar polígono derivado como limite oficial". Rejeitado: duplicar a frase nos dois braços (duas cópias da mesma afirmação jurídica divergem na primeira revisão de copy). Atualização 2026-07-26: o fill-in remover-lista-zonas-mapa-inicio.md remove a listazoneBreakdowninteira (pedido de produto pós-B8 F2). F3 não presupõe o<ul>: quem landar primeiro coloca a ressalva atrelada ao escopo com ZE / presença da malha; o outro só ajusta. - i18n e naming seguem o AGENTS.md: identificadores em inglês, strings visíveis em pt-BR.
Questões em aberto (resolvidas na entrega)
- Qual resolução de chão para as duas malhas? ~~Opções (a) ~100 m nas duas · (b)
25 m nas zonas · (c)Resolvido: (a), ~100 m nas duas (decisão do usuário na sessão de implementação), e a pergunta estava mal-posta: a grade é a mesma nas três malhas, mas o que controla densidade é a tolerância de simplificação, que é por malha. Ver "Como foi entregue → F1" — a tolerância municipal aplicada às zonas custaria 5% da área de Salvador, então elas têm a própria, declarada em m² ao lado das irmãs.25 m nas duas. - Deletar os dois acessores órfãos ou declará-los contrato do módulo? Resolvido: (a), deletados. Os int specs montam o índice em uma linha. Se o produto quiser o contrato uniforme, ele volta de graça no 4º mesh.
Abordagem proposta
flowchart LR
F1["F1 escala de chão<br/>+ guard de densidade"] --> Custo["Chunk do mapa e vértices por frame"]
F2["F2 contrato da map key<br/>artefato -> featureMapKey"] --> Coerencia["Coroplético não pinta cinza em silêncio"]
F3["F3 degradar sem a malha<br/>+ ressalva na comparação"] --> Confianca["Disponibilidade e honestidade do painel"]
F4["F4 deletar o orfanado<br/>+ colapsar formas mortas"] --> Custo
Componentes:
- F1 — escala de chão + guard de densidade. Novo
scripts/lib/topology.mjs(ou nome irmão) expondo o alvo de resolução e umsimplifyTopology/quantizeToGroundScaleque derivam os dígitos do bbox da topologia recebida;scripts/build-municipality-zone-geometries.mjs:52-55,139-145escripts/build-bahia-geometries.mjs:53-57,96-101passam a consumi-lo. Rebuild debahia-municipality-zones.topo.json(o dissolve e oquantizefinal rodam em espaço de topologia com arcos compartilhados, então o snap move os dois lados de uma fronteira igualmente e não abre sliver entre zonas vizinhas).tests/int/bahiaGeometries.int.spec.ts:29ganha a asserção de pontos-por-feature ao lado deMAX_TOPO_BYTES; a janela de 280–360 km² e o centróide-dentro-da-cidade têm folga para o novo corte. Como F1 já reescreve o artefato, remove no mesmo passe a propriedadezoneNumber(script:382), que ninguém consome e é a terceira cópia de um valor derivável do slug. - F2 — contrato da map key. Duas asserções dentro do laço que já existe em
tests/int/bahiaGeometries.int.spec.ts:149:featureMapKey(zoneFeature.properties) === entry.slugefeatureMapKey(municipalFeature.properties) === entry.ibgeCode. Resolve junto a contradição entre o comentário debahiaMapStyle.ts:15("as três nunca coexistem num feature") e o unit de precedência emtests/unit/bahiaMapStyle.unit.spec.ts:51-55, que pina uma forma{ municipalitySlug, codarea }que nenhuma malha produz: se o invariante vale, cai o teste de precedência e o comentário de ordenação; se não vale, o comentário do invariante é que está errado. - F3 — degradar + ressalva.
BahiaMap.tsx:81passa a.catch()a metade das zonas e seguir com a malha municipal (Salvador volta a ser um polígono agregado, o que é pior que zonas e muito melhor que mapa nenhum). Ressalva: se o fill-in remover-lista-zonas-mapa-inicio.md já tiver landado, a frase já mora fora da lista — F3 só confirma que ela permanece no modo comparação e não reintroduz o<ul>; se F3 landar primeiro, relocate a frase para fora de!comparisonActive(ainda atrelada à malha/escopo com ZE) e o fill-in depois apaga só o<ul>+ o campo do bundle. - F4 — deletar e colapsar.
MunicipalityMapNavigation(municipalityMapNavigation.ts:18,64-70) virou{kind:'none'} | {kind:'navigate';slug}, três estados para umstring | undefined: colapsa emmunicipalitiesByMapKey[key]?.slugnos dois call sites (MunicipalityMapPanel.tsx:680-683,696-702), deletando o tipo, o helper, ouseMemoque só existe porque o helper aloca objeto por chamada, o widening a| nullemMapFeatureReadout.tsx:24,108e três unit tests.MunicipalityMapSlugEntry.namenão é lido no índice 1:1 (o rótulo do readout vem defeature.name), entãoMunicipalitiesByMapKeyviraRecord<string, string>— são 435 strings mortas no payload RSC de toda visita ao Início. Deleta os dois acessores órfãos (ver Questões em aberto) e, com eles,city/zoneNumberdeMunicipalityZoneNeighborhoodEntry(verificado: nem o card nem o script os leem — o script só usamunicipalitySlugeneighborhoods), o que também apaga o filtroentry.city === 'Salvador'provavelmente-sempre-verdadeiro no int. ExportapolygonRingsOfdemunicipalityProximity.ts:109(3º call site: o spec novo etests/helpers/featureBounds.ts:16reescrevem o mesmo branch Polygon/MultiPolygon) e alinhafeatureBounds.tsaoBahiaGeometryFeature— hoje é o único ainda tipado emBahiaMunicipalityFeature. TrocaBahiaGeometryFeature(que carregaGeoJsonProperties, ou sejaany) por umtype PolygonalFeature = { geometry: Polygon | MultiPolygon }que declara o requisito real dos três helpers, e introduzBahiaFeature<P>para as 4 grafias deFeature<Polygon | MultiPolygon, P>embahiaGeometriesTypes.ts. No script: a chave resolvida por alias é calculada duas vezes com o mesmo fallback (:329-330e:355-360) e odiedo guard é construído sobre a segunda — uma divergência faria o guard passar sobre um artefato que o primeiro passe montou errado; calcular uma vez numMapacima do passe 1. Fecha compolygonCount(:156, dois branches inalcançáveis depois dodie),[await shp(buffer)].flat()(:248, lê como erro), ourlqueensureCachedBinarydevolve e ninguém desestrutura, e a referência morta ao branchnone|navigate|zonesemdocs/plans/municipio-mais-proximo.md:141. - Migration: sem migration, sem collection, sem server action. F1 reescreve um artefato commitado (esperado e revisável no diff); F2–F4 não tocam dado.
Dependências
- Nenhuma dura de outro item aberto. F1 é dependência suave de B5 F2 (cache/factory dos scripts de geometria): quem fizer F1 cria o primeiro módulo compartilhado em
scripts/lib/, e o B5 F2 deve pousar nele em vez de abrir um segundo. - Reusa:
scripts/build-bahia-geometries.mjs(o irmão que define o padrão),bahiaGeometriesTypes.ts,municipalityProximity.ts,municipalityCatalog.ts, o laço de contrato detests/int/bahiaGeometries.int.spec.ts.
Não escopo
- Rank competitivo por ZE e resolver a ZE exata no card "Onde estou" — os dois débitos de produto do B8 F2, com gatilho no plano do B8. Este lote é custo e coerência, não capacidade nova.
- Bloco de helpers de CLI dos scripts (
die,sha256,cacheDir,ensureCached*,writeJson— hoje na 4ª–6ª cópia) — B5 F2 (escala-dry-pos-b2.md), cujo contador de call sites já foi atualizado com este script (5º deensureCached*, e o primeiro segundo consumidor real deGEOMETRIES_CACHE_DIR, o que valida a decisão decacheDirpor chamada que aquele plano tomou). OensureCachedBinarydaqui devolve o buffer cru em vez deJSON.parse— a variante que faltava paraensureCachedDownloadser superconjunto das quatro. - Genérico para o trio
*GeometryModule— adiado com gatilho abaixo; renomear um acessor público a três malhas não paga. normalizeNeighborhoodeIBGE_TO_TRE_NEIGHBORHOODno script — ficam locais de propósito: a regra do parêntese é o que transforma"Periperi (parte ao norte da Rua das Pedrinhas)"emPERIPERI, enormalizeMunicipalityKeynão faz isso. É chave de bairro, não de município, e o script é o único consumidor.
Rabbit holes
- "Já que estou mexendo na simplificação, melhoro a qualidade das duas malhas." Se alguém "só completar": os três artefatos são rebuildados, o diff fica ilegível e a malha estadual (que todo mundo baixa) engorda. Mitigação: F1 tem critério binário — pontos/feature das zonas cai para a ordem do irmão, e
bahia-municipalities.topo.jsonsai byte-idêntico ou com mudança justificada no plano. - "Já que a chave está em três lugares, crio um resolvedor único." Se alguém "só completar": o resolvedor precisa aceitar registro de catálogo e saco de propriedades GeoJSON, e vira wrapper raso sobre um
??. Mitigação: F2 é só teste; mudança de runtime aqui exige evidência nova. - "Já que estou no
loadLayerFeatures, limpo o modoterritorymorto." O branch não tem consumidor de produção (MunicipalityMapPanelfixamode="municipality"e o B21 tirou o painel de TI do Início), mas o corte v1 do E12 nomeia "sem modo de mapa TI" como adiado, ou seja, o artefato tem consumidor futuro nomeado. Mitigação: F4 deleta os acessores órfãos, não a malha nem o modo; a decisão sobre o modoterritorypertence ao E12.
Adiado com gatilho
rankZonesBySharedBoundaryé O(U × N) — reconstróiarcIndexes(other.arcs)para cada vizinho candidato de cada polígono não atribuído. Medido como irrelevante: centenas de milhares de inserts emSet, milissegundos, ofuscados pelo download e peloshp()da malha estadual. Revisitar quando: uma segunda cidade entrar no dissolve (oU × Ncresce com o mosaico, não com as zonas). Hoje, mexer nisso é queimar entrega em ganho zero.- Helper
createCampaignUserpara os e2e — a criação inline decampaignUserestá na 19ª cópia (campaignZoneMap.e2e.spec.ts:1618-1629é a mais nova), e o int já tem o helper (tests/helpers/campaignFixtures.ts:542-559). Pré-existente emmaine inerte, portanto fora deste lote. Revisitar quando: o próximo spec e2e nascer — ele paga o helper emcampaignE2EFixtures.tse migra os vizinhos que tocar. - Genérico para
MunicipalityZoneGeometryModule/MunicipalityGeometryModule/TerritoryGeometryModulecomgetFeature(key)uniforme. Revisitar quando: a 4ª malha entrar — a três, o rename atravessamunicipalityProximity.ts, dois int specs e um e2e por ganho estético. - Um único laço sobre
municipalitiesemmunicipalityMapData.ts(hoje quatro, cada um chamandomapKeyForMunicipality). Revisitar quando: o bundle ganhar um 5º campo por município — abaixo disso, juntar o laço histórico com o de cenários custa legibilidade e economiza microssegundos. - A grade de quantização é exata em um eixo só (achado do revisor de perf no
/simplifydesta entrega, 2026-07-27).quantizeaceita um número de dígitos e o aplica aos dois eixos, entãoGROUND_GRID_METRESé honrado em longitude e a latitude segue o aspect ratio do bbox: medido, 102,8×109,2 m nas duas malhas estaduais e 102,9×80,1 m na de Salvador. Ancorar em X não é acidente — é o que mantémbahia-municipalities.topo.jsonbyte-idêntico ao build pré-política, que é a prova de que F1 não mexeu na malha que todo mundo baixa. Tornar os dois eixos exatos exige passar um{scale, translate}explícito e reescrever os três artefatos, perdendo essa prova por um erro de ±9% num eixo, num mapa que para emmaxZoom: 10. Registrado, não corrigido: o comentário dequantizeDigitsForcarrega o número edescribeGroundScaleimprime os dois passos em todo build. Revisitar quando: uma malha precisar de precisão métrica isotrópica (medida de área/distância a partir do artefato, não da fonte) — ou de graça, se algum outro motivo já obrigar a reescrever os três artefatos.
Explicitamente fora (descartes deste triage)
geometriesByZone.set(slug, [...anterior, geometry])copia o array a cada insert (:375, O(K²) com K ≈ 30) —pushtem o mesmo tamanho e não é quadrático, mas o custo é indistinguível de zero; entra de graça se alguém já estiver naquela linha.- O laço de rank olhando Salvador 19 vezes (
municipalityMapData.ts:144, ~54 lookups extra por request) emapKeyForMunicipalitychamado 2× por município por ano — os próprios revisores concluíram que não vale tocar. @types/shpjs—tsconfig.jsonsó inclui**/*.ts(x), entãoscripts/*.mjsnunca é type-checked e o pacote compra hover no editor. Há precedente idêntico (@types/topojson-server); é consistência, não defeito.as unknown as MunicipalityZoneTopologyembahiaMunicipalityZoneGeometries.ts:10— byte a byte o padrão dos dois módulos irmãos (import de JSON que o TS não estreita).baseKeysser cinto-e-suspensório cominteractiveKeys— hojescopedKeysnunca contém2927408, então o polígono-base já seria não-interativo pelo caminho antigo. Mantido de propósito:baseKeyscodifica um invariante que o componente possui ("polígono de malha-base nunca é endereçável") em vez de um que o chamador por acaso fornece, e é o que mantém a base fora depathByKeyRef, que ofitMapToHighlightsnovo lê. O que falta é uma asserção dizendo qual dos dois mecanismos é load-bearing; se F2 sobrar tempo, é uma linha no e2e (total: 436, interactive: 435passa sob qualquer um dos dois).tests/int/municipalityMapData.int.spec.ts:73reescrevendo a regra da chave à mão em vez de chamarmapKeyForMunicipality— é o trabalho do teste: expectativa independente.- Shoelace duplicado no int spec (
bahiaGeometries.int.spec.ts:45-66) —featureCentroidprecisa da área assinada e dos momentos no mesmo passe, então compartilhar custaria um segundo passe ou uma forma de retorno estranha. F4 compartilha só opolygonRingsOf; a soma de áreas fica duplicada de propósito.
Referências
docs/roadmap.md(Fill-ins abertos — B8+)- poligonos-pracas-zona.md — o pai do lote (B8 F2 ✓), incl. os dois débitos de produto com gatilho
- escala-dry-pos-b2.md — B5 F2, dono do bloco de helpers de CLI que F1 encosta
- mapa-bahia-geometrias.md — B2, dono do padrão de artefato estático
- municipio-mais-proximo.md — B14 ✓, dono da memoização de rejeição que F3 preserva (e da referência morta que F4 corrige)
scripts/build-municipality-zone-geometries.mjs+scripts/build-bahia-geometries.mjs— F1, F4tests/int/bahiaGeometries.int.spec.ts(:29budget,:149laço de contrato) — F1, F2src/lib/bahiaMapStyle.ts(:15,:95-109) +tests/unit/bahiaMapStyle.unit.spec.ts— F2src/components/campaign/map/BahiaMap.tsx(:73-90duas malhas) +MunicipalityMapPanel.tsx(ressalva; listazoneBreakdown→ fill-in remover-lista-zonas-mapa-inicio.md) — F3src/utilities/municipalityMapNavigation.ts+src/lib/municipalityProximity.ts+src/lib/bahiaGeometriesTypes.ts+tests/helpers/featureBounds.ts— F4- AGENTS.md — geometrias B2/B8 F2, gate por entrega,
overrideAccess: falsecomuser .agents/rules/engineering-standards.mdc— "dead code dies immediately" (F4), depth check (F1/F2)