Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ jobs:
uv run namescommons run --offline --sources tests/fixtures/sources.yaml --adapters adaptadores \
--cache "$RUNNER_TEMP/cache" --out "$RUNNER_TEMP/dist" --release "$(date -u +%Y.%m)"
test -s "$RUNNER_TEMP/dist/by_country/PT.csv"
- name: namescommons run --offline sobre as amostras das fontes oficiais
- name: namescommons run --offline sobre as amostras das fontes de sources.yaml
run: |
mkdir -p "$RUNNER_TEMP/cache-oficiais"
find tests/fixtures -mindepth 1 -maxdepth 1 -type d ! -name exemplo ! -name planilhas -exec cp -R {} "$RUNNER_TEMP/cache-oficiais/" \;
uv run namescommons run --offline --cache "$RUNNER_TEMP/cache-oficiais" \
--out "$RUNNER_TEMP/dist-oficiais" --release "$(date -u +%Y.%m)"
for pais in CA ES FR IE NO NZ US; do test -s "$RUNNER_TEMP/dist-oficiais/by_country/$pais.csv"; done
test -s "$RUNNER_TEMP/dist-oficiais/variant_links.parquet"
31 changes: 31 additions & 0 deletions docs/adr/0008-acesso-ao-wikidata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 0008. Acesso ao Wikidata: consultas SPARQL no QLever, paginadas e guardadas no cache

## Status

Aceito (2026-10-12)

## Contexto

A spec 004 lê do Wikidata os itens de prenome (`Q202444` e subclasses) e de sobrenome (`Q101352` e subclasses), com rótulo nativo, classe e ligações entre itens. O contrato da spec 002 exige recursos com URL fixa (`Resource(url, filename)`), baixados uma vez para o cache e lidos depois com `--offline`.

Há três caminhos para os dados:

- **Wikidata Query Service (WDQS, `query.wikidata.org`).** Serviço oficial da Wikimedia, com limite de 60 s por consulta. Em 2026-10-12, contar os sobrenomes com rótulo nativo (`?i wdt:P31 wd:Q101352 . ?i wdt:P1705 ?l`) estourou o tempo duas vezes ("upstream request timeout"), e a consulta das subclasses de prenome com contagem também. Uma consulta por letra inicial ou por faixa de identificador continuaria perto do limite, e a lista de páginas mudaria conforme o Wikidata cresce.
- **Dump JSON completo (`dumps.wikimedia.org/wikidatawiki/entities/latest-all.json.bz2`).** Reproduzível, mas tem mais de 100 GB compactado para extrair menos de 1 milhão de itens. Ler o dump inteiro excede o orçamento de tempo e disco da spec 002 (R8) e da CI.
- **QLever (`qlever.dev/api/wikidata`).** Motor SPARQL da Universidade de Freiburg sobre o dump completo do Wikidata, com atualização contínua (a página de estatísticas do índice informa "Complete Wikidata ... updated LIVE"). As mesmas consultas que estouraram no WDQS responderam em 1 a 12 s: 734.234 linhas de sobrenome em 11 s, 134.180 de prenome em 6 s. Os dados são os do Wikidata, sob CC0.

## Decisão

- Os recursos do adaptador `wikidata` são resultados de consultas SPARQL ao endpoint `https://qlever.dev/api/wikidata`, no formato SPARQL 1.1 Query Results JSON, pedidos por GET com a consulta inteira na URL e o formato no parâmetro `action=sparql_json`. O texto de cada consulta fica no módulo do adaptador.
- Cada consulta é ordenada (`ORDER BY`) e paginada com `LIMIT 200000 OFFSET k`. O número de páginas de cada consulta é fixo no módulo, com folga sobre o tamanho medido. Uma página de 200.000 linhas de JSON ocupa cerca de 28 MB e cerca de 500 MB depois de lida pelo `json` da biblioteca padrão; o arquivo inteiro de sobrenomes (104 MB) passava de 1,8 GB.
- A última página de uma consulta não pode vir cheia: se vier, o adaptador falha com a instrução de aumentar o número de páginas. Uma página incompleta seguida de página com linhas também é erro (o índice mudou entre os downloads), com a instrução de baixar de novo com `--refresh`.
- As consultas usam só SPARQL 1.1 padrão (sem o serviço de rótulos do WDQS), com `PREFIX` explícitos, para rodar em qualquer endpoint com o Wikidata completo.
- O `User-Agent` é o do pipeline (spec 002, R4), que identifica o projeto e o repositório como pede a política de User-Agent da Wikimedia. Os downloads são sequenciais.

## Consequências

- `fetch` baixa 15 arquivos (216 MB, em 54 s na medição de 2026-10-12), e `ingest --offline` lê o cache sem rede, como as outras fontes.
- O projeto depende de um serviço acadêmico sem acordo de nível de serviço. Se o QLever sair do ar, as consultas valem no WDQS só para as classes pequenas; a ingestão do dump completo fica registrada como alternativa na issue #29.
- O resultado reflete o índice do QLever no momento do download, que pode estar alguns dias atrás do Wikidata. O `.meta.json` do cache registra a data e a hora de cada página.
- Se o índice mudar entre o download de duas páginas da mesma consulta, linhas podem se repetir (inofensivo: o pipeline soma observações repetidas de fonte `attested` sem alterar nada) ou faltar na fronteira. A falta não é detectável pela página; o risco é pequeno porque as páginas de uma consulta são baixadas em segundos.
- Quando o Wikidata crescer além da folga, a ingestão falha alto e a correção é mudar um número no módulo.
26 changes: 26 additions & 0 deletions docs/adr/0009-ligacoes-entre-variantes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 0009. Ligações entre variantes numa tabela própria, fora das observações

## Status

Aceito (2026-10-12)

## Contexto

A spec 005 vai preencher `group_id` (spec 001, R3.2) agrupando variantes de um nome: `Мария` e `Maria`, `Иванов` e `Ivanov`, `Ivanov` e `Ivanova`. O Wikidata publica essas relações entre itens: "said to be the same as" (P460), "family name identical to this given name" (P1533), versões para o outro gênero (P1560, P5278), "different from" (P1889) e transliterações (P2440).

A tabela de observações (spec 001, R1) descreve um nome atestado numa fonte; ela não tem lugar para uma relação entre dois nomes. Codificar a ligação num campo existente (por exemplo, `ascii`) mudaria o significado do campo, o que exige versão maior. Acrescentar colunas de ligação a `Observation` duplicaria cada nome por ligação e quebraria a chave de unicidade (R1.4).

## Decisão

- As ligações formam uma tabela própria, `variant_links`, com uma linha por par de nomes ligados e as colunas `source_id`, `relation`, `qid`, `name`, `name_key`, `kind`, `target_qid`, `target_name`, `target_name_key`, `target_kind`. O formato está na spec 004 (R4).
- O contrato do adaptador (spec 002, R1) ganha uma função opcional, `links(files) -> Iterator[RawLink]`. Só o adaptador do Wikidata a implementa por enquanto.
- `ingest` grava a tabela num arquivo à parte (`--links`), `build` a recebe (`--links`) e `run` a grava na release como `variant_links.parquet`.
- A tabela registra as relações como a fonte as publica. Agrupar, decidir quais relações juntam variantes e quais as separam é trabalho da spec 005.
- Acrescentar um arquivo à release é mudança compatível: `SCHEMA_VERSION` passa de `1.0.0` para `1.1.0` (versão menor, spec 001, R5).

## Consequências

- A tabela de observações e as saídas agregadas não mudam; quem lê só `given_names.csv` não percebe a novidade.
- A spec 005 lê as ligações de um único arquivo, com os nomes já normalizados (`name_key`), sem consultar o Wikidata de novo.
- Outras fontes com relações entre nomes (por exemplo, listas oficiais de grafias equivalentes) podem implementar `links` sem mudar o esquema.
- A release ganha um arquivo de cerca de 437 mil linhas (11 MB em Parquet, medido em 2026-10-12), contado no `manifest.json`.
2 changes: 2 additions & 0 deletions docs/specs/001-esquema/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Regras:

Cada fonte tem: `id`, `name`, `url`, `license`, `attribution` (texto exigido pela licença), `country`, `count_type`, `primary` (booleano, ver R3.3), `suppression` (limite abaixo do qual a fonte omite nomes, ex.: `5` na SSA; vazio se não houver) e `retrieved` (data da coleta). Fonte sem licença compatível com o ADR 0001 não pode ser registrada. Quando houver mais de uma fonte para o mesmo par (`country`, `count_type`), exatamente uma delas tem `primary: true`.

Nota (spec 004, R5): a exigência de exatamente uma `primary` vale só para `births` e `population`. Em `attested`, `primary` não tem efeito e não é conferido (issue #3).

### R3. Saídas agregadas

**R3.1 `observations.parquet`.** A tabela de R1 completa. Somente Parquet.
Expand Down
83 changes: 83 additions & 0 deletions docs/specs/004-wikidata/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# 004. Wikidata: plano de implementação

**Objetivo:** ingerir os prenomes e sobrenomes do Wikidata como fonte `attested`, gravar as ligações entre variantes numa tabela própria para a spec 005 e corrigir a regra de `primary` em fontes `attested` (issue #3).

**Arquitetura:** o adaptador `src/namescommons/adapters/wikidata.py` declara as consultas SPARQL (texto, variáveis, ordem e número de páginas) e deriva delas os 15 `Resource` com URL fixa no QLever. `parse` lê as páginas de rótulos com `json` da biblioteca padrão, uma de cada vez, e emite `RawRecord` sem país, ano nem contagem. A função opcional `links` lê de novo os rótulos, monta o mapa item -> rótulos e cruza com as páginas de ligação e de transliteração, emitindo `RawLink`. O módulo novo `src/namescommons/links.py` define `VariantLink`, `RELATIONS` e `validate_links`; `pipeline.py` ganha `to_links` e `ingest_links`; `io.py` ganha `LINK_SCHEMA`, `write_links` e `read_links`; a CLI ganha `--links` em `ingest` e `build`, e `run` grava `variant_links.parquet`. `sources.py` deixa de conferir `primary` em `attested`.

**Stack:** a mesma da spec 003 (Python 3.12 via `uv`, `pyarrow`, `pyyaml`, `pycountry`, `pytest`), sem dependência nova. Formato lido: SPARQL 1.1 Query Results JSON.

**Spec:** [`spec.md`](spec.md). ADRs: [`0001`](../../adr/0001-licenca-dos-dados.md), [`0008`](../../adr/0008-acesso-ao-wikidata.md), [`0009`](../../adr/0009-ligacoes-entre-variantes.md). Pendências herdadas: issue #3.

## Restrições globais

- Nenhuma dependência nova de runtime ou de desenvolvimento.
- Os testes nunca acessam a rede. A amostra fica em `tests/fixtures/wikidata/`, com os mesmos 15 nomes de arquivo de `RESOURCES` e menos de 20 KB no total.
- Mensagens: `FormatError` com o arquivo e o esperado; `ligação N: <causa>` para erros de `RawLink`, com N a partir de 1 na ordem de `links`.
- A tabela de observações não muda. `SCHEMA_VERSION` passa a `1.1.0` porque a release ganha `variant_links.parquet`.
- `User-Agent` do pipeline, com o repositório `https://github.com/BenchCommons/dataset-names` como contato.

## Decisões de implementação

| Decisão | Motivo |
|---|---|
| QLever em vez de WDQS ou dump (ADR 0008) | O WDQS estourou o tempo nas consultas de sobrenome; o dump tem mais de 100 GB |
| Páginas de 200.000 linhas, número fixo por consulta (1, 5, 3, 1, 1, 1, 2, 1) | Cerca de 500 MB por página lida; o arquivo de sobrenomes inteiro passava de 1,8 GB. A folga sobre o medido é de 36% a 49% nos sobrenomes e prenomes |
| URL com `action=sparql_json` | O cache baixa só com `User-Agent`; o formato fica determinado pela URL e não pelo cabeçalho `Accept` |
| Uma consulta por propriedade de ligação | A consulta única com `VALUES ?property` estourou memória e tempo no QLever |
| Alvo da ligação não é restrito a itens de nome na consulta; o filtro é feito em Python | A restrição no SPARQL estourou o tempo; o mapa de rótulos já diz quais itens são nomes |
| `parse` agrupa por (`name_key`, `kind`) e emite uma linha por gênero informado, ou uma sem gênero | Cumpre R1.5 da spec 002 dentro do adaptador, sem perder atestação (R3.5) |
| Unissex vira `F` e `M` | A classe diz que o nome é usado pelos dois gêneros; uma linha vazia conflitaria com o mesmo nome em itens masculinos ou femininos |
| `links` lê os rótulos de novo, em vez de dividir estado com `parse` | O contrato da spec 002 trata `parse` como função pura sobre os arquivos; ler 130 MB de JSON duas vezes custa segundos |
| Auto-ligação por `name_key` e `kind` descartada no pipeline (`to_links`), recusada em `validate_links` | `Andrea` masculino P1889 `Andrea` feminino é a mesma chave e não informa nada para agrupar chaves |
| `ingest_links` devolve `None` quando nenhuma fonte selecionada tem `links` | `run` só grava `variant_links.parquet` quando há fonte de ligações; a release das fontes de teste da spec 002 não muda |
| Amostra gerada por `tools/amostra_wikidata.py`, que roda as consultas do módulo com `VALUES ?item` | Reproduz a amostra a partir do código, e as consultas da amostra são as mesmas de produção |

## Foco de revisão

1. **Nome dos dois gêneros vindo de itens diferentes** (`Andrea` masculino, feminino e unissex; `Letha` sem gênero e feminino) não pode quebrar R1.5 nem perder gênero (Task 3: `test_parse_da_amostra_real`, `test_genero_vazio_cede_ao_informado`).
2. **Wikidata cresce além das páginas** e a última página vem cheia: falha alta, não perda silenciosa (Task 3: `test_ultima_pagina_cheia_falha`).
3. **Páginas de momentos diferentes do índice** (Task 3: `test_pagina_com_linhas_depois_de_incompleta_falha`).
4. **Rótulos que não são nomes** (`Q34147`, `Ritwik (given name)`, `Felbert,von`, valor desconhecido) não entram nem como observação nem como ligação (Task 3: `test_rotulos_que_nao_sao_nomes_sao_pulados`; Task 4: `test_links_da_amostra_real`).
5. **Duas fontes `attested` sem país** passam sem `primary`; duas `births` no mesmo país continuam exigindo uma (Task 1).

## Arquivos

| Arquivo | Responsabilidade | Tarefa |
|---|---|---|
| `src/namescommons/sources.py`, `tests/test_sources.py` | Regra de `primary` só para `births` e `population` (R5) | 1 |
| `src/namescommons/adapters/__init__.py` | `RawLink`; `check_adapter` confere `links` | 2 |
| `src/namescommons/links.py` | `VariantLink`, `RELATIONS`, `validate_links` (R4.2, R4.5) | 2 |
| `src/namescommons/pipeline.py` | `to_links`, `ingest_links` (R4.4) | 2 |
| `src/namescommons/io.py` | `LINK_SCHEMA`, `write_links`, `read_links` | 2 |
| `src/namescommons/schema.py`, `tests/test_io.py` | `SCHEMA_VERSION` 1.1.0 (R4.7) | 2 |
| `tests/test_links.py` | Testes de R4.1, R4.2, R4.4 e R4.5 | 2 |
| `src/namescommons/adapters/wikidata.py` | Consultas, recursos, leitura paginada, `parse` (R1 a R3) e `links` (R4.3) | 3, 4 |
| `tools/amostra_wikidata.py` | Gera a amostra a partir das consultas do módulo | 3 |
| `tests/fixtures/wikidata/`, `tests/fixtures/ATRIBUICAO.md`, `sources.yaml` | Amostra, atribuição e registro da fonte | 3 |
| `tests/test_wikidata.py` | Testes do adaptador | 3, 4 |
| `src/namescommons/cli.py`, `tests/test_cli_pipeline.py` | `--links` em `ingest` e `build`; `variant_links.parquet` em `run` (R4.6) | 5 |
| `.github/workflows/ci.yml`, `tests/test_ci.py` | Passo da CI com o Wikidata (R7) | 5 |

## Cobertura da spec

| Requisito | Tarefa |
|---|---|
| R1 | 3 |
| R2.1, R2.2 | 3 (`test_recursos_e_consultas`) |
| R2.3 | 3 |
| R2.4, R2.5 | 3, 4 |
| R3.1 a R3.7 | 3 |
| R4.1, R4.2, R4.4, R4.5, R4.7 | 2 |
| R4.3 | 4 |
| R4.6 | 5 |
| R5 | 1 |
| R6 | 3, 4 (`tests/test_fontes_oficiais.py` percorre a nova fonte) |
| R7 | 5 |
| R8 | Fechamento (medição com os arquivos reais) |
| Critério de aceite 1 | 3 |
| Critério 2 | 3 |
| Critério 3 | 4 |
| Critério 4 | 3 |
| Critério 5 | 5 |
| Critério 6 | 1 |
| Critérios 7, 8, 9 | Fechamento |
Loading
Loading