senado-br-mcp 3.20.0 → 3.20.1

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.
@@ -200,11 +200,11 @@ const ideiasSchema = {
200
200
  {
201
201
  name: "autorUf",
202
202
  type: "string",
203
- description: "UF do cidadão autor da ideia (SEM nome — conteúdo de cidadão).",
203
+ description: "Localidade do cidadão autor da ideia como o portal a publica: a sigla da UF ou \"Exterior\" (SEM nome — conteúdo de cidadão).",
204
204
  sourceEndpoint: EP_IDEIAS_DETALHE,
205
- sourceField: "página de detalhe: \"Ideia proposta por … (UF)\" (na linha do tempo desde 12/10/2026) — 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.",
205
+ sourceField: "página de detalhe: \"Ideia proposta por … (UF)\" (na linha do tempo desde 12/10/2026) — apenas o parêntese",
206
+ operationalization: "Guarda o parêntese depois do nome tal como publicado: uma das 27 siglas de UF ou \"Exterior\" (os únicos valores medidos em 18/10/2026); parêntese vazio → null. O NOME do cidadão é descartado NA ORIGEM (nunca gravado no corpus).",
207
+ caveat: "Não é sempre uma UF: \"Exterior\" marca autor fora do país (antes de 18/10/2026 esse caso era gravado como null). null = ideia ainda não enriquecida pelo detalhe, ou portal sem localidade publicada. Nome do autor deliberadamente não coletado.",
208
208
  },
209
209
  {
210
210
  name: "descricao",
@@ -127,16 +127,38 @@ function unico(c, campo, valor) {
127
127
  throw new IdeiaDetalheForaDoMoldeError(`${c.onde}: segundo acontecimento para "${String(campo)}"`);
128
128
  c.ideia[campo] = valor;
129
129
  }
130
+ /**
131
+ * Valores do parêntese do autor medidos em 18/10/2026 nas 2.968 ideias gravadas sem UF: as 27 siglas
132
+ * e "Exterior". O valor é guardado COMO PUBLICADO (classificação da fonte), e o vazio é null. Qualquer
133
+ * outro texto é página fora do molde — o vigia o vê, em vez de um valor desconhecido entrar no corpus.
134
+ */
135
+ const LOCALIDADES_DO_AUTOR = new Set([
136
+ "AC", "AL", "AM", "AP", "BA", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", "PA", "PB", "PE", "PI", "PR",
137
+ "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO", "Exterior",
138
+ ]);
139
+ function localidadeDoAutor(span, onde) {
140
+ const s = decodificar(span ?? "").trim();
141
+ if (s === "")
142
+ return null;
143
+ const p = /^\((.*)\)$/.exec(s)?.[1]?.trim();
144
+ if (p === undefined || !LOCALIDADES_DO_AUTOR.has(p)) {
145
+ throw new IdeiaDetalheForaDoMoldeError(`${onde}: parêntese do autor fora dos valores medidos: ${JSON.stringify(s.slice(0, 40))}`);
146
+ }
147
+ return p;
148
+ }
130
149
  /** Tenta cada tipo em ordem; devolve o tipo reconhecido (e preenche a ideia) ou null. */
131
150
  function lerItem(c) {
132
151
  const { html, item, onde } = c;
133
152
  const t = item.texto;
134
153
  if (/^Ideia proposta por /.test(t)) {
135
- const m = /Ideia proposta por\s*<strong>\s*<span>([\s\S]*?)<\/span>\s*(?:<span>\s*\(([A-Z]{2})\)\s*<\/span>)?\s*<\/strong>/.exec(html);
154
+ // O segundo <span> leva o parêntese da localidade SEM tags dentro: assim o nome (preguiçoso) para no
155
+ // primeiro </span> em todos os casos. Com o parêntese restrito a duas letras, "(Exterior)" caía fora
156
+ // do grupo opcional e o nome se esticava até engoli-lo — a UF saía null, calada (A8, 18/10/2026).
157
+ const m = /Ideia proposta por\s*<strong>\s*<span>([\s\S]*?)<\/span>\s*(?:<span>([^<]*)<\/span>)?\s*<\/strong>/.exec(html);
136
158
  if (!m)
137
159
  throw new IdeiaDetalheForaDoMoldeError(`${onde}: proposta sem "<strong><span>NOME</span><span> (UF)</span>"`);
138
160
  unico(c, "autorNome", textoDoHtml(m[1]) || null);
139
- c.ideia.autorUf = m[2] ?? null;
161
+ c.ideia.autorUf = localidadeDoAutor(m[2], onde);
140
162
  unico(c, "dataProposta", item.data);
141
163
  return "proposta";
142
164
  }
@@ -297,7 +297,7 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
297
297
  }
298
298
  });
299
299
  // G5. senado_ecidadania_listar_ideias
300
- server.tool("senado_ecidadania_listar_ideias", "Lista ideias legislativas propostas por cidadãos no e-Cidadania — **conjunto completo** (corpus persistido em D1; ideias em andamento atualizadas diariamente; ~118 mil ideias, incluindo encerradas e convertidas em proposição). Retorna `{ count, ideias, avisoDatas }`, cada ideia com `id`, `titulo`, `apoios`, `status` (a categoria da listagem do portal em que a ideia aparece: `aberta` = \"Aberta\", \"Na comissão\" ou \"Aguardando envio à CDH\"; `encerrada` = \"Encerrada – sem apoio suficiente\" ou \"Não acatada\" pela CDH; `convertida` = \"Transformada em proposição\"; `removida` = retirada do portal, que não a publica mais — o corpus guarda o último estado conhecido), `url` e os campos lidos do detalhe quando o backfill já passou por ela (`dataPublicacao`, `autorUf`, `descricao`, `plConvertido`; `null` até lá). **Datas:** `dataPublicacao` é a DATA LIMITE para receber os 20.000 apoios — a única data que o corpus guarda; o nome do campo é histórico — e por isso cai no futuro em ideia aberta; `dataCriacaoEstimada` (= data limite − 120 dias, a regra dos 4 meses do portal) é calculada pelo servidor e é estimativa, não dado da fonte; a data REAL da proposição sai em `senado_ecidadania_obter_ideia` (`dataProposta`); `avisoDatas` repete isso na resposta. Aceita filtro por `status` e `limite` (padrão 20). Para um ranking das mais apoiadas, ordene por apoios (`ordenarPor: \"apoios\"`, `ordem: \"desc\"`). Para o detalhe completo de uma ideia (texto, autor, linha do tempo, Sugestão, decisão da CDH, proposições em que virou) chame `senado_ecidadania_obter_ideia` com o `id`.", {
300
+ server.tool("senado_ecidadania_listar_ideias", "Lista ideias legislativas propostas por cidadãos no e-Cidadania — **conjunto completo** (corpus persistido em D1; ideias em andamento atualizadas diariamente; ~118 mil ideias, incluindo encerradas e convertidas em proposição). Retorna `{ count, ideias, avisoDatas }`, cada ideia com `id`, `titulo`, `apoios`, `status` (a categoria da listagem do portal em que a ideia aparece: `aberta` = \"Aberta\", \"Na comissão\" ou \"Aguardando envio à CDH\"; `encerrada` = \"Encerrada – sem apoio suficiente\" ou \"Não acatada\" pela CDH; `convertida` = \"Transformada em proposição\"; `removida` = retirada do portal, que não a publica mais — o corpus guarda o último estado conhecido), `url` e os campos lidos do detalhe quando o backfill já passou por ela (`dataPublicacao`, `autorUf`, `descricao`, `plConvertido`; `null` até lá; `autorUf` traz a sigla da UF ou \"Exterior\", como o portal publica, e `null` quando o portal não publica a localidade). **Datas:** `dataPublicacao` é a DATA LIMITE para receber os 20.000 apoios — a única data que o corpus guarda; o nome do campo é histórico — e por isso cai no futuro em ideia aberta; `dataCriacaoEstimada` (= data limite − 120 dias, a regra dos 4 meses do portal) é calculada pelo servidor e é estimativa, não dado da fonte; a data REAL da proposição sai em `senado_ecidadania_obter_ideia` (`dataProposta`); `avisoDatas` repete isso na resposta. Aceita filtro por `status` e `limite` (padrão 20). Para um ranking das mais apoiadas, ordene por apoios (`ordenarPor: \"apoios\"`, `ordem: \"desc\"`). Para o detalhe completo de uma ideia (texto, autor, linha do tempo, Sugestão, decisão da CDH, proposições em que virou) chame `senado_ecidadania_obter_ideia` com o `id`.", {
301
301
  status: z.enum(["aberta", "encerrada", "convertida", "removida", "todas"]).optional().describe("Filtrar por status (`removida` = retirada do portal)"),
302
302
  ordenarPor: z.enum(["apoios", "data", "comentarios"]).optional().describe("Campo para ordenação (apoios é o disponível no corpus; data/comentarios só no detalhe)"),
303
303
  ordem: z.enum(["asc", "desc"]).optional().describe("Ordem de ordenação"),
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.20.0";
2
+ export const VERSION = "3.20.1";
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.20.0",
3
+ "version": "3.20.1",
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",