From 32a1a46fada36972b8a9ab0ff941867111d8eea0 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Tue, 15 Sep 2026 09:20:10 -0300 Subject: [PATCH] =?UTF-8?q?feat(C163):=20demografia=20complementar,=20ativ?= =?UTF-8?q?idade=20econ=C3=B4mica=20e=20transporte=20no=20relat=C3=B3rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/relatorio-cidade/SKILL.md | 38 +++++-- ...15-c163-economia-transporte-diversidade.md | 1 + scripts/lib/cityReportBlocks.mjs | 100 ++++++++++++++++-- scripts/lib/cityReportResearch.mjs | 36 +++++++ tests/unit/cityReportBlocks.unit.spec.ts | 49 ++++++++- tests/unit/cityReportRender.unit.spec.ts | 6 +- 6 files changed, 209 insertions(+), 21 deletions(-) create mode 100644 docs/changelog/2026-09-15-c163-economia-transporte-diversidade.md diff --git a/.agents/skills/relatorio-cidade/SKILL.md b/.agents/skills/relatorio-cidade/SKILL.md index 3e18387d..7401ce2e 100644 --- a/.agents/skills/relatorio-cidade/SKILL.md +++ b/.agents/skills/relatorio-cidade/SKILL.md @@ -107,6 +107,18 @@ URL por item) e emendas lidas da fonte oficial **em tempo de geração**. Págin "sourceUrl": "https://…", "sourceDate": "2024-08-14" } ], + "demography": [ // cor/raça e poder aquisitivo (Censo 2022) + { + "topic": "Cor/raça (Censo 2022)", "detail": "Parda 73,1% …", + "sourceUrl": "https://sidra.ibge.gov.br/tabela/9605", "sourceDate": "2026-09-15" + } + ], + "economy": [ // PIB, emprego, atividades (IBGE/RAIS e imprensa) + { "topic": "PIB e perfil", "detail": "…", "sourceUrl": "https://…", "sourceDate": "…" } + ], + "transport": [ // rodovias, aeroporto, portos, ferrovia + { "topic": "Rodovia federal (BR-101)", "detail": "…", "sourceUrl": "https://…", "sourceDate": "…" } + ], "gaps": [{ "id": "…", "label": "…", "reason": "…" }] // lacunas que o agente já sabe } ``` @@ -127,6 +139,18 @@ mais de uma fonte. O PDF mostra o item como "Emendas — indícios web" na pági 1 e as URLs na seção de fontes. **Nunca somar** indício de região/polo como emenda da cidade. +**Demografia, economia e transporte (pesquisa):** `demography`, `economy` e +`transport` são listas de `{topic, detail, sourceUrl, sourceDate}` (sem fonte → +lacuna). Pesquise: **cor/raça e rendimento** (Censo 2022 — SIDRA 9605 e 10295; +IBGE Cidades para salário médio); **economia** (PIB e PIB per capita — IBGE +Cidades; composição setorial e emprego formal — RAIS/CAGED e perfis regionais; +café, pecuária e comércio no caso do Extremo Sul); **transporte** (rodovias +federais/estaduais que cortam o município e obras novas/reformadas — DNIT, +Seinfra; aeroporto mais próximo em operação e o do polo, com situação; portos e +ferrovia da região — FIOL/Porto Sul, deixando claro quando não serve +diretamente a cidade). O enquadramento é "onde falta × onde o estado e a União +têm acertado" — o candidato é da base. + **Emendas oficiais:** sem `--emendas`, o builder consulta o Portal da Transparência (`PORTAL_TRANSPARENCIA_API_KEY` no ambiente; sem chave → lacuna). A API oficial **não filtra por município** (só UF/Nacional/Múltiplo na @@ -148,12 +172,14 @@ cacheado em `data/relatorios-cidade/.emendas.json` para replay. e estadual)` — top 5 por votos de 2022 na base TSE, com série 2014/2018/2022 e os prováveis candidatos do campo do prefeito (pesquisa) · `3. Rede e lideranças` — inclui as lideranças locais pesquisadas (ex-prefeitos/vices, - vereadores mais votados) · `4. Conjuntura` · `5. Sinais` · `6. Demandas e - visitas` · `7. Demografia` · `8. Acervo de falas` — cada fala com "O que é" - (sumário oficial) e "Menção ao município" (passagem que cita a cidade ou, se - o nome não aparece nos trechos, a marcação do acervo com o nº de municípios) - · `9. Notícias e imprensa` · `10. Panorama regional` · `11. Abordagem - sugerida (personas)` · `12. Fontes e limites`. + vereadores mais votados, com partido) · `4. Conjuntura` · `5. Sinais` · `6. + Demandas e visitas` · `7. Demografia` — IBGE Censo 2022 do artefato + + **complemento pesquisado** (cor/raça e poder aquisitivo) · `8. Atividade + econômica (pesquisa)` · `9. Transporte e conexões (pesquisa)` · `10. Acervo + de falas` — cada fala com "O que é" (sumário oficial) e "Menção ao município" + (passagem que cita a cidade ou, se o nome não aparece nos trechos, a marcação + do acervo com o nº de municípios) · `11. Notícias e imprensa` · `12. Panorama + regional` · `13. Abordagem sugerida (personas)` · `14. Fontes e limites`. - **Links clicáveis:** todo URL no PDF é um link (``): células de tabela, fontes por linha e a seção de fontes. diff --git a/docs/changelog/2026-09-15-c163-economia-transporte-diversidade.md b/docs/changelog/2026-09-15-c163-economia-transporte-diversidade.md new file mode 100644 index 00000000..64d2764b --- /dev/null +++ b/docs/changelog/2026-09-15-c163-economia-transporte-diversidade.md @@ -0,0 +1 @@ +**C163 — demografia complementar, economia e transporte no relatório (2026-09-15):** a seção de Demografia ganhou o complemento pesquisado de **cor/raça e poder aquisitivo** (Censo 2022; Itamaraju: parda 73,1%, branca 13,7%, preta 11,1%, indígena 2,0%; renda domiciliar per capita média R$ 997,27 e mediana R$ 725,00) e entraram duas seções novas: **"8. Atividade econômica (pesquisa)"** (PIB e per capita, composição setorial, emprego formal e atividades — IBGE Cidades, RAIS/perfis regionais; Itamaraju: PIB per capita R$ 23.412,56 em 2023, serviços 38,2%, agropecuária 27,7%, administração pública 23,7%, indústria 10,3%) e **"9. Transporte e conexões (pesquisa)"** (rodovias que cortam o município e obras novas/reformadas, aeroporto mais próximo em operação e o do polo, ferrovia/portos da região — FIOL/Porto Sul com a ressalva de não servir diretamente a cidade). O enquadramento é "onde falta × onde o estado e a União têm acertado", com URL por item. Contrato: `demography`, `economy` e `transport` (`{topic, detail, sourceUrl, sourceDate}`) validados — sem fonte vira lacuna —, listados nas fontes e documentados na skill; seções renumeradas para 14. Unit 3118/3118, `tsc`/eslint verdes; PDF de Itamaraju em 9 páginas. diff --git a/scripts/lib/cityReportBlocks.mjs b/scripts/lib/cityReportBlocks.mjs index f31e556a..20f93e75 100644 --- a/scripts/lib/cityReportBlocks.mjs +++ b/scripts/lib/cityReportBlocks.mjs @@ -747,7 +747,43 @@ const buildDemandsVisitsSection = ({ snapshot }) => { return blocks } -const buildDemographicsSection = ({ snapshot }) => { +const buildSourcedListBlocks = ({ title, note, items }) => { + if (!items.length) { + return [ + { + kind: 'callout', + tone: 'gap', + title, + body: ['Nenhum item pesquisado com fonte para este município.', GAP_COPY], + }, + ] + } + return [ + { + kind: 'table', + title, + columns: [ + { key: 'topic', label: 'Tema', width: 22 }, + { key: 'detail', label: 'Dado', width: 62 }, + { key: 'source', label: 'Fonte', width: 16 }, + ], + rows: items.map((item) => ({ + topic: item.topic, + detail: item.detail, + source: formatDateBr(item.sourceDate), + })), + note, + sources: items.map((item) => ({ + kind: 'web', + label: item.topic, + url: item.sourceUrl, + date: item.sourceDate, + })), + }, + ] +} + +const buildDemographicsSection = ({ snapshot, research }) => { const demographics = snapshot.demographics if (!demographics) { return [ @@ -759,10 +795,10 @@ const buildDemographicsSection = ({ snapshot }) => { }, ] } - return [ + const blocks = [ { kind: 'stats', - title: 'Demografia', + title: 'Demografia (IBGE Censo 2022)', rows: [ { label: 'População', value: formatInteger(demographics.population) }, { label: '0–17 anos', value: formatInteger(demographics.ageBands['0-17']) }, @@ -779,8 +815,30 @@ const buildDemographicsSection = ({ snapshot }) => { sources: [sourceTeqo('IBGE Censo 2022 (artefato commitado)', null)], }, ] + blocks.push( + ...buildSourcedListBlocks({ + title: 'Demografia complementar (pesquisa)', + note: 'Cor/raça e poder aquisitivo (IBGE Censo 2022) — leitura para agenda e linguagem na cidade; cada item com URL.', + items: research.demography ?? [], + }), + ) + return blocks } +const buildEconomySection = ({ research }) => + buildSourcedListBlocks({ + title: 'Atividade econômica (pesquisa)', + note: 'PIB, emprego e principais atividades (IBGE/CAGED e imprensa local) — onde a cidade gera renda e onde depende de fora; cada item com URL.', + items: research.economy ?? [], + }) + +const buildTransportSection = ({ research }) => + buildSourcedListBlocks({ + title: 'Transporte e conexões (pesquisa)', + note: 'Rodovias (novas/reformadas), aeroporto mais próximo, portos e ferrovia: como a cidade se conecta à Bahia e o que está em obra/planejado — cada item com URL.', + items: research.transport ?? [], + }) + const buildSpeechesSection = ({ snapshot }) => { const { speeches, municipality } = snapshot if (!speeches.rows.length) { @@ -996,6 +1054,16 @@ const buildSourcesSection = ({ snapshot, research, emendas }) => { date: item.sourceDate, }) } + for (const listKey of ['demography', 'economy', 'transport']) { + for (const item of research[listKey] ?? []) { + items.push({ + kind: 'web', + label: `${listKey === 'demography' ? 'Demografia' : listKey === 'economy' ? 'Economia' : 'Transporte'} — ${item.topic}`, + url: item.sourceUrl, + date: item.sourceDate, + }) + } + } return [ { @@ -1069,26 +1137,40 @@ export const buildCityReport = ({ snapshot, research, emendas, generatedAt = new title: '6. Demandas e visitas', blocks: buildDemandsVisitsSection({ snapshot }), }, - { id: 'demografia', title: '7. Demografia', blocks: buildDemographicsSection({ snapshot }) }, - { id: 'falas', title: '8. Acervo de falas', blocks: buildSpeechesSection({ snapshot }) }, + { + id: 'demografia', + title: '7. Demografia', + blocks: buildDemographicsSection({ snapshot, research }), + }, + { + id: 'economia', + title: '8. Atividade econômica (pesquisa)', + blocks: buildEconomySection({ research }), + }, + { + id: 'transporte', + title: '9. Transporte e conexões (pesquisa)', + blocks: buildTransportSection({ research }), + }, + { id: 'falas', title: '10. Acervo de falas', blocks: buildSpeechesSection({ snapshot }) }, { id: 'noticias', - title: '9. Notícias internas e imprensa local', + title: '11. Notícias internas e imprensa local', blocks: buildNewsSection({ research }), }, { id: 'regiao', - title: '10. Panorama regional (Território de Identidade)', + title: '12. Panorama regional (Território de Identidade)', blocks: buildRegionSection({ snapshot }), }, { id: 'abordagem', - title: '11. Abordagem sugerida (pesquisa)', + title: '13. Abordagem sugerida (pesquisa)', blocks: buildApproachSection({ research }), }, { id: 'fontes', - title: '12. Fontes e limites', + title: '14. Fontes e limites', blocks: buildSourcesSection({ snapshot, research, emendas }), }, ] diff --git a/scripts/lib/cityReportResearch.mjs b/scripts/lib/cityReportResearch.mjs index 4a875ed3..90ad7c5e 100644 --- a/scripts/lib/cityReportResearch.mjs +++ b/scripts/lib/cityReportResearch.mjs @@ -267,6 +267,39 @@ export const normalizeResearchInput = (raw, { now = new Date() } = {}) => { }) } + const sourcedLists = { demography: [], economy: [], transport: [] } + for (const key of Object.keys(sourcedLists)) { + for (const entry of Array.isArray(raw[key]) ? raw[key] : []) { + const topic = isNonEmptyString(entry?.topic) ? entry.topic.trim() : null + const detail = isNonEmptyString(entry?.detail) ? entry.detail.trim() : null + const sourceUrl = isNonEmptyString(entry?.sourceUrl) ? entry.sourceUrl.trim() : null + const sourceDate = isValidDate(entry?.sourceDate) ? entry.sourceDate : null + if (!topic || !detail) { + gaps.push({ + id: `${key}_incompleto`, + label: topic, + reason: 'Item sem tema ou dado — descartado.', + }) + continue + } + if (!sourceUrl || !sourceDate) { + gaps.push({ + id: `${key}_sem_fonte`, + label: topic, + reason: 'Item sem fonte: URL e data são obrigatórias.', + }) + continue + } + sourcedLists[key].push({ + topic, + detail, + sourceUrl, + sourceDate, + consultedAt: isValidDate(entry.consultedAt) ? entry.consultedAt : null, + }) + } + } + return { municipalitySlug: raw.municipalitySlug.trim(), researchedAt: researchedAt.toISOString(), @@ -275,6 +308,9 @@ export const normalizeResearchInput = (raw, { now = new Date() } = {}) => { approach, preCandidates, leaders, + demography: sourcedLists.demography, + economy: sourcedLists.economy, + transport: sourcedLists.transport, gaps, } } diff --git a/tests/unit/cityReportBlocks.unit.spec.ts b/tests/unit/cityReportBlocks.unit.spec.ts index f462c07d..33486ad4 100644 --- a/tests/unit/cityReportBlocks.unit.spec.ts +++ b/tests/unit/cityReportBlocks.unit.spec.ts @@ -70,6 +70,30 @@ const research = normalizeResearchInput( sourceDate: '2026-03-05', }, ], + demography: [ + { + topic: 'Cor/raça', + detail: 'Parda 62%, branca 25%.', + sourceUrl: 'https://exemplo.test/cor-raca', + sourceDate: '2026-09-10', + }, + ], + economy: [ + { + topic: 'PIB', + detail: 'Serviços e agropecuária lideram.', + sourceUrl: 'https://exemplo.test/pib', + sourceDate: '2026-09-10', + }, + ], + transport: [ + { + topic: 'Rodovias', + detail: 'BR-101 corta o município; trecho em obra.', + sourceUrl: 'https://exemplo.test/br101', + sourceDate: '2026-09-10', + }, + ], gaps: [], }, { now: generatedAt }, @@ -323,12 +347,29 @@ const asReport = (value: unknown) => value as ReportShape describe('buildCityReport', () => { const report = asReport(buildCityReport({ snapshot, research, emendas, generatedAt })) - it('builds the 12 deepening sections in the product order', () => { - expect(report.sections).toHaveLength(12) + it('builds the 14 deepening sections in the product order', () => { + expect(report.sections).toHaveLength(14) expect(report.sections[0].title).toMatch(/Conta eleitoral completa/) expect(report.sections[1].title).toMatch(/Concorrentes no município/) - expect(report.sections[10].title).toMatch(/Abordagem sugerida/) - expect(report.sections[11].title).toMatch(/Fontes e limites/) + expect(report.sections[7].title).toMatch(/Atividade econômica/) + expect(report.sections[8].title).toMatch(/Transporte e conexões/) + expect(report.sections[12].title).toMatch(/Abordagem sugerida/) + expect(report.sections[13].title).toMatch(/Fontes e limites/) + }) + + it('adds the researched demography, economy and transport data', () => { + const demografia = report.sections.find((section) => section.id === 'demografia')! + const complementary = demografia.blocks.find( + (block) => block.title === 'Demografia complementar (pesquisa)', + )! + expect((complementary.rows as unknown as TableRow[])[0].topic).toBe('Cor/raça') + const economia = report.sections.find((section) => section.id === 'economia')!.blocks[0] + expect((economia.rows as unknown as TableRow[])[0].detail).toContain('Serviços') + const transporte = report.sections.find((section) => section.id === 'transporte')!.blocks[0] + expect((transporte.rows as unknown as TableRow[])[0].detail).toContain('BR-101') + expect(transporte.sources![0]).toEqual( + expect.objectContaining({ url: 'https://exemplo.test/br101' }), + ) }) it('lists the main competitors with their vote series and the pre-candidates', () => { diff --git a/tests/unit/cityReportRender.unit.spec.ts b/tests/unit/cityReportRender.unit.spec.ts index 0edeafec..d0d467ba 100644 --- a/tests/unit/cityReportRender.unit.spec.ts +++ b/tests/unit/cityReportRender.unit.spec.ts @@ -114,8 +114,10 @@ describe('renderReportMd', () => { it('carries the same sections as tables and headings', () => { expect(md).toContain('## 1. Conta eleitoral completa') expect(md).toContain('## 2. Concorrentes no município (federal e estadual)') - expect(md).toContain('## 11. Abordagem sugerida (pesquisa)') - expect(md).toContain('## 12. Fontes e limites') + expect(md).toContain('## 8. Atividade econômica (pesquisa)') + expect(md).toContain('## 9. Transporte e conexões (pesquisa)') + expect(md).toContain('## 13. Abordagem sugerida (pesquisa)') + expect(md).toContain('## 14. Fontes e limites') expect(md).toContain('| Ano | Votos |') })