senado-br-mcp 3.18.4 → 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
@@ -553,18 +553,24 @@ Coverage spans all four upstream sources, each with its own `source`/`citation`/
553
553
  - **Senado Federal — Execução Orçamentária e Financeira** — Arquimedes/Financeiro feed at `senado.gov.br`
554
554
  - **Senado Federal — Portal e-Cidadania** — `www12.senado.leg.br/ecidadania`
555
555
 
556
- Fields of the `concise` block (per response — one tool, one source; keys in this fixed order, `null` when the source does not expose the value):
556
+ Fields of the `concise` block (contract v1.3 since 3.18.5; per response — one tool, one source; keys in this fixed order; the first seven are always present, with `null` when the source does not expose the value; the optional ones are **absent**, never empty, when they do not apply):
557
557
 
558
558
  | Field | Meaning |
559
559
  |-------|---------|
560
560
  | `source` | Official source name (e.g. *Senado Federal — Dados Abertos (Legislativo)*) |
561
561
  | `source_url` | Canonical endpoint/item URL consulted (e.g. `…/processo/{id}`) |
562
562
  | `data_vintage` | Vintage/competência of the data (e.g. `2024-03-15`, `2019`) — named `reference_period` before v3.5.0 |
563
- | `retrieved_at` | ISO-8601 of the **upstream extraction** — carried through the cache, so it reflects when the data was actually fetched, not the build or the cache-hit time |
563
+ | `retrieved_at` | ISO-8601 of the **upstream extraction** — carried through the cache, so it reflects when the data was actually fetched, not the build or the cache-hit time; on merged responses, the oldest of the parts |
564
+ | `retrieval` | Measured diagnostics of the trip to the source (attempts, outcome), or `null` when nothing was fetched in the call (pure cache, local data) — since 3.11.0 (v1.1) |
564
565
  | `citation` | Ready-to-use citation string (human-readable) |
565
566
  | `license` | Source terms (Dados Abertos do Senado Federal) |
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 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
+ | `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 |
566
570
 
567
- The canonical model behind the block also carries `dataset.id` (item/series identifier, e.g. `codigoMateria=137808`) and `api_version`; those are validated on every build and inform `attribution` (below), but are not part of the `concise` projection. Per-field `field_sources` are (contract v1.2, since 3.17.0), on the responses that merge reads.
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.
572
+
573
+ The canonical model behind the block also carries `dataset.id` (item/series identifier, e.g. `codigoMateria=137808`) and `api_version`; those are validated on every build and inform `attribution` (below), but are not part of the `concise` projection.
568
574
 
569
575
  In addition to the `provenance` envelope, `structuredContent` carries a top-level **`attribution`** list — the distinct source URLs behind the response. This mirrors the naming proposed in [modelcontextprotocol#711](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/711) (where `attribution` is a list of source references at the response level), so the server stays forward-compatible if that RFC lands; the richer `provenance` object remains this server's own extension.
570
576
 
@@ -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);
@@ -134,7 +134,7 @@ export function parseDocumentoProcesso(d) {
134
134
  * também entra como sub-fonte: o `retrieved_at` do topo é derivado pelo adaptador como o
135
135
  * MAIS ANTIGO das três (contrato §3) e, sem este item, o instante do detalhe sumiria do
136
136
  * bloco quando o resumo fosse mais velho. Antes da 3.17.0 o topo era o fetch do detalhe,
137
- * feito DEPOIS das sub-fontes — com a v1.2 ligada, a lib lançaria e a tool cairia.
137
+ * feito DEPOIS das sub-fontes — com a regra da v1.2 (que a 1.3 mantém), a lib lançaria e a tool cairia.
138
138
  */
139
139
  export function provenanceMateriaDetalhe(baseUrl, a) {
140
140
  const base = baseUrl.replace(/\/$/, "");
@@ -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 { 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) {
@@ -249,7 +251,7 @@ export function registerOrcamentoSenadoTools(server) {
249
251
  ano: params.ano ?? null,
250
252
  totalLinhas: itens.length,
251
253
  ...estatisticasExecucao(itens, { tipo, campo: params.campo, agruparPor: params.agruparPor, topN: params.topN ?? 10 }),
252
- }, prov);
254
+ }, derivadoDeEstatisticas(prov));
253
255
  }
254
256
  if (tipo === "despesas") {
255
257
  let itens = ensureArray(bruto?.despesas).map(parseDespesa);
@@ -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 { 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) {
@@ -157,7 +159,7 @@ export function registerSenadoresAdminTools(server, admBaseUrl) {
157
159
  totalDespesas: filtrado.length,
158
160
  valorTotal: totalGeral,
159
161
  ...estatisticasCeaps(filtrado, { agruparPor: params.agruparPor, topN: params.topN ?? 10 }),
160
- }, prov);
162
+ }, derivadoDeEstatisticas(prov));
161
163
  }
162
164
  const limite = params.limite ?? 100;
163
165
  const modo = params.modo ?? "por-senador";
@@ -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 { 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",
@@ -342,13 +352,13 @@ export function particionarPorUnidade(lista, indice, codUnidade) {
342
352
  * organograma foi lido agora, o que também é. Com ele, o topo passa a ser o mais antigo
343
353
  * das duas (derivado pelo adaptador, contrato §3), e a folha entra como sub-fonte própria
344
354
  * para que o seu instante não suma do bloco. Antes da 3.17.0 o topo era o da folha, mais
345
- * novo que o snapshot — com a v1.2 ligada, a lib lançaria e a tool cairia.
355
+ * novo que o snapshot — com a regra da v1.2 (que a 1.3 mantém), a lib lançaria e a tool cairia.
346
356
  *
347
357
  * `folhaDoCache` é o `fromCache` da leitura da folha; o snapshot embutido não é cache nem
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
@@ -473,7 +483,7 @@ export function registerServidoresTools(server, admBaseUrl) {
473
483
  agruparPor: params.agruparPor,
474
484
  topN: params.topN ?? 10,
475
485
  }),
476
- }, prov);
486
+ }, derivadoDeEstatisticas(prov));
477
487
  }
478
488
  if ((params.modo ?? "resumo") === "detalhe") {
479
489
  const limite = params.limite ?? 50;
@@ -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");
@@ -539,7 +549,7 @@ export function registerServidoresTools(server, admBaseUrl) {
539
549
  total: itens.length,
540
550
  valorTotal,
541
551
  ...estatisticasHorasExtras(itens, { agruparPor: params.agruparPor, topN: params.topN ?? 10 }),
542
- }, prov);
552
+ }, derivadoDeEstatisticas(prov));
543
553
  }
544
554
  return resultWithProvenance({
545
555
  ano: params.ano,
@@ -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");
@@ -9,7 +9,7 @@ import { z } from "zod";
9
9
  import { cachedFetchWithMeta } from "../cache/manager.js";
10
10
  import { admFetch } from "../throttle/adm.js";
11
11
  import { errorFrom, ensureArray } from "../utils/validation.js";
12
- import { leituraDoCache, provenanceFor, resultWithProvenance, withFieldSources } from "../utils/provenance.js";
12
+ import { derivadoDeEstatisticas, leituraDoCache, provenanceFor, resultWithProvenance, withFieldSources } from "../utils/provenance.js";
13
13
  import { computarEstatisticas, arredondarEstatisticas, arredondarEntradas, } from "../utils/estatisticas.js";
14
14
  import { CACHE_STATIC } from "../types.js";
15
15
  import { matchesFiltro } from "./contratacoes.js";
@@ -285,7 +285,7 @@ export function registerSupridosTools(server, admBaseUrl) {
285
285
  modo: "estatisticas",
286
286
  total: lista.length,
287
287
  ...estatisticasSuprimento(lista, { tipo, campo: params.campo, agruparPor: params.agruparPor, topN: params.topN ?? 10, nomePorCodigo: nomes?.nomes }),
288
- }, nomes ? provenanceSuprimentoComNomes(prov, { fromCache }, params.ano, admBaseUrl, nomes, params.agruparPor) : prov);
288
+ }, derivadoDeEstatisticas(nomes ? provenanceSuprimentoComNomes(prov, { fromCache }, params.ano, admBaseUrl, nomes, params.agruparPor) : prov));
289
289
  }
290
290
  return resultWithProvenance({
291
291
  ano: params.ano,
@@ -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");
@@ -17,14 +17,16 @@
17
17
  * - v1.1 (release 3.11.0): o concise tem 7 chaves fixas — `source`, `source_url`,
18
18
  * `data_vintage` (ex-`reference_period`), `retrieved_at`, `citation`, `license` e
19
19
  * `retrieval` (diagnóstico medido da ida à origem, ou `null`).
20
- * - v1.2 (EMITIDA desde a release 3.17.0): acrescenta ao concise a chave opcional
21
- * `field_sources` (sub-fontes por campo), só nas respostas que fundem recortes. Com
22
- * ela a lib COBRA que o `retrieved_at` do topo seja o mais antigo entre as sub-fontes
23
- * (lança `ProvenanceContractError`, o que derrubaria a tool) — por isso o adaptador
24
- * o deriva por construção (`oldestRetrievedAt`), nunca a tool à mão.
25
- * - v1.3 (declarada pelo `outputSchema` desde a 3.17.0, ainda NÃO emitida): `notices`,
26
- * `derived`/`derivation_note` e `revision`. A `revision` já é informada aqui, por
27
- * fonte (`SOURCES`), e a lib a descarta enquanto o servidor emitir 1.2.
20
+ * - v1.2 (release 3.17.0): acrescenta ao concise a chave opcional `field_sources`
21
+ * (sub-fontes por campo), só nas respostas que fundem recortes. Com ela a lib COBRA
22
+ * que o `retrieved_at` do topo seja o mais antigo entre as sub-fontes (lança
23
+ * `ProvenanceContractError`, o que derrubaria a tool) — por isso o adaptador o deriva
24
+ * por construção (`oldestRetrievedAt`), nunca a tool à mão.
25
+ * - v1.3 (EMITIDA desde a release 3.18.5; o `outputSchema` a aceitava desde a 3.17.0):
26
+ * o concise leva `revision` SEMPRE (por fonte, em `SOURCES`) e `derived` +
27
+ * `derivation_note` nos modos `estatisticas` (`derivadoDeEstatisticas`). `notices`
28
+ * não sai: nenhuma fonte do senado publica aviso junto com o dado. Sem cálculo, as
29
+ * chaves opcionais ficam AUSENTES, não vazias.
28
30
  * - O rodapé de texto segue a redação fixada do contrato: linha de fonte
29
31
  * ("Fonte: … · url · dados de … · extraído em …"), linha de licença e o aviso ao
30
32
  * leitor de que a referência completa pode ser solicitada na própria conversa.
@@ -57,7 +59,7 @@ export const provenanceContext = createProvenanceContext({
57
59
  locale: "pt-BR",
58
60
  timezone: { offset: "-03:00", label: "horário de Brasília" },
59
61
  defaultMode: "concise",
60
- contractVersion: "1.2",
62
+ contractVersion: "1.3",
61
63
  });
62
64
  const BRASILIA = provenanceContext.timezone;
63
65
  /**
@@ -134,7 +136,7 @@ const extraToCanonical = (extra = {}) => {
134
136
  * `current` em todas as fontes do senado, `final` em nenhuma — só com prova da fonte).
135
137
  * A `note` só repete o que o servidor JÁ diz nas instructions (`src/app-surface.ts`) sobre
136
138
  * os dados administrativos; nas demais fontes não há texto publicado, então `null`.
137
- * Enquanto o servidor emitir a v1.2 a lib descarta o campo do fio — fica no canônico.
139
+ * Sai no concise de toda resposta (v1.3).
138
140
  */
139
141
  const REVISAO_VIGENTE = { status: "current", note: null };
140
142
  const REVISAO_ADMINISTRATIVA = {
@@ -268,6 +270,56 @@ export function withFieldSources(prov, fieldSources) {
268
270
  field_sources: fieldSources.map(toCanonicalFieldSource),
269
271
  });
270
272
  }
273
+ /**
274
+ * Nota fixa dos modos `estatisticas` (mesma redação do ibge-br-mcp, adaptada à fonte):
275
+ * a distribuição, os agregados e o ranking são cálculo deste servidor; cada valor
276
+ * individual citado é o que o Senado publica.
277
+ */
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
+ /**
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.
282
+ */
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) {
309
+ return provenanceContext.build({
310
+ ...prov,
311
+ derived: true,
312
+ derivation_note: nota,
313
+ });
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
+ }
271
323
  /** Rodapé de texto do contrato: fonte · url · vintage · extração, licença e aviso ao leitor. */
272
324
  export const provenanceFooter = (p) => provenanceContext.footer(p);
273
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.4";
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.4",
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",