# 08 — Agregados e ficheiros de análise vs dataset — auditoria de fidelidade

- **Painel:** MonumentosES — auditoria de fidelidade, agente 8/8
- **Data:** 2026-09-04
- **Estado auditado:** pós-desdup Cantabria ×8 (2.400 → 300): **22.237** records, **21.506** com coordenadas, `cant=300` [C]
- **Datasets auditados:** `www/explorer/bic-stats.json`, `www/explorer/bic_data.json`, `raw/interm/*.json`, `raw/prot_dates.json`, `www/data/bic-{temporal,patterns,heatmap,sunburst,aggregated}.json`, `www/dumps/` (manifest + CSV + JSON + .bib + .ttl), `site/dist/`, `www/`
- **Marcações:** [C] = confirmado programaticamente no estado atual do repositório; [I] = inferido/secundário
- **Nota de conjugação:** os relatórios 01–03 e 05 (agentes anteriores) descrevem o estado **pré**-desdup (24.337 records / 23.606 coords / cant 2.400); este relatório audita o estado **actual** (22.237 / 21.506 / 300). A desdup é visível em `tools/ingest_cantabria.py` (dedup por `seen`, linhas 45–65) e em `raw/interm/cantabria.json` (`count: 300`, 300 ids únicos) [C]; `docs/relatorio-proveniencia.md` still registra a estimativa pré-desdup «Cantábria ~2.400» [C].

---

## Âmbito

1. **`www/explorer/bic-stats.json`** — re-derivação **independente** (Python, stdlib) das 10 métricas e comparação com (a) recontagens diretas de `bic_data.json` e (b) o terreno dos intermédios `raw/interm/*.json`, onde residem os campos que definem as fórmulas (`code_ri`, `pd`/`disp`, `d` pós-`norm_prov`).
2. **Agregados de análise em `www/data/`** — `bic-temporal.json` (soma de fichas == nº de datas, 7.926), `bic-patterns.json` (séculos, décadas labels vs data, sagrado/profano), `bic-heatmap.json` (matrizes século×província e século×categoria), `bic-sunburst.json` (soma recursiva == 22.237), `bic-aggregated.json` (idem).
3. **`www/dumps/manifest.json`** — `total_records` == 22.237? `with_protection_date` == 7.926? sha256/bytes/rows dos 4 ficheiros (o sha256 da auditoria programática é re-confirmado aqui por recálculo).
4. **Consistência numérica do site** — grep em `site/dist/` (e `www/`) por resíduos `24 337 | 24,337 | 24.337 | 23 606 | 2 400 edificios`; confirmação da presença dos números novos (22 237 / 21 506).

## Método

- Python 3 (stdlib: `json`, `csv`, `hashlib`, `collections`, `re`), **só leitura** — nenhum ficheiro de dados foi reescrito [C].
- **Re-derivação em duas camadas:** primeiro recontagem direta dos records FR (`bic_data.json`); depois, como as stats usam campos internos que a vista FR compacta (ex.: `code_ri` → `p`; `pd`/`disp`/`pb` → `pr`), a fórmula exata foi confirmada por leitura de `tools/build_dataset.py` (linhas 95–99) e re-derivada a partir de `raw/interm/*.json` (15 fontes, soma declarada = 22.237 [C]).
- **Datas de proteção:** re-parse independente do campo `pr` de todos os 22.237 records com o módulo de regexes de `tools/build_prot_dates.py` (importado, não copiado), comparado com `raw/prot_dates.json` e com o campo `fecha_proteccion` do dump.
- **Heatmap/aggregated:** cross-tabs século-de-proteção × província (`d`, com fallback `r`) e século × categoria (`t1` canónico via `tools/t1_canon.json`) rederivados célula a célula a partir de `bic_data.json` + `prot_dates.json`; século com a mesma convenção do gerador (`y//100+1`, exceto anos divisíveis por 100).
- **Sunburst:** soma recursiva da árvore (folhas `size`), comparada por nível (t1 canónico → t2) com contagens do dataset.
- **Manifest:** sha256 + bytes recalculados dos 4 ficheiros; CSV contado com `csv.reader` (delimitador `;`, BOM); SKOS contado por ocorrências de `a skos:Concept`; o **código actual do gerador do .bib foi executado isoladamente** para testar reprodução do conteúdo.
- **Site:** greps com contexto (±18 chars) para separar resíduos reais de falsos positivos (substrings de coordenadas); md5 `www/` ↔ `site/dist/` para mapear o artefacto servido; leitura de `www/js/patterns.js` contra o JSON servido para validar o contrato código↔dados.

## Achados

### 1. `bic-stats.json` — 9 de 10 métricas conformes; `with_province` obsoleto

| Métrica | Site | Re-derivado | Estado |
|---|---|---|---|
| `total` | 22.237 | 22.237 (interm: soma declarada também 22.237) | ✔ 0 [C] |
| `per_src` (15 fontes) | arg 1.021 · eus 10.295 · **cant 300** · … | idêntico (Counter sobre `src`) | ✔ 0 [C] |
| `per_ccaa` (15 CCAA) | Euskadi 10.295 · CyL 2.479 · … Cantabria 300 | idêntico | ✔ 0 [C] |
| `with_coords` | 21.506 | 21.506 (`la`/`lo` ≠ null) — `bic-geo.json`: 21.506 pontos, 0 outliers | ✔ 0 [C] |
| `with_code_ri` | 854 | 854 (`code_ri` nos intermédios) = **838 `RI-…` + 13 `ARI-…` + 3 `BI00…` (todas ast)** | ✔ 0 [C] |
| `with_protection_act_or_date` | 5.867 | 5.867 (`pd` **ou** `disp` nos intermédios) | ✔ 0 [C]¹ |
| `cat_entorns_flagged` | 311 | 311 (records `cat` com `en==1`) | ✔ 0 [C] |
| `with_province` | **17.597** | **22.225** no dataset actual (17.597 pré-PIP + 4.628 preenchidos) | ✘ obsoleto — **D1** [C] |
| `t1_distinct` | 77 | 77 | ✔ 0 [C] |
| `with_image` (em `bic_data.json`) | 530 | 530 (campo `img` ≠ null) | ✔ 0 [C] |

¹ Não é erro: é outra definição. `pr` não vazio no dataset actual = 8.360 (inclui records só com `pb`, o referência de publicación); a stat conta `pd|disp` por `build_dataset.py:96` [C].

Notas [C]:
- A contagem de RI por prefixo em `p` da vista FR daria **838** — os 16 restantes são códigos asturianos sem o prefixo `RI-` (`ARI-51/53-…` ×13, `BI00…` ×3). A stat de 854 está correta face à sua definição.
- **Cantabria 300 vs 301:** `per_ccaa` Cantabria = 300 mas o filtro `departements` tem «Cantabria: 301» — o record extra é `cyl:7806` (IGLESIA PARROQUIAL DE SANTIAGO, `r`=Castilla y León, `d`=Cantabria, la 42.81/lo −4.17). Explicado, não é erro de contagem; é um caso de fonte vs PIP já documentado pelo agente 4/8.
- **Causa de D1:** `tools/prepare_map_points.py` reescreve `bic_data.json` com as províncias preenchidas por PIP («reescrito com {filled} `d` preenchidos») mas **nenhum passo regenera `bic-stats.json`**; mtimes: stats 15:13 < `bic_data.json` 15:15 [C]. A stats é consumida pela página `/api` (listagem de ficheiros) e é publicada para reutilização [C].

### 2. Agregados `www/data/` — 4 de 5 exatos; `bic-patterns.json` quebra a página `/patterns`

| Ficheiro | Métrica | Site | Re-derivado | Estado |
|---|---|---|---|---|
| bic-temporal | nº fichas | 7.926 | 7.926 = nº de datas em `raw/prot_dates.json`; ids únicos, todos ⊆ dataset e todos com data | ✔ 0 [C] |
| bic-temporal | janela `[s,e]` por ficha | 39 fichas com ano de protecção fora da janela | 38 do ano **2000** + 1 do ano **1900** (convenção de século vs intervalos 1800–1899/1900–1999/2000–2099) | **D4** [C] |
| bic-patterns | `centuries` | labels `19e/20e/21e`, data [38, 5.819, 2.069], soma 7.926 | re-derivação por ano de protecção: {19: 38, 20: 5.819, 21: 2.069} — **exato** | ✔ 0 [C]**/**D7 |
| bic-patterns | `protections` | 18 décadas, soma 7.926 | 18 décadas **exatas** célula a célula (1840s: 2 … 2020s: 221); labels == dados | ✔ 0 [C] |
| bic-patterns | `sacredData`+`secularData` | 1.429 + 6.497 = 7.926 | 7.926 — 0 não classificados | ✔ 0 [C] |
| bic-patterns | `builders` | `{centuries, firms}` — **sem chave `persons`** | `patterns.js:42` faz `data.builders.persons.map(...)` → TypeError → o `.catch` (linha 67) **substitui a `.pat-grid` inteira por mensagem de erro** → **página `/patterns` não renderiza nenhum gráfico** [C por contrato código+dados; verificado por leitura de `www/js/patterns.js` (idêntico em dist, md5) + JSON servido] | **D3** |
| bic-heatmap | `matrixDept` soma | 7.926 | 7.926; **matriz exata** (43 colunas) vs cross-tab (`d` com fallback `r`): as 8 datas sem província ficam nas colunas CCAA (Castilla y León 1, Catalunya 7) | ✔ 0 [C]**/**D5 |
| bic-heatmap | `matrixDom` soma | 7.926 | 7.926; **matriz exata** (21 colunas) vs cross-tab com `t1` canónico (`tools/t1_canon.json`) | ✔ 0 [C] |
| bic-sunburst | soma recursiva `size` | 22.237 | 22.237; **exato** por nível: t1 canónico → t2; «(sin especificar)» 11.291 = 11.283 sem `t1` + 8 mapeados para canónico vazio (`No Especificado` 5, `Inmueble` 1, `Fondo de museo (primera)` 2) | ✔ 0 [C] |
| bic-aggregated | `binsByDomaine` / `binsByDepartement` | somas 7.926 / 7.926 | 7.926 / 7.926; **transpostas exatas** das matrizes do heatmap (verificação célula a célula) | ✔ 0 [C] |

### 3. `www/dumps/manifest.json` — contagens conformes; `.bib` citável está corrompido

- `total_records`: **22.237** ✓ [C]; `with_protection_date`: **7.926** ✓ [C] — igual a `len(raw/prot_dates.json)`; o re-parse independente do campo `pr` reproduz **exatamente** 7.926 datas (0 divergências nas datas guardadas; resíduo não-parseável 434, coerente com 8.360 `pr` não vazios) [C].
- sha256 recalculado dos 4 ficheiros: **todos OK**; bytes: todos OK [C].
- CSV: 22.237 linhas de dados + header, 16 colunas [C]; JSON dump: `total` 22.237, 22.237 records, ids == dataset [C].
- **Desvio menor:** `files["monumentoses-skos.ttl"].rows = 22237` — o TTL tem **24** `skos:Concept` (vocabulário, não linhas de dataset); o gerador copia `len(rows)` para qualquer ficheiro não-.bib (`build_dumps.py`) [C] — **D6**.
- **Desvio grave (ficheiro citável):** `www/dumps/monumentoses-dataset.bib`:
  1. a nota diz «**2422.237** bienes inmuebles BIC» — resíduo do «24» de 24.337 na f-string `f"note = {{24{len(rows):,} …"`;
  2. o `.replace(",", ".")` (aplicado para converter 22,237 → 22.237) converteu **todas as vírgulas do BibTeX em pontos** (`@misc{monumentoses20260903.` · `year = {2026}.`) → **BibTeX inválido**;
  3. **reproduzido deterministicamente** executando o código actual de `tools/build_dumps.py` (com `len(rows)=22237`): a saída é idêntica ao ficheiro publicado [C] — **D2**.

### 4. Números no site — 0 resíduos reais nas páginas; 1 resíduo no dump

- Grep `24[ ,.]?337|23[ ,.]?606|2[ ,]?400 edificios` em `site/dist/`: **apenas** nos `.md` de auditoria publicados (`audit/01-identidade.md`, `02-datacao.md`, `03-protecao.md`, `05-coordenadas.md`) — citações históricas do estado pré-correcção, não páginas utilizador. **0 ocorrências em HTML/JS** [C].
- Matches de `24337`/`23606` em `www/explorer/bic_data.json`, `bic-geo.json` e no dump JSON são **falsos positivos**: substrings de coordenadas (`lat: 43.243377`, `43.236069`) [C].
- Números novos presentes e consistentes: «22 237» / «21 506» em `index.html`, `api/`, `mapa/`, `metodologia/`, `tipologias/`, `explorador/` (e «22.237» na formatação DE) [C].
- **Porém:** o resíduo real que sobreviveu à correcção é o `.bib` publicado (D2, «2422.237») — exatamente o tipo de resíduo que a correcção 24.337→22.237 visava eliminar [C].
- Observação de infra-estrutura: `site/dist/` **não contém** `data/`, `explorer/` nem `dumps/` (só `site/public/` é copiado); o artefacto completo servido é `www/` (HTML do Astro + `_astro` + `js` + `data` + `explorer` + `dumps`); `patterns/index.html`, `js/patterns.js`, `explorer.js`, `index.html` são md5-idênticos entre `www/` e `site/dist/` [C].

## Desvios

| # | Desvio | Gravidade | Evidência |
|---|---|---|---|
| D1 | `bic-stats.json` `with_province: 17597` está obsoleto — o dataset actual tem 22.225 records com província (PIP preencheu 4.628 depois de a stats ser escrita) | **Média** — número público errado (usado por terceiros/página `/api`) | `www/explorer/bic-stats.json` vs recontagem; `tools/prepare_map_points.py` [C] |
| D2 | `monumentoses-dataset.bib` inválido: nota «2422.237 bienes» (resíduo de 24.337) + todas as vírgulas BibTeX convertidas em pontos | **Alta** — é o ficheiro de citação académica do dataset | `www/dumps/monumentoses-dataset.bib`; reprodução do gerador [C] |
| D3 | `/patterns` quebrada em produção: JSON tem `builders` sem `persons`; `patterns.js:42` lança TypeError e o `.catch` apaga a grelha de gráficos toda | **Alta** — página utilizador morta | `www/data/bic-patterns.json` + `www/js/patterns.js` (contrato verificado) [C] |
| D4 | `bic-temporal.json`: 39 fichas (38×ano 2000, 1×ano 1900) com ano de protecção fora do intervalo `[s,e]` apresentado (convenção de século inconsistente entre geradores) | **Baixa** — 0,49% das fichas; visível na cronologia | `www/data/bic-temporal.json` vs `raw/prot_dates.json` [C] |
| D5 | Eixo de províncias do heatmap/aggregated usa valores crus por normalizar só um dicionário mínimo: `A` (gal:BIC.000.785), `Terue` (arg:1610, arg:1675), `BADAJOZ`, `HUESCA`/`Huesca`, `ZARAGOZA`/`Zaragoza`, `TERUEL`/`Teruel`, `A Coruña / Lugo`… — rótulos ilegíveis/duplicados na UI | **Média-baixa** — cosmética mas pública | `www/data/bic-heatmap.json` `departements` (43 valores) [C] |
| D6 | `manifest.json` declara `rows: 22237` para `monumentoses-skos.ttl`, que tem 24 conceitos | **Baixa** — metadata enganosa | `www/dumps/manifest.json`; contagem `skos:Concept` [C] |
| D7 | Rótulos de século em francês (`19e/20e/21e`) em `bic-patterns.json` e gerados em JS (`c + 'e'`, `patterns.js:26,38,60`) — herança do fork FR, visíveis em `/patterns` | **Baixa** — i18n | JSON + JS [C] |

**Não são desvios** (verificados, por eliminação de dúvida): `with_code_ri` 854 (inclui 16 códigos sem prefixo `RI-`); `with_protection_act_or_date` 5.867 vs 8.360 `pr` (definições `pd|disp` vs `pd|disp|pb`); Cantabria 300 (CCAA) vs 301 (província, `cyl:7806`); builders a zero (documentado em `meta.note` como «extracción de personas pendiente»).

## Recomendações

1. **R1 (D1):** regenerar `bic-stats.json` no **fim** do pipeline (após `prepare_map_points.py`), ou mover o cálculo das stats para um passo final único; idealmente, nenhum passo deve reescrever `bic_data.json` sem atualizar os agregados derivados. Atualização imediata: `with_province` → 22.225 (e considerar publicar também o pré-PIP como `with_province_source`).
2. **R2 (D2):** corrigir `tools/build_dumps.py`: remover o prefixo literal «24» e o `.replace(",", ".")` global; formatar apenas o número (ex.: `f"{n:,}".replace(",", ".")` isolado da estrutura BibTeX) e regenerar `.bib` + manifest. Validar com um parser BibTeX.
3. **R3 (D3):** alinhar o contrato `builders`: emitir `"persons": []` no JSON (coerente com «a cero») **ou** guardar o acesso em `patterns.js` (`data.builders.persons && …`). Acrescentar um teste de fase (schema JSON de `/data/*.json` vs consumers JS) para impedir regressões — o `.catch` que apaga a página inteira também merece ser granularizado por gráfico.
4. **R4 (D5):** normalizar o eixo província antes de construir `departements` (aplicar `PROV_FIX` + dedup case-insensitive + correção manual de `Terue`→Teruel, `A`→(sem/vide fonte)) ou corrigir na origem as fichas `arg:1610`, `arg:1675`, `gal:BIC.000.785`.
5. **R5 (D4):** fixar UMA convenção de século (ex.: séc. XX = 1901–2000; ano 1900 → séc. XIX) e aplicá-la em `build_prot_dates`/`build_patterns_es`/temporal para que `[s,e]`, séculos e décadas fiquem mutuamente consistentes; documentá-la em `meta`.
6. **R6 (D6):** no manifest, `rows` do `.ttl` deve ser o nº de conceitos (24) ou `null`.
7. **R7 (D7):** traduzir os rótulos de século para ES (`s. XIX`) no JSON ou no render (há `t()` disponível).
8. **R8:** nota editorial nos `.md` de auditoria publicados em `/audit/` (contêm 24.337/23.606 como histórico) — marcar explicitamente «números do estado auditado, já corrigidos» para não confundir leitores externos.
9. **R9:** eliminar a bifurcação `www/` ↔ `site/dist/` (dist sem `data/`, `explorer/`, `dumps/`): ou copiar esses diretórios para `site/public/`, ou documentar `www/` como única raiz de deploy com passo de sincronização explícito.

## Fontes

Tudo verificado no repositório local `C:\Users\Atelier X\Nextcloud\Documents\00_Website\MonumentosES` (estado 2026-09-04, pós-commits abaixo) — sem fontes externas:

- Dados: `www/explorer/bic-stats.json`, `www/explorer/bic_data.json`, `www/explorer/bic-geo.json`, `raw/interm/*.json` (15 fontes), `raw/prot_dates.json`
- Agregados: `www/data/bic-temporal.json`, `bic-patterns.json`, `bic-heatmap.json`, `bic-sunburst.json`, `bic-aggregated.json`, `tools/t1_canon.json`
- Dumps: `www/dumps/manifest.json`, `monumentoses-bic-2026-09-03.csv`, `monumentoses-bic-2026-09-03.json`, `monumentoses-dataset.bib`, `monumentoses-skos.ttl`
- Geradores (definições das fórmulas): `tools/build_dataset.py` (stats, linhas 95–99; `norm_prov`), `tools/build_site_view.py`, `tools/prepare_map_points.py` (PIP + reescrita de `bic_data.json`), `tools/build_prot_dates.py` (regex de datas), `tools/build_dumps.py` (manifest + .bib), `tools/build_patterns_es.py`, `tools/ingest_cantabria.py` (dedup)
- Site: `site/dist/`, `www/js/patterns.js`, `www/patterns/index.html`, `site/src/page-components/Api.astro`, `www/api/index.html`
- Contexto do painel: `site/public/audit/05-coordenadas.md` (estado pré-desdup), `docs/relatorio-proveniencia.md`, `www/audit/08-agregados.md` (versão FR do fork, a substituir)
- Git: `dd3d20b` (Fase D.1 dumps), `835bb9f` (Fase D.2 análises), `f187d73` (Fase D.3 SKOS)

## Comandos de reprodução

```bash
cd "C:/Users/Atelier X/Nextcloud/Documents/00_Website/MonumentosES"

# 1. Recontagens vs bic-stats.json (total, coords, CCAA, província, RI, datas)
python - << 'EOF'
import json, re
from collections import Counter
from pathlib import Path
recs = json.load(open('www/explorer/bic_data.json', encoding='utf-8'))['records']
stats = json.load(open('www/explorer/bic-stats.json', encoding='utf-8'))
items = [it for f in sorted(Path('raw/interm').glob('*.json'))
         for it in json.loads(f.read_text(encoding='utf-8'))['items']]
print('total', len(recs), stats['total'])
print('with_coords', sum(1 for r in recs if r.get('la') is not None), stats['with_coords'])
print('per_ccaa', dict(Counter(r['r'] for r in recs)) == stats['per_ccaa'])
print('with_province actual', sum(1 for r in recs if r.get('d')), 'stats(stale)', stats['with_province'])
print('with_code_ri', sum(1 for i in items if i.get('code_ri')), stats['with_code_ri'],
      '| prefixo RI- em p:', sum(1 for r in recs if str(r.get('p','')).startswith('RI-')))
print('with_prot(pd|disp)', sum(1 for i in items if i.get('pd') or i.get('disp')),
      stats['with_protection_act_or_date'], '| pr nao vazio:', sum(1 for r in recs if r.get('pr')))
print('cat_entorns', sum(1 for r in recs if r.get('src')=='cat' and r.get('en')==1))
EOF

# 2. Datas: re-parse independente == 7926?
python - << 'EOF'
import json, importlib.util
spec = importlib.util.spec_from_file_location('bpd', 'tools/build_prot_dates.py')
bpd = importlib.util.module_from_spec(spec); spec.loader.exec_module(bpd)
recs = json.load(open('www/explorer/bic_data.json', encoding='utf-8'))['records']
out = {r['i'] for r in recs if r.get('pr') and bpd.parse(r['pr'])}
print('datas extraidas:', len(out))  # 7926
EOF

# 3. Somas dos agregados (temporal 7926, heatmap 7926+7926, sunburst 22237, aggregated 7926+7926)
python - << 'EOF'
import json
def s(n):
    return sum(s(c) for c in n['children']) if 'children' in n else n.get('size',0)
t = json.load(open('www/data/bic-temporal.json', encoding='utf-8'))
h = json.load(open('www/data/bic-heatmap.json', encoding='utf-8'))
sb = json.load(open('www/data/bic-sunburst.json', encoding='utf-8'))
a = json.load(open('www/data/bic-aggregated.json', encoding='utf-8'))
p = json.load(open('www/data/bic-patterns.json', encoding='utf-8'))
print('temporal', len(t['monuments']))
print('matrixDept', sum(map(sum,h['matrixDept'])), 'matrixDom', sum(map(sum,h['matrixDom'])))
print('sunburst', s(sb))
print('agg', sum(sum(v for k,v in b.items() if k!='century') for b in a['binsByDomaine']),
      sum(sum(v for k,v in b.items() if k!='century') for b in a['binsByDepartement']))
print('patterns centuries', sum(p['centuries']['data']), 'protections', sum(p['protections']['data']))
EOF

# 4. Manifest: sha256/bytes/rows + linhas CSV
python - << 'EOF'
import json, hashlib, csv
from pathlib import Path
D = Path('www/dumps'); man = json.load(open(D/'manifest.json', encoding='utf-8'))
print(man['total_records'], man['with_protection_date'])
for n, i in man['files'].items():
    ok = hashlib.sha256((D/n).read_bytes()).hexdigest() == i['sha256']
    print(n, 'sha256 OK' if ok else 'DIFERE', i['bytes'])
with open(D/'monumentoses-bic-2026-09-03.csv', encoding='utf-8-sig', newline='') as f:
    print('CSV data rows:', sum(1 for _ in csv.reader(f, delimiter=';')) - 1)
EOF

# 5. Residuos de numeros antigos no site (falsos positivos = coordenadas 43.24xxxx)
grep -rEo "24[ ,.]?337|23[ ,.]?606|2[ ,]?400 edificios" site/dist | sort | uniq -c
grep -rEo ".{18}(24[ ,.]?337|23[ ,.]?606).{18}" www --include="*.json" | grep -v audit
grep -rEo "22[ ,.]?237|21[ ,.]?506" site/dist --include="*.html" | sort | uniq -c | head

# 6. Paridade www vs dist (artefacto servido)
md5sum www/js/patterns.js site/dist/js/patterns.js www/patterns/index.html site/dist/patterns/index.html
ls site/dist/data site/dist/explorer site/dist/dumps 2>&1 | head -3
```
