senado-br-mcp 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +891 -810
- package/README.pt-BR.md +829 -788
- package/dist/app-surface.js +122 -0
- package/dist/dataset/dictionary.js +94 -0
- package/dist/dataset/harmonize.js +32 -0
- package/dist/dataset/provenance.js +56 -0
- package/dist/dataset/release.js +164 -0
- package/dist/dataset/schema.js +572 -0
- package/dist/legal.js +114 -0
- package/dist/observability/origin.js +36 -0
- package/dist/openai-app-widget.js +455 -0
- package/dist/openai-domain-verification.js +10 -0
- package/dist/scraper/ecidadania.js +207 -33
- package/dist/scraper/pipeline.js +24 -4
- package/dist/server.js +22 -9
- package/dist/status.js +7 -0
- package/dist/throttle/upstream.js +2 -1
- package/dist/tools/comissoes.js +82 -31
- package/dist/tools/composicao.js +76 -33
- package/dist/tools/contratacoes.js +69 -26
- package/dist/tools/discursos.js +48 -15
- package/dist/tools/ecidadania.js +71 -23
- package/dist/tools/legislacao.js +46 -36
- package/dist/tools/materias.js +65 -16
- package/dist/tools/orcamento-senado.js +130 -10
- package/dist/tools/orcamento.js +82 -36
- package/dist/tools/plenario.js +61 -34
- package/dist/tools/processos.js +107 -19
- package/dist/tools/referencia.js +3 -4
- package/dist/tools/senadores-admin.js +109 -20
- package/dist/tools/senadores.js +58 -12
- package/dist/tools/servidores.js +254 -18
- package/dist/tools/supridos.js +150 -1
- package/dist/tools/taquigrafia.js +32 -9
- package/dist/tools/votacao-comissao.js +52 -29
- package/dist/tools/votacoes.js +42 -7
- package/dist/utils/estatisticas.js +134 -0
- package/dist/utils/placar.js +34 -0
- package/dist/utils/provenance.js +67 -23
- package/dist/utils/untrusted.js +0 -0
- package/dist/utils/upstream-parse.js +103 -0
- package/dist/utils/validation.js +53 -0
- package/dist/version.js +2 -1
- package/package.json +30 -7
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dataset de participação do e-Cidadania — ESQUEMA HARMONIZADO (Fase 1.2, sessão C1).
|
|
3
|
+
*
|
|
4
|
+
* Este arquivo é a FONTE ÚNICA da verdade da harmonização: para cada variável canônica do dataset
|
|
5
|
+
* ele declara o tipo, a descrição, e — o que dá valor científico ao produto — a PROVENIÊNCIA por campo
|
|
6
|
+
* (`sourceEndpoint` + `sourceField`) e a operacionalização (como o valor foi produzido a partir da
|
|
7
|
+
* fonte). O mesmo objeto alimenta (a) o envelope de proveniência por registro (`provenance.ts`) e
|
|
8
|
+
* (b) o dicionário de variáveis (`dictionary.ts`) — não há um segundo lugar a manter em sincronia.
|
|
9
|
+
*
|
|
10
|
+
* Base factual: `docs/reconhecimento-ecidadania.md` (recon de escopo fechado, 30/06–01/07/2026) e os
|
|
11
|
+
* parsers reais do repo (`scripts/ingest-ecidadania/{listing,ideias-listing,eventos-listing,csv}.ts`,
|
|
12
|
+
* `src/scraper/ecidadania.ts`). Os `sourceField` apontam para o campo/seletor UPSTREAM verdadeiro —
|
|
13
|
+
* NÃO para o payload já normalizado — porque a ETAPA 4 do ROADMAP valida cada `sourceField` à mão.
|
|
14
|
+
*
|
|
15
|
+
* CONVENÇÃO DE HONESTIDADE (`derived:`): variáveis SEM fonte upstream discreta usam o prefixo
|
|
16
|
+
* `derived:` em `sourceEndpoint`, para que um auditor veja de imediato que o valor nasce do nosso
|
|
17
|
+
* processo, não do Senado:
|
|
18
|
+
* - `derived:ecidadania_history` — observação do próprio corpus (ex.: `firstSeenAt`).
|
|
19
|
+
* - `derived:calculo-local` — cálculo em código a partir de outros campos do mesmo registro
|
|
20
|
+
* (ex.: `totalVotos`, percentuais, `url` construída).
|
|
21
|
+
* Variáveis COM fonte upstream mas transformadas (ex.: `status`, derivado de `/processo?tramitando=S`)
|
|
22
|
+
* mantêm o endpoint real em `sourceEndpoint` e anotam a derivação em `sourceField`.
|
|
23
|
+
*/
|
|
24
|
+
import { ECIDADANIA_ARQUIMEDES_CSV_URL, ECIDADANIA_BASE_URL } from "../utils/provenance.js";
|
|
25
|
+
/** Versão do esquema harmonizado. Gravada em CADA registro (load-bearing na máquina de releases, C2). */
|
|
26
|
+
export const DATASET_SCHEMA_VERSION = "2.0.0";
|
|
27
|
+
/** Licença do DADO (separada da licença de código). Mesma string do registry de proveniência. */
|
|
28
|
+
export const DATASET_LICENSE = "Dados Abertos do Senado Federal — uso livre com atribuição da fonte.";
|
|
29
|
+
// ── Endpoints canônicos (strings de proveniência) ───────────────────────────
|
|
30
|
+
const EP_CONSULTAS_LISTING = `GET ${ECIDADANIA_BASE_URL}/pesquisamateria?p={N}`;
|
|
31
|
+
const EP_CONSULTAS_DETALHE = `GET ${ECIDADANIA_BASE_URL}/visualizacaomateria?id={ID}`;
|
|
32
|
+
const EP_IDEIAS_LISTING = `GET ${ECIDADANIA_BASE_URL}/pesquisaideia?situacao={S}&p={N}`;
|
|
33
|
+
const EP_IDEIAS_DETALHE = `GET ${ECIDADANIA_BASE_URL}/visualizacaoideia?id={ID}`;
|
|
34
|
+
const EP_EVENTOS_LISTING = `GET ${ECIDADANIA_BASE_URL}/principalaudiencia?p={N}`;
|
|
35
|
+
const EP_EVENTOS_DETALHE = `GET ${ECIDADANIA_BASE_URL}/visualizacaoaudiencia?id={ID}`;
|
|
36
|
+
const EP_EVENTOS_COMENTARIOS = `GET ${ECIDADANIA_BASE_URL}/ajaxcolecaocomentarioaudiencia?audienciaId={ID}`;
|
|
37
|
+
const EP_PROCESSO_TRAMITANDO = "GET https://legis.senado.leg.br/dadosabertos/processo?sigla={SIGLA}&tramitando=S";
|
|
38
|
+
const EP_CSV_ARQUIMEDES = `GET ${ECIDADANIA_ARQUIMEDES_CSV_URL}`;
|
|
39
|
+
const EP_HISTORY = "derived:ecidadania_history";
|
|
40
|
+
const EP_CALC = "derived:calculo-local";
|
|
41
|
+
// ── consultas (corpus vivo) ─────────────────────────────────────────────────
|
|
42
|
+
const consultasSchema = {
|
|
43
|
+
titulo: "Consultas públicas (Apoie)",
|
|
44
|
+
fonteResumo: "Listagem HTML paginada `pesquisamateria` (cobertura integral; votos no gráfico) + status derivado de `/processo?tramitando=S`.",
|
|
45
|
+
variables: [
|
|
46
|
+
{
|
|
47
|
+
name: "materia",
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Identificação da matéria em consulta (ex.: \"PL 5064/2023\").",
|
|
50
|
+
sourceEndpoint: EP_CONSULTAS_LISTING,
|
|
51
|
+
sourceField: "div.resumo-materia > header > a (texto da âncora)",
|
|
52
|
+
operationalization: "Texto da âncora do cabeçalho do bloco de resultado, com entidades HTML decodificadas.",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "ementa",
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "Ementa da matéria.",
|
|
58
|
+
sourceEndpoint: EP_CONSULTAS_LISTING,
|
|
59
|
+
sourceField: "div.resumo-materia > section > a (texto da âncora)",
|
|
60
|
+
operationalization: "Texto da âncora da seção do bloco, com entidades HTML decodificadas.",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "votosSim",
|
|
64
|
+
type: "integer",
|
|
65
|
+
unit: "votos",
|
|
66
|
+
description: "Total de votos SIM (favoráveis) na consulta.",
|
|
67
|
+
sourceEndpoint: EP_CONSULTAS_LISTING,
|
|
68
|
+
sourceField: "figure.grafico-consulta-publica > header > span[1] (SIM)",
|
|
69
|
+
operationalization: "Primeiro <span> do header do gráfico; número BR (ponto de milhar) via parseBrNum.",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "votosNao",
|
|
73
|
+
type: "integer",
|
|
74
|
+
unit: "votos",
|
|
75
|
+
description: "Total de votos NÃO (contrários) na consulta.",
|
|
76
|
+
sourceEndpoint: EP_CONSULTAS_LISTING,
|
|
77
|
+
sourceField: "figure.grafico-consulta-publica > header > span[2] (NÃO)",
|
|
78
|
+
operationalization: "Segundo <span> do header do gráfico; número BR via parseBrNum.",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "totalVotos",
|
|
82
|
+
type: "integer",
|
|
83
|
+
unit: "votos",
|
|
84
|
+
description: "Soma de votosSim + votosNao.",
|
|
85
|
+
sourceEndpoint: EP_CALC,
|
|
86
|
+
sourceField: "votosSim + votosNao",
|
|
87
|
+
operationalization: "Derivado em código: soma dos dois campos de voto do mesmo registro.",
|
|
88
|
+
derived: true,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "percentualSim",
|
|
92
|
+
type: "integer",
|
|
93
|
+
unit: "%",
|
|
94
|
+
description: "Percentual de votos SIM sobre o total.",
|
|
95
|
+
sourceEndpoint: EP_CALC,
|
|
96
|
+
sourceField: "round(votosSim / totalVotos * 100)",
|
|
97
|
+
operationalization: "Derivado em código; 0 quando totalVotos = 0. Math.round.",
|
|
98
|
+
derived: true,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "percentualNao",
|
|
102
|
+
type: "integer",
|
|
103
|
+
unit: "%",
|
|
104
|
+
description: "Percentual de votos NÃO sobre o total.",
|
|
105
|
+
sourceEndpoint: EP_CALC,
|
|
106
|
+
sourceField: "round(votosNao / totalVotos * 100)",
|
|
107
|
+
operationalization: "Derivado em código; 0 quando totalVotos = 0. Math.round.",
|
|
108
|
+
derived: true,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "autoria",
|
|
112
|
+
type: "string",
|
|
113
|
+
description: "Autoria da matéria em consulta (parlamentar/órgão — agente público).",
|
|
114
|
+
sourceEndpoint: EP_CONSULTAS_DETALHE,
|
|
115
|
+
sourceField: "página de detalhe: <b>Autoria:</b> <span>…</span>",
|
|
116
|
+
operationalization: "Lido da página de detalhe (visualizacaomateria) no crawl enriquecido; null quando o detalhe não expõe. Nome MANTIDO (informação pública por função — ver postura de privacidade).",
|
|
117
|
+
caveat: "Detail-only (v2): populado pelo crawl de detalhe; null nas linhas ainda não enriquecidas.",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "relator",
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "Relator(a) da matéria (agente público).",
|
|
123
|
+
sourceEndpoint: EP_CONSULTAS_DETALHE,
|
|
124
|
+
sourceField: "página de detalhe: <b>Relator(a):</b> <span>…</span>",
|
|
125
|
+
operationalization: "Lido da página de detalhe (visualizacaomateria); null quando não há relatoria ou o detalhe não a expõe. Nome MANTIDO (público por função).",
|
|
126
|
+
caveat: "Detail-only (v2): populado pelo crawl de detalhe; null nas linhas ainda não enriquecidas.",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "status",
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "Situação da consulta: \"aberta\" | \"encerrada\".",
|
|
132
|
+
sourceEndpoint: EP_PROCESSO_TRAMITANDO,
|
|
133
|
+
sourceField: "(derivado) presença de codigoMateria no conjunto tramitando=S",
|
|
134
|
+
operationalization: "aberta ⟺ a matéria está no universo /processo?tramitando=S; senão encerrada. A derivação ocorre no MESMO run do scrape da listagem, então o retrievedAt do registro cobre também este campo.",
|
|
135
|
+
derived: true,
|
|
136
|
+
caveat: "Linger re-status: matéria que sai de tramitação vira \"encerrada\" e congela com os votos finais (nunca por mera ausência da listagem).",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "url",
|
|
140
|
+
type: "url",
|
|
141
|
+
description: "URL canônica da consulta no portal.",
|
|
142
|
+
sourceEndpoint: EP_CALC,
|
|
143
|
+
sourceField: "visualizacaomateria?id={entityId}",
|
|
144
|
+
operationalization: "Construída a partir do codigoMateria; não é lida do upstream.",
|
|
145
|
+
derived: true,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "firstSeenAt",
|
|
149
|
+
type: "date",
|
|
150
|
+
description: "Primeira vez que o registro foi observado no corpus (proxy prospectivo de ritmo de entrada).",
|
|
151
|
+
sourceEndpoint: EP_HISTORY,
|
|
152
|
+
sourceField: "MIN(scraped_at) per entity_id",
|
|
153
|
+
operationalization: "MIN(scraped_at) sobre ecidadania_history (Recon Parte III). Observação do nosso crawler — NÃO é data upstream do Senado.",
|
|
154
|
+
derived: true,
|
|
155
|
+
caveat: "Censura à esquerda: piso 14/06/2026; baseline 16/06 (98,6% do corpus) deve ser excluído de análises de ritmo; série interpretável a partir de 22/06/2026. Resolução = cadência do crawl de corpus completo.",
|
|
156
|
+
select: (ctx) => ctx.meta.firstSeenAt ?? null,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
// ── ideias (corpus vivo) ─────────────────────────────────────────────────────
|
|
161
|
+
const ideiasSchema = {
|
|
162
|
+
titulo: "Ideias legislativas",
|
|
163
|
+
fonteResumo: "Listagem HTML paginada `pesquisaideia`, varrida por valor de `situacao` (status vem do parâmetro).",
|
|
164
|
+
variables: [
|
|
165
|
+
{
|
|
166
|
+
name: "titulo",
|
|
167
|
+
type: "string",
|
|
168
|
+
description: "Título da ideia legislativa.",
|
|
169
|
+
sourceEndpoint: EP_IDEIAS_LISTING,
|
|
170
|
+
sourceField: "article.resumo-ideia > section > a (texto da âncora)",
|
|
171
|
+
operationalization: "Texto da âncora da seção, com entidades HTML decodificadas.",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "apoios",
|
|
175
|
+
type: "integer",
|
|
176
|
+
unit: "apoios",
|
|
177
|
+
description: "Número de apoios recebidos pela ideia.",
|
|
178
|
+
sourceEndpoint: EP_IDEIAS_LISTING,
|
|
179
|
+
sourceField: "article.resumo-ideia > figure.grafico-ideia-legislativa > footer > span[1] (\"N apoios\")",
|
|
180
|
+
operationalization: "Primeiro <span> do footer do figure.grafico-ideia-legislativa (\"253.804 apoios\"); número BR via parseBrNum. O span de meta (limiar) é ignorado.",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "status",
|
|
184
|
+
type: "string",
|
|
185
|
+
description: "Situação da ideia: \"aberta\" | \"encerrada\" | \"convertida\".",
|
|
186
|
+
sourceEndpoint: EP_IDEIAS_LISTING,
|
|
187
|
+
sourceField: "(derivado) parâmetro GET situacao da listagem varrida",
|
|
188
|
+
operationalization: "A listagem é varrida por situacao=N; o status é o mapa SITUACAO_STATUS ({5,6,8}→aberta, {7,9}→encerrada, 10→convertida) do valor usado no crawl.",
|
|
189
|
+
derived: true,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "dataPublicacao",
|
|
193
|
+
type: "date",
|
|
194
|
+
description: "Data limite para atingir os 20.000 apoios (única âncora temporal upstream da ideia).",
|
|
195
|
+
sourceEndpoint: EP_IDEIAS_DETALHE,
|
|
196
|
+
sourceField: "página de detalhe: bloco \"Data limite\" (DD/MM/AAAA)",
|
|
197
|
+
operationalization: "extractDate sobre a data-limite exibida no detalhe (visualizacaoideia) → ISO YYYY-MM-DD; lido no crawl de detalhe. Reaberto na v2 (era null por design na v1).",
|
|
198
|
+
caveat: "Detail-only (v2): null nas ideias ainda não enriquecidas pelo backfill de detalhe (censura à esquerda residual nesses campos até o backfill completar).",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "autorUf",
|
|
202
|
+
type: "string",
|
|
203
|
+
description: "UF do cidadão autor da ideia (SEM nome — conteúdo de cidadão).",
|
|
204
|
+
sourceEndpoint: EP_IDEIAS_DETALHE,
|
|
205
|
+
sourceField: "página de detalhe: \"Ideia proposta por … (UF)\" — apenas o \"(UF)\"",
|
|
206
|
+
operationalization: "Extrai só a sigla de UF entre parênteses; o NOME do cidadão é descartado NA ORIGEM (nunca gravado no corpus). Postura de privacidade: conteúdo de cidadão → só UF.",
|
|
207
|
+
caveat: "Detail-only (v2): null nas ideias ainda não enriquecidas. Nome do autor deliberadamente não coletado.",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "descricao",
|
|
211
|
+
type: "string",
|
|
212
|
+
description: "Texto/corpo da ideia legislativa (conteúdo deliberativo do cidadão).",
|
|
213
|
+
sourceEndpoint: EP_IDEIAS_DETALHE,
|
|
214
|
+
sourceField: "página de detalhe: corpo da ideia (visualizacaoideia)",
|
|
215
|
+
operationalization: "Texto do detalhe, com tags removidas; lido no crawl de detalhe. É conteúdo, não identidade.",
|
|
216
|
+
caveat: "Detail-only (v2): null nas ideias ainda não enriquecidas.",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: "plConvertido",
|
|
220
|
+
type: "string",
|
|
221
|
+
description: "Proposição gerada pela ideia (ex.: \"SUG 12/2024\"), quando convertida.",
|
|
222
|
+
sourceEndpoint: EP_IDEIAS_DETALHE,
|
|
223
|
+
sourceField: "página de detalhe: identificação da proposição (SUG/PL nº)",
|
|
224
|
+
operationalization: "Regex de identificação de proposição no detalhe; null quando a ideia não gerou proposição.",
|
|
225
|
+
caveat: "Detail-only (v2): null nas ideias ainda não enriquecidas ou não convertidas.",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "url",
|
|
229
|
+
type: "url",
|
|
230
|
+
description: "URL canônica da ideia no portal.",
|
|
231
|
+
sourceEndpoint: EP_CALC,
|
|
232
|
+
sourceField: "visualizacaoideia?id={entityId}",
|
|
233
|
+
operationalization: "Construída a partir do id; não lida do upstream.",
|
|
234
|
+
derived: true,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "firstSeenAt",
|
|
238
|
+
type: "date",
|
|
239
|
+
description: "Primeira observação do registro no corpus (proxy prospectivo de ritmo de entrada).",
|
|
240
|
+
sourceEndpoint: EP_HISTORY,
|
|
241
|
+
sourceField: "MIN(scraped_at) per entity_id",
|
|
242
|
+
operationalization: "MIN(scraped_at) sobre ecidadania_history. Observação do crawler — NÃO é data upstream.",
|
|
243
|
+
derived: true,
|
|
244
|
+
caveat: "Censura à esquerda: piso 14/06/2026; baseline de ideias = 29/06/2026 (~99,9% do corpus — primeiro crawl completo da entidade) deve ser excluído de análises de ritmo; série interpretável a partir de 30/06/2026. Resolução = cadência do crawl de corpus completo.",
|
|
245
|
+
select: (ctx) => ctx.meta.firstSeenAt ?? null,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
// ── eventos (corpus vivo) ────────────────────────────────────────────────────
|
|
250
|
+
const eventosSchema = {
|
|
251
|
+
titulo: "Eventos interativos (audiências)",
|
|
252
|
+
fonteResumo: "Listagem HTML paginada `principalaudiencia` (status no sufixo de classe do bloco).",
|
|
253
|
+
variables: [
|
|
254
|
+
{
|
|
255
|
+
name: "titulo",
|
|
256
|
+
type: "string",
|
|
257
|
+
description: "Título/descrição do evento.",
|
|
258
|
+
sourceEndpoint: EP_EVENTOS_LISTING,
|
|
259
|
+
sourceField: "article.resumo-audiencia .descricao > a (texto da âncora)",
|
|
260
|
+
operationalization: "Texto da âncora dentro de .descricao, com entidades HTML decodificadas.",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: "data",
|
|
264
|
+
type: "date",
|
|
265
|
+
description: "Data do evento (canônica, da página de detalhe).",
|
|
266
|
+
sourceEndpoint: EP_EVENTOS_DETALHE,
|
|
267
|
+
sourceField: "span.audiencia-data (parte DD/MM/AAAA)",
|
|
268
|
+
operationalization: "extractDate sobre \"DD/MM/AAAA - HH:MM\" do detalhe → ISO YYYY-MM-DD. Corrigido do detalhe na v2 (estudo A3: listagem fiel em 98,9%, mas o detalhe é canônico). Fallback para a data da listagem nas linhas ainda não enriquecidas.",
|
|
269
|
+
caveat: "Detalhe é canônico (estudo A3). Divergência de borda de 1 dia possível vs. listagem; linhas pré-enriquecimento carregam a data da listagem como provisória.",
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: "hora",
|
|
273
|
+
type: "string",
|
|
274
|
+
description: "Hora do evento (HH:MM, canônica, da página de detalhe).",
|
|
275
|
+
sourceEndpoint: EP_EVENTOS_DETALHE,
|
|
276
|
+
sourceField: "span.audiencia-data (parte HH:MM)",
|
|
277
|
+
operationalization: "extractTime sobre a mesma célula do detalhe. Corrigido do detalhe na v2 — a listagem era degradada (placeholder 00:00 em eventos antigos + offset mediano +15 min; estudo A3: 57% divergente). Fallback para a hora da listagem nas linhas ainda não enriquecidas.",
|
|
278
|
+
caveat: "Detalhe é canônico (estudo A3). A hora da listagem não serve para cruzamento fino; linhas pré-enriquecimento carregam a hora da listagem como provisória.",
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: "comissao",
|
|
282
|
+
type: "string",
|
|
283
|
+
description: "Sigla da comissão promotora.",
|
|
284
|
+
sourceEndpoint: EP_EVENTOS_LISTING,
|
|
285
|
+
sourceField: "em.sigla (token após \"|\")",
|
|
286
|
+
operationalization: "Célula \" | CCT\": mantém o token após o último \"|\".",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "comissaoNomeCompleto",
|
|
290
|
+
type: "string",
|
|
291
|
+
description: "Nome completo da comissão promotora.",
|
|
292
|
+
sourceEndpoint: EP_EVENTOS_DETALHE,
|
|
293
|
+
sourceField: "div.audiencia-comissao",
|
|
294
|
+
operationalization: "Texto do bloco de comissão no detalhe; null nas linhas ainda não enriquecidas.",
|
|
295
|
+
caveat: "Detail-only (v2).",
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: "local",
|
|
299
|
+
type: "string",
|
|
300
|
+
description: "Local de realização do evento.",
|
|
301
|
+
sourceEndpoint: EP_EVENTOS_DETALHE,
|
|
302
|
+
sourceField: "div.audiencia-local",
|
|
303
|
+
operationalization: "Texto do bloco de local no detalhe; null quando ausente ou não enriquecido.",
|
|
304
|
+
caveat: "Detail-only (v2).",
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: "descricao",
|
|
308
|
+
type: "string",
|
|
309
|
+
description: "Finalidade/descrição do evento.",
|
|
310
|
+
sourceEndpoint: EP_EVENTOS_DETALHE,
|
|
311
|
+
sourceField: "div.audiencia-finalidade",
|
|
312
|
+
operationalization: "Texto do bloco de finalidade no detalhe; null quando ausente ou não enriquecido.",
|
|
313
|
+
caveat: "Detail-only (v2).",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "pauta",
|
|
317
|
+
type: "object",
|
|
318
|
+
description: "Itens de pauta do evento (lista de strings).",
|
|
319
|
+
sourceEndpoint: EP_EVENTOS_DETALHE,
|
|
320
|
+
sourceField: "div.audiencia-pauta (itens)",
|
|
321
|
+
operationalization: "Bloco de pauta do detalhe, dividido em itens; [] quando ausente ou não enriquecido.",
|
|
322
|
+
caveat: "Detail-only (v2).",
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "convidados",
|
|
326
|
+
type: "object",
|
|
327
|
+
description: "Convidados do evento (nomes — agentes públicos por função).",
|
|
328
|
+
sourceEndpoint: EP_EVENTOS_DETALHE,
|
|
329
|
+
sourceField: "p.titulo-convidados > span (nomes)",
|
|
330
|
+
operationalization: "Lista de nomes dos convidados do detalhe; [] quando ausente ou não enriquecido. Nome MANTIDO (convidado é agente público na função — ver postura de privacidade).",
|
|
331
|
+
caveat: "Detail-only (v2).",
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "videoUrl",
|
|
335
|
+
type: "url",
|
|
336
|
+
description: "URL do vídeo (embed do YouTube) do evento, quando houver.",
|
|
337
|
+
sourceEndpoint: EP_EVENTOS_DETALHE,
|
|
338
|
+
sourceField: "embed YouTube (src do iframe)",
|
|
339
|
+
operationalization: "URL do embed do YouTube no detalhe; null até haver vídeo ou nas linhas não enriquecidas.",
|
|
340
|
+
caveat: "Detail-only (v2); null é o estado comum (nem todo evento tem vídeo).",
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "comentarios",
|
|
344
|
+
type: "integer",
|
|
345
|
+
unit: "comentários",
|
|
346
|
+
description: "Número CANÔNICO de comentários no evento.",
|
|
347
|
+
sourceEndpoint: EP_EVENTOS_COMENTARIOS,
|
|
348
|
+
sourceField: "nº de blocos <div class=\"comentario\"> no fragmento AJAX",
|
|
349
|
+
operationalization: "Contagem dos blocos de comentário retornados por ajaxcolecaocomentarioaudiencia (fragmento sem paginação) — a fonte canônica (estudo A3: a listagem tinha 0 espúrio em 82% dos eventos, captando só ~6,7% do engajamento). VOLÁTIL: recontado a cada ciclo de crawl. 0 nas linhas ainda não enriquecidas.",
|
|
350
|
+
caveat: "Contagem canônica via AJAX (estudo A3). Volátil (comentários acumulam) → re-crawl por ciclo. O nível-comentário está na entidade `eventos_comentarios`.",
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: "status",
|
|
354
|
+
type: "string",
|
|
355
|
+
description: "Situação do evento: \"agendado\" | \"encerrado\" | \"cancelado\".",
|
|
356
|
+
sourceEndpoint: EP_EVENTOS_LISTING,
|
|
357
|
+
sourceField: "sufixo da classe resumo-audiencia-STATUS (fallback por data)",
|
|
358
|
+
operationalization: "mapEventoStatus: CANCELADO→cancelado; REALIZADO/ENCERRADO→encerrado; AGENDADO→agendado; sem classe → por data.",
|
|
359
|
+
derived: true,
|
|
360
|
+
caveat: "Fidelidade (Recon §4.1): o sufixo REGISTRADO (sabatina \"sem data prevista\") cai em \"agendado\" — indistinguível dos agendados genuínos. Não corrigido nesta fase (é dívida de tool, não de dataset); apenas declarado.",
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: "url",
|
|
364
|
+
type: "url",
|
|
365
|
+
description: "URL canônica do evento no portal.",
|
|
366
|
+
sourceEndpoint: EP_CALC,
|
|
367
|
+
sourceField: "visualizacaoaudiencia?id={entityId}",
|
|
368
|
+
operationalization: "Construída a partir do id; não lida do upstream.",
|
|
369
|
+
derived: true,
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "firstSeenAt",
|
|
373
|
+
type: "date",
|
|
374
|
+
description: "Primeira observação do registro no corpus (proxy prospectivo de ritmo de entrada).",
|
|
375
|
+
sourceEndpoint: EP_HISTORY,
|
|
376
|
+
sourceField: "MIN(scraped_at) per entity_id",
|
|
377
|
+
operationalization: "MIN(scraped_at) sobre ecidadania_history. Observação do crawler — NÃO é data upstream.",
|
|
378
|
+
derived: true,
|
|
379
|
+
caveat: "Censura à esquerda: piso 14/06/2026; baseline de eventos = 29/06/2026 (~99,5% do corpus — primeiro crawl completo pós-ruptura do container) deve ser excluído de análises de ritmo; série interpretável a partir de 30/06/2026. Resolução = cadência do crawl de corpus completo.",
|
|
380
|
+
select: (ctx) => ctx.meta.firstSeenAt ?? null,
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
};
|
|
384
|
+
// ── eventos_comentarios (nível-comentário das audiências; tabela NOVA na v2) ──
|
|
385
|
+
// Linha = UM comentário de audiência. Fonte = fragmento AJAX ajaxcolecaocomentarioaudiencia
|
|
386
|
+
// (sem paginação). VOLÁTIL (comentários acumulam) → re-crawl por ciclo. Privacidade: conteúdo de
|
|
387
|
+
// cidadão → guarda UF + texto + timestamp; o NOME do comentarista é descartado NA ORIGEM.
|
|
388
|
+
const eventosComentariosSchema = {
|
|
389
|
+
titulo: "Comentários de audiências (nível-comentário)",
|
|
390
|
+
fonteResumo: "Fragmento AJAX `ajaxcolecaocomentarioaudiencia?audienciaId={ID}` (blocos `<div class=\"comentario\">`). Nível-comentário das audiências; sem nome do comentarista (só UF).",
|
|
391
|
+
variables: [
|
|
392
|
+
{
|
|
393
|
+
name: "eventoId",
|
|
394
|
+
type: "integer",
|
|
395
|
+
description: "Id da audiência a que o comentário pertence (FK para a entidade `eventos`).",
|
|
396
|
+
sourceEndpoint: EP_EVENTOS_COMENTARIOS,
|
|
397
|
+
sourceField: "parâmetro audienciaId da chamada AJAX",
|
|
398
|
+
operationalization: "audienciaId usado na requisição do fragmento de comentários; liga ao nível-evento.",
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
name: "comentarioId",
|
|
402
|
+
type: "integer",
|
|
403
|
+
description: "Id estável do comentário no portal.",
|
|
404
|
+
sourceEndpoint: EP_EVENTOS_COMENTARIOS,
|
|
405
|
+
sourceField: "atributo data-id do bloco <div class=\"comentario\">",
|
|
406
|
+
operationalization: "Inteiro do data-id (equivalente ao sufixo de id=\"comentario-N\").",
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: "uf",
|
|
410
|
+
type: "string",
|
|
411
|
+
description: "UF do cidadão comentarista (SEM nome — conteúdo de cidadão).",
|
|
412
|
+
sourceEndpoint: EP_EVENTOS_COMENTARIOS,
|
|
413
|
+
sourceField: "div.titulo-comentarios, parte \"(XX)\"",
|
|
414
|
+
operationalization: "Extrai só a sigla de UF entre parênteses de \"NOME (XX)\"; o NOME é descartado NA ORIGEM (nunca gravado). Postura de privacidade: conteúdo de cidadão → só UF.",
|
|
415
|
+
caveat: "Nome do comentarista deliberadamente não coletado; null quando o bloco não traz \"(UF)\".",
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: "texto",
|
|
419
|
+
type: "string",
|
|
420
|
+
description: "Texto do comentário (verbatim — conteúdo deliberativo).",
|
|
421
|
+
sourceEndpoint: EP_EVENTOS_COMENTARIOS,
|
|
422
|
+
sourceField: "div.texto-comentarios",
|
|
423
|
+
operationalization: "Texto do bloco, com tags removidas; preservado verbatim.",
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: "data",
|
|
427
|
+
type: "date",
|
|
428
|
+
description: "Data do comentário.",
|
|
429
|
+
sourceEndpoint: EP_EVENTOS_COMENTARIOS,
|
|
430
|
+
sourceField: "div.horadata-comentarios (parte DD/MM/AAAA)",
|
|
431
|
+
operationalization: "extractDate sobre \"HHhMM - DD/MM/AAAA\" → ISO YYYY-MM-DD.",
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: "hora",
|
|
435
|
+
type: "string",
|
|
436
|
+
description: "Hora do comentário (HH:MM).",
|
|
437
|
+
sourceEndpoint: EP_EVENTOS_COMENTARIOS,
|
|
438
|
+
sourceField: "div.horadata-comentarios (parte HHhMM)",
|
|
439
|
+
operationalization: "Converte \"HHhMM\" da mesma célula em HH:MM.",
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: "momentoVideoUrl",
|
|
443
|
+
type: "url",
|
|
444
|
+
description: "URL do momento do vídeo ancorado ao comentário, quando houver.",
|
|
445
|
+
sourceEndpoint: EP_EVENTOS_COMENTARIOS,
|
|
446
|
+
sourceField: "momento-comentario / momento-por-link (quando presente)",
|
|
447
|
+
operationalization: "Subconjunto: comentário ancorado a um instante do vídeo; null quando ausente.",
|
|
448
|
+
caveat: "Presente só em comentários ancorados a um momento do vídeo.",
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: "convidadoAssociado",
|
|
452
|
+
type: "string",
|
|
453
|
+
description: "Convidado a quem o comentário se dirige (nome — agente público), quando houver.",
|
|
454
|
+
sourceEndpoint: EP_EVENTOS_COMENTARIOS,
|
|
455
|
+
sourceField: "momento-convidado-nome / momento-convidado-cargo (quando presente)",
|
|
456
|
+
operationalization: "Nome/cargo do convidado associado ao momento; null quando ausente. Convidado é público (nome mantido).",
|
|
457
|
+
caveat: "Presente só quando o comentário está associado a um convidado.",
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
};
|
|
461
|
+
// ── consultas_votos (acervo histórico Arquimedes) ────────────────────────────
|
|
462
|
+
const consultasVotosSchema = {
|
|
463
|
+
titulo: "Votos históricos por UF (acervo Arquimedes)",
|
|
464
|
+
fonteResumo: "CSV Arquimedes `Proposições-com-votos.csv` (~33 MB, windows-1252). Acervo de vintage ÚNICO — não é série temporal.",
|
|
465
|
+
variables: [
|
|
466
|
+
{
|
|
467
|
+
name: "materia",
|
|
468
|
+
type: "string",
|
|
469
|
+
description: "Identificação/nome da matéria.",
|
|
470
|
+
sourceEndpoint: EP_CSV_ARQUIMEDES,
|
|
471
|
+
sourceField: "coluna \"NOME DA MATÉRIA\"",
|
|
472
|
+
operationalization: "Célula da coluna homônima; primeira linha da matéria vence (constante entre UFs).",
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: "ementa",
|
|
476
|
+
type: "string",
|
|
477
|
+
description: "Ementa da matéria.",
|
|
478
|
+
sourceEndpoint: EP_CSV_ARQUIMEDES,
|
|
479
|
+
sourceField: "coluna \"EMENTA\"",
|
|
480
|
+
operationalization: "Célula da coluna EMENTA (parser RFC-4180 tolerante a quebras de linha embutidas).",
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
name: "autoria",
|
|
484
|
+
type: "string",
|
|
485
|
+
description: "Autoria da matéria.",
|
|
486
|
+
sourceEndpoint: EP_CSV_ARQUIMEDES,
|
|
487
|
+
sourceField: "coluna \"AUTORIA\"",
|
|
488
|
+
operationalization: "Célula da coluna homônima.",
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: "status",
|
|
492
|
+
type: "string",
|
|
493
|
+
description: "Status atual segundo o CSV (uniformemente \"Descontinuado\").",
|
|
494
|
+
sourceEndpoint: EP_CSV_ARQUIMEDES,
|
|
495
|
+
sourceField: "coluna \"STATUS ATUAL\"",
|
|
496
|
+
operationalization: "Mantido verbatim; não usado como opinião atual (acervo congelado).",
|
|
497
|
+
caveat: "Uniformemente \"Descontinuado\" em todo o acervo.",
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: "votosSim",
|
|
501
|
+
type: "integer",
|
|
502
|
+
unit: "votos",
|
|
503
|
+
description: "Total de votos SIM somando todas as UFs.",
|
|
504
|
+
sourceEndpoint: EP_CSV_ARQUIMEDES,
|
|
505
|
+
sourceField: "soma da coluna \"VOTO SIM\" nas linhas matéria×UF",
|
|
506
|
+
operationalization: "Agregação: soma de VOTO SIM (parseBrNum) sobre todas as linhas da matéria.",
|
|
507
|
+
derived: true,
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: "votosNao",
|
|
511
|
+
type: "integer",
|
|
512
|
+
unit: "votos",
|
|
513
|
+
description: "Total de votos NÃO somando todas as UFs.",
|
|
514
|
+
sourceEndpoint: EP_CSV_ARQUIMEDES,
|
|
515
|
+
sourceField: "soma da coluna \"VOTO NÃO\" nas linhas matéria×UF",
|
|
516
|
+
operationalization: "Agregação: soma de VOTO NÃO (parseBrNum) sobre todas as linhas da matéria.",
|
|
517
|
+
derived: true,
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: "totalVotos",
|
|
521
|
+
type: "integer",
|
|
522
|
+
unit: "votos",
|
|
523
|
+
description: "Soma de votosSim + votosNao.",
|
|
524
|
+
sourceEndpoint: EP_CALC,
|
|
525
|
+
sourceField: "votosSim + votosNao",
|
|
526
|
+
operationalization: "Derivado em código.",
|
|
527
|
+
derived: true,
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
name: "votosPorUf",
|
|
531
|
+
type: "object",
|
|
532
|
+
description: "Detalhamento de votos SIM/NÃO por UF do cidadão (diferencial regional do acervo).",
|
|
533
|
+
sourceEndpoint: EP_CSV_ARQUIMEDES,
|
|
534
|
+
sourceField: "agregação por coluna \"UF DO CIDADÃO\" de \"VOTO SIM\"/\"VOTO NÃO\"",
|
|
535
|
+
operationalization: "{ UF: { sim, nao } } com chaves de UF ordenadas (JSON determinístico).",
|
|
536
|
+
derived: true,
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
name: "url",
|
|
540
|
+
type: "url",
|
|
541
|
+
description: "URL canônica da matéria no portal.",
|
|
542
|
+
sourceEndpoint: EP_CALC,
|
|
543
|
+
sourceField: "visualizacaomateria?id={entityId}",
|
|
544
|
+
operationalization: "Construída a partir do codigoMateria; não lida do upstream.",
|
|
545
|
+
derived: true,
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
name: "referencePeriod",
|
|
549
|
+
type: "date",
|
|
550
|
+
description: "Vintage do acervo: data do carimbo \"dados atualizados até\" do CSV.",
|
|
551
|
+
sourceEndpoint: EP_CSV_ARQUIMEDES,
|
|
552
|
+
sourceField: "linha 1 do CSV (\"Dados atualizados até DD/MM/AAAA\")",
|
|
553
|
+
operationalization: "extractDate sobre o carimbo → ISO. É o ÚNICO campo temporal do acervo.",
|
|
554
|
+
caveat: "Acervo de vintage único (profundidade de série = 1): não há série temporal; este é o único carimbo de data.",
|
|
555
|
+
select: (ctx) => ctx.meta.referencePeriod ?? null,
|
|
556
|
+
},
|
|
557
|
+
],
|
|
558
|
+
};
|
|
559
|
+
export const ENTITY_SCHEMAS = {
|
|
560
|
+
consultas: consultasSchema,
|
|
561
|
+
ideias: ideiasSchema,
|
|
562
|
+
eventos: eventosSchema,
|
|
563
|
+
eventos_comentarios: eventosComentariosSchema,
|
|
564
|
+
consultas_votos: consultasVotosSchema,
|
|
565
|
+
};
|
|
566
|
+
/** Resolve o valor de uma variável a partir do payload/meta (default: payload[name]). */
|
|
567
|
+
export function selectValue(v, ctx) {
|
|
568
|
+
if (v.select)
|
|
569
|
+
return v.select(ctx);
|
|
570
|
+
const val = ctx.payload[v.name];
|
|
571
|
+
return val === undefined ? null : val;
|
|
572
|
+
}
|