senado-br-mcp 3.18.5 → 3.19.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 CHANGED
@@ -565,7 +565,7 @@ Fields of the `concise` block (contract v1.3 since 3.18.5; per response — one
565
565
  | `citation` | Ready-to-use citation string (human-readable) |
566
566
  | `license` | Source terms (Dados Abertos do Senado Federal) |
567
567
  | `field_sources` | *Optional.* Per-field sub-sources, each with its own extraction instant — only on responses that merge reads (v1.2, since 3.17.0; see below) |
568
- | `derived` / `derivation_note` | *Optional.* `true` + a fixed note when the server computed the values — the `estatisticas` modes (distribution, aggregates, rankings) of `senado_remuneracoes_servidores`, `senado_horas_extras`, `senado_ceaps`, `senado_execucao_orcamentaria` and `senado_suprimento_fundos`; the individual values they cite remain the Senate's own |
568
+ | `derived` / `derivation_note` | *Optional.* `true` + a fixed pt-BR note naming the calculation whenever the server computed values instead of copying them (since 3.19.0, the whole class): the `estatisticas` modes (distribution, aggregates, rankings); group sums, counts and averages — CEAPS `valorTotal` and its per-senator/type/month/supplier modes, the budget execution aggregates (`por-ano`, the default, included), the payroll `resumo`, the overtime `valorTotal`, the `relatoria` distribution, seated senators per party/UF, the historical e-Cidadania national totals; the payroll gross (`bruto`, sum of seven components); servants assigned to an org subtree (`subordinadasA`); a vote tally counted from nominal votes when the source publishes none (plenary open roll calls, committee votes without totals); e-Cidadania percentages, consensus/polarisation rankings and the estimated idea creation date; the current legislature's period. Absent when every number is the source's own (raw lists, `detalhe` of the budget, published tallies). The individual values cited remain the Senate's |
569
569
  | `revision` | Whether the figure may still change at the source: `status` is `current` for every Senate source (the version in force at extraction; none is claimed `final`), and `note` says, for the administrative and budget sources, that the Senate may correct published data later |
570
570
 
571
571
  No Senate source publishes notices alongside the data, so `notices` (also part of v1.3) does not appear. The text footer gains a line only for exceptions — here, the computed-values line on the `estatisticas` modes.
@@ -17,7 +17,7 @@ import { upstreamFetch, UpstreamError } from "../throttle/upstream.js";
17
17
  import { toolError, errorFrom, ensureArray, safeInt, toBool, normalizeText } from "../utils/validation.js";
18
18
  import { digArrayRoot } from "../utils/upstream-parse.js";
19
19
  import { escolherReuniao, ehReuniaoInexistente, mensagemCodigoInexistente } from "./resolver-reuniao.js";
20
- import { leituraDoCache, provenanceFor, resultWithProvenance } from "../utils/provenance.js";
20
+ import { NOTA_DERIVACAO_SOMAS, derivadoSe, leituraDoCache, provenanceFor, resultWithProvenance } from "../utils/provenance.js";
21
21
  import { CACHE_SEMI_STATIC, CACHE_DYNAMIC, CACHE_ON_DEMAND } from "../types.js";
22
22
  export function formatDateYMD(d) {
23
23
  return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}${String(d.getDate()).padStart(2, "0")}`;
@@ -154,6 +154,12 @@ export function provenanceReunioesComissao(baseUrl, sigla, di, df, partes) {
154
154
  : {}),
155
155
  });
156
156
  }
157
+ /**
158
+ * `senado_distribuicao_materias` (contract v1.3): in `relatoria` the source sends one row per
159
+ * (parlamentar, unidade) and the server sums `Quantidade` per parlamentar — derived; in `autoria`
160
+ * each count is the source's own (ordering only).
161
+ */
162
+ export const provenanceDaDistribuicao = (prov, tipo) => derivadoSe(prov, tipo === "relatoria", NOTA_DERIVACAO_SOMAS);
157
163
  export function registerComissoesTools(server, baseUrl) {
158
164
  // E1. senado_listar_comissoes
159
165
  server.tool("senado_listar_comissoes", "Lista comissões (colegiados) ativas do Senado, com filtros por `tipo` (permanente, temporaria, cpi, mista) e `ativa`. Retorna `{ count, comissoes }`, cada item com `codigo`, `sigla`, `nome`, `tipo`, `casa` e `ativa`. O endpoint só traz comissões ativas, logo `ativa=false` resulta em lista vazia. Use para descobrir a `sigla` exigida por `senado_obter_comissao` e `senado_reunioes_comissao`.", {
@@ -554,7 +560,7 @@ export function registerComissoesTools(server, baseUrl) {
554
560
  tipo,
555
561
  count: parlamentares.length,
556
562
  parlamentares,
557
- }, prov);
563
+ }, provenanceDaDistribuicao(prov, tipo));
558
564
  }
559
565
  catch (e) {
560
566
  return errorFrom(e, "Erro ao obter distribuição de matérias");
@@ -12,7 +12,7 @@
12
12
  import { z } from "zod";
13
13
  import { cachedFetchWithMeta } from "../cache/manager.js";
14
14
  import { errorFrom } from "../utils/validation.js";
15
- import { leituraDoCache, provenanceEcidadania, provenanceArquimedesVotos, resultWithProvenance, withFieldSources, } from "../utils/provenance.js";
15
+ import { leituraDoCache, provenanceEcidadania, NOTA_DERIVACAO_DATA_ESTIMADA, NOTA_DERIVACAO_PERCENTUAIS, NOTA_DERIVACAO_RANKING_ECIDADANIA, NOTA_DERIVACAO_SOMAS, derivado, derivadoSe, provenanceArquimedesVotos, resultWithProvenance, withFieldSources, } from "../utils/provenance.js";
16
16
  import { tagUntrustedFields, tagUntrustedList, neutralizeUntrustedText, sanitizeScrapedText } from "../utils/untrusted.js";
17
17
  import { CACHE_ON_DEMAND } from "../types.js";
18
18
  import { resolveList, writeDetalheThrough } from "../scraper/store.js";
@@ -90,6 +90,18 @@ export function provenanceEventoDetalhe(item, id, leitura, corpusMeta) {
90
90
  subFonteDoCorpus("/principalaudiencia", "eventos", corpusMeta, ["comentarios"]),
91
91
  ]);
92
92
  }
93
+ /**
94
+ * Contract v1.3 `derived` on e-Cidadania responses. Every consulta carries percentages the server
95
+ * computes (`buildConsultaResumo`), and `totalVotos` too when the source sends no total; the
96
+ * analysis and the poll suggestion add their own polarisation/consensus metrics and ranking on top;
97
+ * an idea's creation date is an estimate; the historical archive's national totals are summed from
98
+ * the per-UF rows of the CSV.
99
+ */
100
+ export const NOTA_DERIVACAO_ANALISE_CONSULTAS = `${NOTA_DERIVACAO_RANKING_ECIDADANIA} ${NOTA_DERIVACAO_PERCENTUAIS}`;
101
+ export const provenanceDasConsultas = (prov) => derivado(prov, NOTA_DERIVACAO_PERCENTUAIS);
102
+ export const provenanceDaAnaliseDeConsultas = (prov) => derivado(prov, NOTA_DERIVACAO_ANALISE_CONSULTAS);
103
+ export const provenanceDasIdeias = (prov, ideias) => derivadoSe(prov, ideias.some((i) => i.dataCriacaoEstimada != null), NOTA_DERIVACAO_DATA_ESTIMADA);
104
+ export const provenanceDosVotosHistoricos = (prov) => derivado(prov, NOTA_DERIVACAO_SOMAS);
93
105
  /**
94
106
  * Proveniência de `senado_ecidadania_sugerir_tema_enquete`: as sugestões cruzam DOIS corpora
95
107
  * do D1 (consultas e ideias), cada um com o seu `lastScrapedAt`. Até a 3.18.1 o topo levava só
@@ -103,10 +115,10 @@ export function provenanceSugestaoEnquete(consultasMeta, ideiasMeta) {
103
115
  dataset_id: "consultas+ideias",
104
116
  retrieved_at: typeof ts === "string" && ts ? ts : undefined,
105
117
  });
106
- return withFieldSources(prov, [
118
+ return derivado(withFieldSources(prov, [
107
119
  subFonteDoCorpus("/principalmateria", "consultas", consultasMeta, campos),
108
120
  subFonteDoCorpus("/principalideia", "ideias", ideiasMeta, campos),
109
- ]);
121
+ ]), NOTA_DERIVACAO_ANALISE_CONSULTAS);
110
122
  }
111
123
  // ══════════════════════════════════════════════════════════════════════════
112
124
  // Tool registration
@@ -158,7 +170,7 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
158
170
  const limite = params.limite ?? 20;
159
171
  const offset = ((params.pagina ?? 1) - 1) * limite;
160
172
  const out = filtered.slice(offset, offset + limite);
161
- return resultWithProvenance({ count: out.length, consultas: tagUntrustedList("consultas", out), meta }, provLista("/principalmateria", "consultas", meta));
173
+ return resultWithProvenance({ count: out.length, consultas: tagUntrustedList("consultas", out), meta }, provenanceDasConsultas(provLista("/principalmateria", "consultas", meta)));
162
174
  }
163
175
  catch (e) {
164
176
  return ecidadaniaError(e);
@@ -170,7 +182,7 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
170
182
  const { value: r, fetchedAt } = await cachedFetchWithMeta("ecidadania_consulta", { id: params.id }, CACHE_ON_DEMAND, () => obterConsultaInternal(params.id));
171
183
  writeDetalheThrough(db, ctx, "consultas", params.id, r);
172
184
  const detalhe = buildConsultaDetalheResult(r);
173
- return resultWithProvenance(tagUntrustedFields("consultas", detalhe), provDetalhe(r, `/visualizacaomateria?id=${params.id}`, `consulta=${params.id}`, fetchedAt));
185
+ return resultWithProvenance(tagUntrustedFields("consultas", detalhe), provenanceDasConsultas(provDetalhe(r, `/visualizacaomateria?id=${params.id}`, `consulta=${params.id}`, fetchedAt)));
174
186
  }
175
187
  catch (e) {
176
188
  return ecidadaniaError(e);
@@ -216,7 +228,7 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
216
228
  .slice(0, limite);
217
229
  criterio = `≥${percentualMinimo}% numa direção, mínimo ${minimoVotos} votos`;
218
230
  }
219
- return resultWithProvenance({ modo, criterio, count: filtered.length, consultas: tagUntrustedList("consultas", filtered), meta }, provLista("/principalmateria", "consultas", meta));
231
+ return resultWithProvenance({ modo, criterio, count: filtered.length, consultas: tagUntrustedList("consultas", filtered), meta }, provenanceDaAnaliseDeConsultas(provLista("/principalmateria", "consultas", meta)));
220
232
  }
221
233
  catch (e) {
222
234
  return ecidadaniaError(e);
@@ -248,7 +260,7 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
248
260
  ideias: tagUntrustedList("ideias", ideias),
249
261
  avisoDatas: AVISO_DATAS_IDEIA,
250
262
  meta,
251
- }, provLista("/principalideia", "ideias", meta));
263
+ }, provenanceDasIdeias(provLista("/principalideia", "ideias", meta), ideias));
252
264
  }
253
265
  catch (e) {
254
266
  return ecidadaniaError(e);
@@ -269,7 +281,7 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
269
281
  comentarios: null,
270
282
  avisoDatas: AVISO_DATAS_IDEIA,
271
283
  };
272
- return resultWithProvenance(tagUntrustedFields("ideias", detalhe), provDetalhe(r, `/visualizacaoideia?id=${params.id}`, `ideia=${params.id}`, fetchedAt));
284
+ return resultWithProvenance(tagUntrustedFields("ideias", detalhe), provenanceDasIdeias(provDetalhe(r, `/visualizacaoideia?id=${params.id}`, `ideia=${params.id}`, fetchedAt), [detalhe]));
273
285
  }
274
286
  catch (e) {
275
287
  return ecidadaniaError(e);
@@ -474,10 +486,10 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
474
486
  if (out.length === 0 && (meta.fonte === "ao-vivo" || meta.motivo === "d1-vazio" || meta.motivo === "d1-indisponivel")) {
475
487
  payload.aviso = "Acervo de votos ainda não ingerido (primeira carga semanal pendente) ou filtro sem correspondência.";
476
488
  }
477
- return resultWithProvenance(payload, provenanceArquimedesVotos({
489
+ return resultWithProvenance(payload, provenanceDosVotosHistoricos(provenanceArquimedesVotos({
478
490
  reference_period: referencePeriod ?? undefined,
479
491
  retrieved_at: typeof meta.lastScrapedAt === "string" && meta.lastScrapedAt ? meta.lastScrapedAt : undefined,
480
- }));
492
+ })));
481
493
  }
482
494
  catch (e) {
483
495
  return ecidadaniaError(e);
@@ -13,7 +13,7 @@ import { z } from "zod";
13
13
  import { cachedFetchWithMeta } from "../cache/manager.js";
14
14
  import { upstreamFetch } from "../throttle/upstream.js";
15
15
  import { errorFrom, ensureArray, safeInt, parseBRL } from "../utils/validation.js";
16
- import { derivadoDeEstatisticas, provenanceFor, resultWithProvenance } from "../utils/provenance.js";
16
+ import { NOTA_DERIVACAO_SOMAS, derivadoDeEstatisticas, derivadoSe, provenanceFor, resultWithProvenance } from "../utils/provenance.js";
17
17
  import { computarEstatisticas, arredondarEstatisticas, arredondarEntradas, } from "../utils/estatisticas.js";
18
18
  import { CACHE_STATIC } from "../types.js";
19
19
  const FINANCEIRO_BASE = "https://www.senado.gov.br";
@@ -80,6 +80,8 @@ export function parseReceita(r) {
80
80
  };
81
81
  }
82
82
  /** Aggregate normalized despesas by a key, summing the five value columns. */
83
+ /** Todo modo fora de `detalhe` soma por grupo (por-ano, o padrão, também): é cálculo do servidor. */
84
+ export const provenanceDaExecucao = (prov, modo) => derivadoSe(prov, modo !== "detalhe", NOTA_DERIVACAO_SOMAS);
83
85
  export function agregarDespesas(itens, chave) {
84
86
  const grupos = new Map();
85
87
  for (const d of itens) {
@@ -272,7 +274,7 @@ export function registerOrcamentoSenadoTools(server) {
272
274
  agregado = agregado.sort((a, b) => String(a.chave).localeCompare(String(b.chave)));
273
275
  resultado = { agregado: agregado.slice(0, limite) };
274
276
  }
275
- return resultWithProvenance({ tipo, modo, ano: params.ano ?? null, totalLinhas: itens.length, ...resultado }, prov);
277
+ return resultWithProvenance({ tipo, modo, ano: params.ano ?? null, totalLinhas: itens.length, ...resultado }, provenanceDaExecucao(prov, modo));
276
278
  }
277
279
  // receitas
278
280
  let itens = ensureArray(bruto?.receitas).map(parseReceita);
@@ -303,7 +305,7 @@ export function registerOrcamentoSenadoTools(server) {
303
305
  : agregado.sort((a, b) => a.chave.localeCompare(b.chave));
304
306
  resultado = { agregado: agregado.slice(0, limite) };
305
307
  }
306
- return resultWithProvenance({ tipo, modo, ano: params.ano ?? null, totalLinhas: itens.length, ...resultado }, prov);
308
+ return resultWithProvenance({ tipo, modo, ano: params.ano ?? null, totalLinhas: itens.length, ...resultado }, provenanceDaExecucao(prov, modo));
307
309
  }
308
310
  catch (e) {
309
311
  return errorFrom(e, "Erro ao consultar execução orçamentária");
@@ -9,7 +9,7 @@ import { cachedFetchWithMeta } from "../cache/manager.js";
9
9
  import { upstreamFetch } from "../throttle/upstream.js";
10
10
  import { errorFrom, ensureArray } from "../utils/validation.js";
11
11
  import { digArrayRoot } from "../utils/upstream-parse.js";
12
- import { provenanceFor, resultWithProvenance } from "../utils/provenance.js";
12
+ import { NOTA_DERIVACAO_LEGISLATURA, NOTA_DERIVACAO_LEGISLATURA_PADRAO, NOTA_DERIVACAO_SOMAS, derivado, provenanceFor, resultWithProvenance } from "../utils/provenance.js";
13
13
  import { CACHE_STATIC } from "../types.js";
14
14
  export const TIPOS_MATERIA = [
15
15
  { sigla: "PEC", nome: "Proposta de Emenda à Constituição", descricao: "Altera a Constituição Federal" },
@@ -64,6 +64,15 @@ export function deriveLegislaturaAtual(parlamentares) {
64
64
  // Fallback to the 57th legislatura (2023-2027)
65
65
  return { numero: 57, periodo: "2023-2027", dataInicio: "2023-02-01", dataFim: "2027-01-31" };
66
66
  }
67
+ /**
68
+ * `tabela=legislatura-atual` (contract v1.3): period and dates are always the server's arithmetic;
69
+ * when the source sends no legislatura number the number itself is the server's fallback, and the
70
+ * note says so.
71
+ */
72
+ export function provenanceDaLegislatura(prov, parlamentares) {
73
+ const doFonte = Boolean(parlamentares[0]?.Mandato?.PrimeiraLegislaturaDoMandato?.NumeroLegislatura);
74
+ return derivado(prov, doFonte ? NOTA_DERIVACAO_LEGISLATURA : NOTA_DERIVACAO_LEGISLATURA_PADRAO);
75
+ }
67
76
  /** Tally seated senators per party (sigla → count). */
68
77
  export function tabularPartidos(parlamentares) {
69
78
  const counts = {};
@@ -125,7 +134,7 @@ export function registerReferenciaTools(server, baseUrl) {
125
134
  const prov = provenanceFor("SENADO_LEGIS", baseUrl, "/senador/lista/atual", {
126
135
  dataset_id: "tabela=partidos", retrieved_at: fetchedAt,
127
136
  });
128
- return resultWithProvenance({ tabela: params.tabela, count: partidos.length, totalSenadores: parlamentares.length, partidos }, prov);
137
+ return resultWithProvenance({ tabela: params.tabela, count: partidos.length, totalSenadores: parlamentares.length, partidos }, derivado(prov, NOTA_DERIVACAO_SOMAS));
129
138
  }
130
139
  case "ufs": {
131
140
  const { value, fetchedAt } = await fetchSenadoresAtuais(baseUrl);
@@ -134,7 +143,7 @@ export function registerReferenciaTools(server, baseUrl) {
134
143
  const prov = provenanceFor("SENADO_LEGIS", baseUrl, "/senador/lista/atual", {
135
144
  dataset_id: "tabela=ufs", retrieved_at: fetchedAt,
136
145
  });
137
- return resultWithProvenance({ tabela: params.tabela, count: ufs.length, totalSenadores: parlamentares.length, ufs }, prov);
146
+ return resultWithProvenance({ tabela: params.tabela, count: ufs.length, totalSenadores: parlamentares.length, ufs }, derivado(prov, NOTA_DERIVACAO_SOMAS));
138
147
  }
139
148
  case "legislatura-atual": {
140
149
  const { value, fetchedAt } = await fetchSenadoresAtuais(baseUrl);
@@ -142,7 +151,7 @@ export function registerReferenciaTools(server, baseUrl) {
142
151
  const prov = provenanceFor("SENADO_LEGIS", baseUrl, "/senador/lista/atual", {
143
152
  dataset_id: "tabela=legislatura-atual", retrieved_at: fetchedAt,
144
153
  });
145
- return resultWithProvenance({ tabela: params.tabela, ...deriveLegislaturaAtual(parlamentares) }, prov);
154
+ return resultWithProvenance({ tabela: params.tabela, ...deriveLegislaturaAtual(parlamentares) }, provenanceDaLegislatura(prov, parlamentares));
146
155
  }
147
156
  case "tipos-norma": {
148
157
  const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_tipos_norma", {}, CACHE_STATIC, () => upstreamFetch("/legislacao/tiposNorma", {}, baseUrl));
@@ -12,7 +12,7 @@ import { cachedFetchWithMeta } from "../cache/manager.js";
12
12
  import { admFetch, admFetchLarge } from "../throttle/adm.js";
13
13
  import { errorFrom, ensureArray, normalizeText, parseBRL } from "../utils/validation.js";
14
14
  import { unwrapAdmEnvelope } from "../utils/upstream-parse.js";
15
- import { derivadoDeEstatisticas, provenanceFor, resultWithProvenance } from "../utils/provenance.js";
15
+ import { NOTA_DERIVACAO_SOMAS, derivado, derivadoDeEstatisticas, provenanceFor, resultWithProvenance } from "../utils/provenance.js";
16
16
  import { computarEstatisticas, arredondarEstatisticas, arredondarEntradas, } from "../utils/estatisticas.js";
17
17
  import { CACHE_SEMI_STATIC, CACHE_STATIC } from "../types.js";
18
18
  const norm = normalizeText;
@@ -40,6 +40,8 @@ export function filtrarCeaps(itens, f) {
40
40
  });
41
41
  }
42
42
  /** Aggregate CEAPS items by a key extractor. */
43
+ /** Toda resposta fora de `estatisticas` leva `valorTotal` (soma do servidor), em qualquer modo. */
44
+ export const provenanceDoCeaps = (prov) => derivado(prov, NOTA_DERIVACAO_SOMAS);
43
45
  export function agregarCeaps(itens, chave, extra) {
44
46
  const grupos = new Map();
45
47
  for (const d of itens) {
@@ -186,7 +188,7 @@ export function registerSenadoresAdminTools(server, admBaseUrl) {
186
188
  totalDespesas: filtrado.length,
187
189
  valorTotal: totalGeral,
188
190
  ...resultado,
189
- }, prov);
191
+ }, provenanceDoCeaps(prov));
190
192
  }
191
193
  catch (e) {
192
194
  return errorFrom(e, "Erro ao consultar despesas CEAPS");
@@ -12,7 +12,7 @@ import { cachedFetchWithMeta } from "../cache/manager.js";
12
12
  import { admFetch, admFetchLarge } from "../throttle/adm.js";
13
13
  import { errorFrom, ensureArray, parseBRL } from "../utils/validation.js";
14
14
  import { unwrapAdmEnvelope } from "../utils/upstream-parse.js";
15
- import { derivadoDeEstatisticas, provenanceFor, resultWithProvenance } from "../utils/provenance.js";
15
+ import { NOTA_DERIVACAO_BRUTO, NOTA_DERIVACAO_SOMAS, NOTA_DERIVACAO_SUBORDINADAS, derivado, derivadoDeEstatisticas, provenanceFor, resultWithProvenance } from "../utils/provenance.js";
16
16
  import { computarEstatisticas, arredondarEstatisticas, arredondarEntradas, } from "../utils/estatisticas.js";
17
17
  import { CACHE_SEMI_STATIC, CACHE_STATIC } from "../types.js";
18
18
  import { matchesFiltro, matchesFiltroCampo } from "./contratacoes.js";
@@ -55,6 +55,16 @@ export function resumoRemuneracao(r) {
55
55
  num(r.gratificacao_natalina) + num(r.horas_extras) + num(r.outras_eventuais) + num(r.abono_permanencia)) * 100) / 100,
56
56
  };
57
57
  }
58
+ /**
59
+ * Provenance of the non-statistics remuneration modes (contract v1.3): `detalhe` carries the
60
+ * server-summed `bruto` per row; `resumo` (the default) sums and averages that `bruto` per
61
+ * `tipoFolha`, so its note names both calculations.
62
+ */
63
+ export const provenanceDaRemuneracao = (prov, modo) => modo === "detalhe"
64
+ ? derivado(prov, NOTA_DERIVACAO_BRUTO)
65
+ : derivado(prov, `${NOTA_DERIVACAO_SOMAS} ${NOTA_DERIVACAO_BRUTO}`);
66
+ /** Overtime outside `estatisticas`: `valorTotal` at the top is the server's sum of the rows. */
67
+ export const provenanceDasHorasExtras = (prov) => derivado(prov, NOTA_DERIVACAO_SOMAS);
58
68
  /** The 7 columns that sum into `bruto` (identical composition to `resumoRemuneracao`). */
59
69
  const COMPONENTES_BRUTO = [
60
70
  "remuneracaoBasica", "vantagensPessoais", "funcaoComissionada", "gratificacaoNatalina",
@@ -348,7 +358,7 @@ export function particionarPorUnidade(lista, indice, codUnidade) {
348
358
  * busca, então a sub-fonte dele não diz (`served_from_cache: null`).
349
359
  */
350
360
  export function provenanceServidoresSubordinadas(prov, situacao, folhaDoCache) {
351
- return withFieldSources(prov, [
361
+ return derivado(withFieldSources(prov, [
352
362
  {
353
363
  fields: ["servidores", "total", "count", "naoClassificados", "afastadosOuEmTransito"],
354
364
  source_url: prov.source_url,
@@ -363,7 +373,7 @@ export function provenanceServidoresSubordinadas(prov, situacao, folhaDoCache) {
363
373
  reference_period: ESTRUTURA_VINTAGE.slice(0, 10),
364
374
  retrieved_at: ESTRUTURA_VINTAGE,
365
375
  },
366
- ]);
376
+ ]), NOTA_DERIVACAO_SUBORDINADAS);
367
377
  }
368
378
  export function registerServidoresTools(server, admBaseUrl) {
369
379
  // P1. senado_servidores
@@ -484,7 +494,7 @@ export function registerServidoresTools(server, admBaseUrl) {
484
494
  total: itens.length,
485
495
  ...(itens.length > limite ? { aviso: `Exibindo ${limite} de ${itens.length} registros. Filtre por nome ou use modo=resumo.` } : {}),
486
496
  remuneracoes: itens.slice(0, limite).map(resumoRemuneracao),
487
- }, prov);
497
+ }, provenanceDaRemuneracao(prov, "detalhe"));
488
498
  }
489
499
  const porFolha = new Map();
490
500
  for (const r of itens) {
@@ -503,7 +513,7 @@ export function registerServidoresTools(server, admBaseUrl) {
503
513
  mediaBruta: Math.round((g.totalBruto / g.registros) * 100) / 100,
504
514
  }))
505
515
  .sort((a, b) => b.totalBruto - a.totalBruto);
506
- return resultWithProvenance({ ano: params.ano, mes: params.mes, totalRegistros: itens.length, resumo }, prov);
516
+ return resultWithProvenance({ ano: params.ano, mes: params.mes, totalRegistros: itens.length, resumo }, provenanceDaRemuneracao(prov, "resumo"));
507
517
  }
508
518
  catch (e) {
509
519
  return errorFrom(e, "Erro ao consultar remunerações");
@@ -548,7 +558,7 @@ export function registerServidoresTools(server, admBaseUrl) {
548
558
  total: itens.length,
549
559
  valorTotal,
550
560
  horasExtras: itens.slice(0, limite),
551
- }, prov);
561
+ }, provenanceDasHorasExtras(prov));
552
562
  }
553
563
  catch (e) {
554
564
  return errorFrom(e, "Erro ao consultar horas extras");
@@ -7,14 +7,14 @@ import { cachedFetchWithMeta } from "../cache/manager.js";
7
7
  import { upstreamFetch } from "../throttle/upstream.js";
8
8
  import { toolError, errorFrom, buildParams, ensureArray, normalizeText } from "../utils/validation.js";
9
9
  import { digArrayRoot } from "../utils/upstream-parse.js";
10
- import { provenanceFor, resultWithProvenance } from "../utils/provenance.js";
10
+ import { NOTA_DERIVACAO_PLACAR_COMISSAO, derivadoSe, provenanceFor, resultWithProvenance } from "../utils/provenance.js";
11
11
  import { CACHE_DYNAMIC } from "../types.js";
12
12
  const VOTACAO_COMISSAO_ROOT = [["VotacoesComissao", "Votacoes", "Votacao"]];
13
13
  /** Parse a committee vote item (VotacoesComissao.Votacoes.Votacao[]). */
14
14
  export function parseVotacaoComissao(v) {
15
15
  const votacao = v.Votacao || v;
16
16
  const votosRaw = ensureArray(votacao.Votos?.Voto ?? votacao.votos);
17
- let totalSim = 0, totalNao = 0, totalAbstencao = 0;
17
+ let totalSim = 0, totalNao = 0, totalAbstencao = 0; // recontagem, usada só se a fonte não publicar
18
18
  const votos = votosRaw.map((vt) => {
19
19
  const q = normalizeText(vt.QualidadeVoto || vt.DescricaoVoto || vt.voto);
20
20
  if (q === "s" || q === "sim")
@@ -37,10 +37,35 @@ export function parseVotacaoComissao(v) {
37
37
  reuniao: votacao.CodigoReuniao || null,
38
38
  materia: votacao.IdentificacaoMateria || votacao.DescricaoIdentificacaoMateria || null,
39
39
  descricao: votacao.DescricaoVotacao || votacao.descricaoVotacao || null,
40
- totalSim, totalNao, totalAbstencao,
40
+ ...placarDaComissao(votacao, { sim: totalSim, nao: totalNao, abstencao: totalAbstencao }),
41
41
  votos,
42
42
  };
43
43
  }
44
+ const totalPublicado = (v) => {
45
+ if (v === undefined || v === null || v === "")
46
+ return null;
47
+ const n = Number(v);
48
+ return Number.isFinite(n) ? n : null;
49
+ };
50
+ /**
51
+ * Committee tally: the source's `TotalVotosSim/Nao/Abstencao` when it publishes them (it does on
52
+ * every vote measured — 395 of 395 at the CCJ on 15/10/2026), the recount of the nominal votes
53
+ * only when all three are missing, then flagged `placarComputado`. Until 3.18.5 the parser always
54
+ * recounted, and 5 of those 395 came out different from the published tally: the source repeats
55
+ * the nominal list in some votes (24/25 counted against 12/13 published) and counts an "A" vote
56
+ * as no abstention in others. The published number is the source's own; the recount is not.
57
+ */
58
+ export function placarDaComissao(votacao, recontagem) {
59
+ const sim = totalPublicado(votacao?.TotalVotosSim);
60
+ const nao = totalPublicado(votacao?.TotalVotosNao);
61
+ const abstencao = totalPublicado(votacao?.TotalVotosAbstencao);
62
+ if (sim !== null || nao !== null || abstencao !== null) {
63
+ return { totalSim: sim ?? 0, totalNao: nao ?? 0, totalAbstencao: abstencao ?? 0 };
64
+ }
65
+ return { totalSim: recontagem.sim, totalNao: recontagem.nao, totalAbstencao: recontagem.abstencao, placarComputado: true };
66
+ }
67
+ /** `derived` (contract v1.3) only when some item's tally had to be counted from the votes. */
68
+ export const provenanceDaVotacaoComissao = (prov, votacoes) => derivadoSe(prov, votacoes.some((v) => v.placarComputado === true), NOTA_DERIVACAO_PLACAR_COMISSAO);
44
69
  /**
45
70
  * Filter parsed committee votes locally by reunion date. The upstream serves the full
46
71
  * history regardless of the date query params, so the window is enforced in-Worker.
@@ -66,7 +91,7 @@ export function registerVotacaoComissaoTools(server, baseUrl) {
66
91
  "`por: comissao` → exige `siglaComissao`; lista as votações daquela comissão. " +
67
92
  "`por: senador` → exige `codigoSenador`; lista os votos do senador em comissões (filtro opcional `comissao`). " +
68
93
  "`por: materia` → exige `sigla`, `numero` e `ano` (ex.: PL 2630/2020); lista as votações da proposição em comissões (filtro opcional `comissao`). " +
69
- "Em todos os casos aceita período opcional `dataInicio`/`dataFim` (YYYYMMDD, filtrado pela data da reunião) e retorna `{ por, ...contexto, count, votacoes }`, cada votação com `codigo`, `data`, `comissao`, `reuniao`, `materia`, `descricao`, totais computados dos votos (`totalSim`/`totalNao`/`totalAbstencao`) e `votos` (senador, partido, voto). Sem paginação. Períodos amplos podem estourar o tempo-limite da consulta (erro de timeout): prefira janelas de até 3 meses e divida períodos maiores em chamadas sucessivas. " +
94
+ "Em todos os casos aceita período opcional `dataInicio`/`dataFim` (YYYYMMDD, filtrado pela data da reunião) e retorna `{ por, ...contexto, count, votacoes }`, cada votação com `codigo`, `data`, `comissao`, `reuniao`, `materia`, `descricao`, totais publicados pela fonte (`totalSim`/`totalNao`/`totalAbstencao`; contados dos votos, com `placarComputado: true`, só quando a fonte não os traz) e `votos` (senador, partido, voto). Sem paginação. Períodos amplos podem estourar o tempo-limite da consulta (erro de timeout): prefira janelas de até 3 meses e divida períodos maiores em chamadas sucessivas. " +
70
95
  "Obtenha siglas via `senado_listar_comissoes`, `codigoSenador` via `senado_listar_senadores`; para votações no plenário use `senado_votos_materia`. Atenção: o `codigo` de cada votação de comissão pertence a um espaço de numeração próprio e NÃO é válido em `senado_obter_votacao` (que é exclusivo de plenário) — podem coincidir numericamente, mas apontam para votações diferentes.", {
71
96
  por: z.enum(["comissao", "senador", "materia"]).optional().default("comissao").describe("Eixo da consulta: comissao, senador ou materia"),
72
97
  siglaComissao: z.string().min(2).optional().describe("Sigla da comissão (obrigatório quando por=comissao; ex: CCJ, CAE)"),
@@ -97,7 +122,7 @@ export function registerVotacaoComissaoTools(server, baseUrl) {
97
122
  ? `${params.dataInicio}/${params.dataFim}` : undefined,
98
123
  retrieved_at: fetchedAt,
99
124
  });
100
- return resultWithProvenance({ por, codigoSenador: params.codigoSenador, count: votacoes.length, votacoes }, prov);
125
+ return resultWithProvenance({ por, codigoSenador: params.codigoSenador, count: votacoes.length, votacoes }, provenanceDaVotacaoComissao(prov, votacoes));
101
126
  }
102
127
  if (por === "materia") {
103
128
  if (!params.sigla || !params.numero || !params.ano) {
@@ -117,7 +142,7 @@ export function registerVotacaoComissaoTools(server, baseUrl) {
117
142
  reference_period: String(params.ano),
118
143
  retrieved_at: fetchedAt,
119
144
  });
120
- return resultWithProvenance({ por, materia: `${sigla} ${params.numero}/${params.ano}`, count: votacoes.length, votacoes }, prov);
145
+ return resultWithProvenance({ por, materia: `${sigla} ${params.numero}/${params.ano}`, count: votacoes.length, votacoes }, provenanceDaVotacaoComissao(prov, votacoes));
121
146
  }
122
147
  // por === "comissao" (padrão)
123
148
  if (!params.siglaComissao)
@@ -136,7 +161,7 @@ export function registerVotacaoComissaoTools(server, baseUrl) {
136
161
  ? `${params.dataInicio}/${params.dataFim}` : undefined,
137
162
  retrieved_at: fetchedAt,
138
163
  });
139
- return resultWithProvenance({ por, siglaComissao: sigla, count: votacoes.length, votacoes }, prov);
164
+ return resultWithProvenance({ por, siglaComissao: sigla, count: votacoes.length, votacoes }, provenanceDaVotacaoComissao(prov, votacoes));
140
165
  }
141
166
  catch (e) {
142
167
  return errorFrom(e, "Erro ao obter votações em comissões");
@@ -11,7 +11,7 @@ import { cachedFetchWithMeta } from "../cache/manager.js";
11
11
  import { upstreamFetch } from "../throttle/upstream.js";
12
12
  import { toolError, errorFrom, buildParams, ensureArray } from "../utils/validation.js";
13
13
  import { computarPlacar } from "../utils/placar.js";
14
- import { provenanceFor, resultWithProvenance } from "../utils/provenance.js";
14
+ import { NOTA_DERIVACAO_PLACAR, derivadoSe, provenanceFor, resultWithProvenance } from "../utils/provenance.js";
15
15
  import { CACHE_ON_DEMAND } from "../types.js";
16
16
  /** Convert YYYYMMDD → YYYY-MM-DD (required by /votacao endpoint). */
17
17
  export function toISODate(yyyymmdd) {
@@ -42,6 +42,12 @@ export function expandirResultado(code) {
42
42
  const raw = String(code).trim();
43
43
  return { resultado: RESULTADO_VOTACAO[raw.toUpperCase()] ?? raw, resultadoCodigo: raw };
44
44
  }
45
+ /**
46
+ * Provenance of a plenary vote response (contract v1.3): `derived` only when at least one item's
47
+ * tally was counted from the nominal votes (`placarComputado`); totals the source publishes are
48
+ * copied and do not mark the block.
49
+ */
50
+ export const provenanceDaVotacao = (prov, votacoes) => derivadoSe(prov, votacoes.some((v) => v.placarComputado === true), NOTA_DERIVACAO_PLACAR);
45
51
  /** Parse a single vote item from the /votacao endpoint (flat camelCase). */
46
52
  export function parseVotacaoItem(v, includeVotos = false) {
47
53
  const votosRaw = Array.isArray(v.votos) ? v.votos : [];
@@ -138,8 +144,8 @@ export function registerVotacoesTools(server, baseUrl) {
138
144
  retrieved_at: fetchedAt,
139
145
  });
140
146
  if (votacoes.length === 1)
141
- return resultWithProvenance(votacoes[0], prov);
142
- return resultWithProvenance({ codigoSessao: codigo, count: votacoes.length, votacoes }, prov);
147
+ return resultWithProvenance(votacoes[0], provenanceDaVotacao(prov, votacoes));
148
+ return resultWithProvenance({ codigoSessao: codigo, count: votacoes.length, votacoes }, provenanceDaVotacao(prov, votacoes));
143
149
  }
144
150
  // PASSO 2 — vazio não é resposta: o número pode ser do OUTRO espaço.
145
151
  // Até 24/09/2026 o handler paravaaqui e devolvia `{count: 0}` — para
@@ -152,11 +158,11 @@ export function registerVotacoesTools(server, baseUrl) {
152
158
  const achado = acharPorCodigoVotacao(cru, codigo);
153
159
  if (achado) {
154
160
  const votacao = parseVotacaoItem(achado, true);
155
- return resultWithProvenance(votacao, provenanceFor("SENADO_LEGIS", baseUrl, "/votacao", {
161
+ return resultWithProvenance(votacao, provenanceDaVotacao(provenanceFor("SENADO_LEGIS", baseUrl, "/votacao", {
156
162
  dataset_id: `${CAMPO_VOTACAO}=${codigo}`,
157
163
  reference_period: votacao.data || undefined,
158
164
  retrieved_at: fetchedAtJanela,
159
- }));
165
+ }), [votacao]));
160
166
  }
161
167
  // PASSO 3 — não está em nenhum dos dois espaços: ausência TIPADA.
162
168
  const ondeProcurou = params.ano
@@ -187,7 +193,7 @@ export function registerVotacoesTools(server, baseUrl) {
187
193
  reference_period: votacoes[0]?.data || undefined,
188
194
  retrieved_at: fetchedAt,
189
195
  });
190
- return resultWithProvenance({ codigoMateria: params.codigoMateria, count: votacoes.length, votacoes }, prov);
196
+ return resultWithProvenance({ codigoMateria: params.codigoMateria, count: votacoes.length, votacoes }, provenanceDaVotacao(prov, votacoes));
191
197
  }
192
198
  catch (e) {
193
199
  return errorFrom(e, "Erro ao obter votações da matéria");
@@ -235,7 +241,7 @@ export function registerVotacoesTools(server, baseUrl) {
235
241
  reference_period: di && df ? `${di}/${df}` : di || df || undefined,
236
242
  retrieved_at: fetchedAt,
237
243
  });
238
- return resultWithProvenance({ count: votacoes.length, votacoes }, prov);
244
+ return resultWithProvenance({ count: votacoes.length, votacoes }, provenanceDaVotacao(prov, votacoes));
239
245
  }
240
246
  catch (e) {
241
247
  return errorFrom(e, "Erro na busca de votações");
@@ -277,17 +277,49 @@ export function withFieldSources(prov, fieldSources) {
277
277
  */
278
278
  export const NOTA_DERIVACAO_ESTATISTICAS = "Estatísticas (distribuição, agregados e rankings) computadas pelo servidor a partir dos registros brutos retornados pela fonte; os valores individuais permanecem os originais do Senado Federal.";
279
279
  /**
280
- * Marca o bloco como derivado (contrato v1.3): `derived: true` + a nota fixa dos modos
281
- * `estatisticas`. Aplique por último, sobre o bloco já completo (inclusive o de
282
- * `withFieldSources`); o envelope é re-validado inteiro no contrato (build).
280
+ * Notas fixas dos demais cálculos do servidor (contrato v1.3, 3.19.0). Uma por TIPO de
281
+ * cálculo, não por tool: o leitor precisa saber o que foi calculado, não onde.
283
282
  */
284
- export function derivadoDeEstatisticas(prov) {
283
+ /** Somas, contagens e médias por grupo (modos agregados, `valorTotal`, contagens por unidade/partido/UF). */
284
+ export const NOTA_DERIVACAO_SOMAS = "Totais, contagens e médias por grupo calculados pelo servidor a partir dos registros da fonte; os valores individuais permanecem os originais do Senado Federal.";
285
+ /** `bruto` das remunerações: soma dos sete componentes da folha. */
286
+ export const NOTA_DERIVACAO_BRUTO = "A remuneração bruta é a soma, feita pelo servidor, dos sete componentes publicados pela fonte, que permanecem os originais.";
287
+ /** Servidores sob uma unidade: classificação de cada lotação no organograma, feita pelo servidor. */
288
+ export const NOTA_DERIVACAO_SUBORDINADAS = "Servidores atribuídos a cada unidade e contados pelo servidor, cruzando a lotação de cada um com o organograma publicado; o total é um piso (lotações não reconhecidas ficam de fora).";
289
+ /** Placar de votação do plenário contado dos votos nominais (votação aberta sem totais na fonte). */
290
+ export const NOTA_DERIVACAO_PLACAR = "Totais de votos contados pelo servidor a partir dos votos nominais, porque a fonte não publica o placar desta votação.";
291
+ /** Placar de votação de comissão contado dos votos, quando a fonte não traz os totais. */
292
+ export const NOTA_DERIVACAO_PLACAR_COMISSAO = "Totais de votos de pelo menos uma votação contados pelo servidor a partir dos votos de cada parlamentar, porque a fonte não publicou o placar dela.";
293
+ /** Percentuais (e o total, quando ausente) das consultas públicas do e-Cidadania. */
294
+ export const NOTA_DERIVACAO_PERCENTUAIS = "Percentuais de sim e não, e o total de votos quando a fonte não o publica, calculados pelo servidor a partir dos votos da fonte.";
295
+ /** Polarização, consenso e ordem por participação no e-Cidadania. */
296
+ export const NOTA_DERIVACAO_RANKING_ECIDADANIA = "Polarização, consenso e ordem por participação calculados pelo servidor a partir dos votos e apoios publicados pela fonte.";
297
+ /** Data de criação estimada das ideias legislativas. */
298
+ export const NOTA_DERIVACAO_DATA_ESTIMADA = "A data de criação é estimada pelo servidor (fim do prazo de apoio menos 120 dias); a fonte não a publica.";
299
+ /** Legislatura sem número na fonte: o próprio número é o padrão do servidor. */
300
+ export const NOTA_DERIVACAO_LEGISLATURA_PADRAO = "A fonte não informou a legislatura: o servidor assume a 57ª (2023–2027) e calcula o período e as datas.";
301
+ /** Período e datas da legislatura derivados do número dela. */
302
+ export const NOTA_DERIVACAO_LEGISLATURA = "Período e datas da legislatura calculados pelo servidor a partir do número publicado pela fonte (mandatos de quatro anos).";
303
+ /**
304
+ * Marca o bloco como derivado (contrato v1.3): `derived: true` + a nota dada. Aplique por
305
+ * último, sobre o bloco já completo (inclusive o de `withFieldSources`); o envelope é
306
+ * re-validado inteiro no contrato (build).
307
+ */
308
+ export function derivado(prov, nota) {
285
309
  return provenanceContext.build({
286
310
  ...prov,
287
311
  derived: true,
288
- derivation_note: NOTA_DERIVACAO_ESTATISTICAS,
312
+ derivation_note: nota,
289
313
  });
290
314
  }
315
+ /** `derivado` só quando `calculou`; senão o bloco volta intacto (sem `derived`). */
316
+ export function derivadoSe(prov, calculou, nota) {
317
+ return calculou ? derivado(prov, nota) : prov;
318
+ }
319
+ /** `derivado` com a nota dos modos `estatisticas`. */
320
+ export function derivadoDeEstatisticas(prov) {
321
+ return derivado(prov, NOTA_DERIVACAO_ESTATISTICAS);
322
+ }
291
323
  /** Rodapé de texto do contrato: fonte · url · vintage · extração, licença e aviso ao leitor. */
292
324
  export const provenanceFooter = (p) => provenanceContext.footer(p);
293
325
  /**
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Single source of truth for the server version (kept in sync with package.json). */
2
- export const VERSION = "3.18.5";
2
+ export const VERSION = "3.19.0";
3
3
  export const USER_AGENT = `senado-br-mcp/${VERSION} (+https://senado.sidneybissoli.com; sbissoli76@gmail.com)`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "senado-br-mcp",
3
- "version": "3.18.5",
3
+ "version": "3.19.0",
4
4
  "description": "MCP server for Brazilian Senate open data — runs hosted on Cloudflare Workers or locally via npx (stdio)",
5
5
  "keywords": [
6
6
  "mcp",