# Auditoria 1/8 — Identidade dos registos (i, código p, denominação n, fonte)

Projecto: **MonumentosES** · Data: 2026-09-04 · Agente: 1/8 (painel de auditoria de fidelidade)
Dataset auditado: `www/explorer/bic_data.json` (vista) e `raw/interm/*.json` (intermédios, 15 fontes)

Legenda: **[C]** = verificado por execução de código sobre os ficheiros do repositório · **[I]** = inferido/secundário (indica fundamento).

---

## Âmbito

Identidade dos registos do explorador: unicidade e formato da chave `i`; cobertura da denominação `n`; presença e formato dos códigos (`code_ri`/`code_local` → campo `p` da vista); fidelidade vista↔intermédio (amostragem estratificada + verificações de população total); duplicados internos e inter-fonte por chave normalizada (nome sem acentos/maiúsculas + município), classificando-os como conhecidos/documentados vs. novos. **Não foi corrigido nada** — apenas documentado.

## Método

Tudo re-derivado com Python 3 (json, re, unicodedata, collections), sem consultar o código de ingest para além da documentação de proveniência. Verificações:

1. Reconte de `records` de `bic_data.json`: total, `i` únicos, `i` duplicados, formato `src:localId`.
2. Contagem de `n` vazio por fonte em `raw/interm/<src>.json` e na vista.
3. Contagem de `code_ri`/`code_local` por fonte; validação de `code_ri` contra a regex `^RI-\d{2}-\d{5,7}(-\d{5})?$`.
4. Amostragem estratificada: 20 registos, `random.seed(42)`, alocação proporcional com mínimo de 1 por fonte (15 fontes → `arg:1, ast:1, cant:2, cat:2, clm:1, cyl:2, eiv:1, eus:3, ext:1, gal:1, gva:1, mad:1, nav:1, rio:1, ten:1`); comparação campo a campo (i, p, n, d, m, la, lo) vista↔intermédio. Complemento: comparação de **população total** de `n`, `la`, `lo` entre vista e intermédio.
5. Duplicados: chave = (normalizar(n), normalizar(m)), com NFKD, remoção de acentos, minúsculas, espaços colapsados. Cross-fonte (≥2 fontes diferentes) e internos por fonte, com e sem exigência de `m` preenchido; verificação também sobre a vista; duplicados de `code_ri`; reconciliação com o CSV raw da GVA.

## Achados

### 1. Chaves `i` — únicas [C]

- 24 337 registos na vista (`total` declarado = 24 337 = `len(records)`) · **24 337 `i` únicos · 0 duplicados**.
- Contagens por fonte batem certo vista↔intermédio nas 15 fontes (arg 1021, ast 331, cant 2400, cat 2161, clm 631, cyl 2479, eiv 728, eus 10295, ext 250, gal 723, gva 1185, mad 443, nav 1426, rio 65, ten 199) [C].
- **Desvio de formato:** 12 `i` não respeitam o padrão limpo `src:localId` (contêm espaços, `/`, parênteses, `+`): 10 cat (ex. `cat:1977 / 4991 / 54338-4180-MH`, `cat:56114-4702-MH (Torre)`), 1 ast (`ast:111702001001 / 111702001002`), 1 ext (`ext:2334+crues+bienes muebles`). São identificadores "multi-código" compostos no ingest. [C]

### 2. Cobertura de `n` (denominação) [C]

| Fonte | Items | `n` vazio | Cobertura |
|---|---|---|---|
| **gal** | 723 | **97** | 86,58 % |
| **rio** | 65 | **2** | 96,92 % |
| restantes 13 | — | 0 | 100 % |
| **Total interm** | 24 337 | **99** | 99,59 % |

- **Galiza = 97 confirmado** [C]. Consistente com `docs/fuentes/galicia.md:30`: «109/785 registos com `NOME` vazio (97 Inmoble; 93 "Xenérica"…)» [C doc] — o ingest manteve 723 registos dos quais 97 sem nome.
- **Nova fonte com vazios: La Rioja — `rio:002` e `rio:062`** (não encontrei menção a estes na documentação consultada [I — ausência de referência]).
- Na vista não há nenhum `n` vazio: os 99 recebem o *placeholder* **`(sin denominación)`** (97 gal + 2 rio) [C]. Efeito colateral: ver Achados 5e.

### 3. Códigos (`code_ri` / `code_local` → `p`) [C]

Cobertura de código por fonte (intermédio):

| Fonte | com código | % | `code_ri` | `code_local` | `code_ri` fora do formato |
|---|---|---|---|---|---|
| arg | 1021/1021 | 100 % | 0 | 1021 | 0 |
| ast | 331/331 | 100 % | 204 | 331 | **16** |
| cant | 512/2400 | **21,3 %** | 160 | 512 | **56 (7 distintos)** |
| cat | 2161/2161 | 100 % | 0 | 2161 | 0 |
| clm | 471/631 | 74,6 % | 0 | 471 | 0 |
| cyl | 2479/2479 | 100 % | 0 | 2479 | 0 |
| eiv | **0/728** | **0 %** | 0 | 0 | 0 |
| eus | 10295/10295 | 100 % | 0 | 10295 | 0 |
| ext | 226/250 | 90,4 % | 65 | 215 | 0 |
| gal | 723/723 | 100 % | 565 | 723 | 0 |
| gva | 1185/1185 | 100 % | 0 | 1185 | 0 |
| mad | 443/443 | 100 % | 0 | 443 | 0 |
| nav | 1426/1426 | 100 % | 0 | 1426 | 0 |
| rio | **0/65** | **0 %** | 0 | 0 | 0 |
| ten | 199/199 | 100 % | 0 | 199 | 0 |

Violações da regex `^RI-\d{2}-\d{5,7}(-\d{5})?$` [C]:

- **ast, 16 valores distintos:** 13 com prefixo `ARI-` (`ARI-51-0010450`, `ARI-51-0012124`, `ARI-51-0008308`, `ARI-53-0000662`, `ARI-51-0011518`, `ARI-51-0011514`, `ARI-51-0010812`, `ARI-51-0011519`, `ARI-51-0010418`, `ARI-51-0010444`, `ARI-51-0006954`, `ARI-51-0010447`, `ARI-53-0000025`) + 3 em formato legado `BI…` (`BI000016301`, `BI0030521`, `BI0030619`).
- **cant, 56 ocorrências / 7 valores distintos** — todos com **8 dígitos** no serial (fora do `\d{5,7}`): `RI-51-00004878`, `-00005052`, `-00005202`, `-00006834`, `-00006841`, `-00007322`, `-00008580`, cada um repetido ×8 (ver duplicação de Cantabria abaixo).
- Nota de desenho da vista: o campo `cp` da vista **não é um código** — é cronologia («S. XII», 520 registos: clm 464, rio 56) [C]. O código da vista é `p` = `code_ri` ou, na sua falta, `code_local`; quando não há nenhum, `p` recai no próprio `i` — acontece em **2 865 registos** (cant 1888, eiv 728, clm 160, ext 24, rio 65) [C].

### 4. Amostragem estratificada (20 registos, seed 42) + população total [C]

- **`n`, `la`, `lo`: 0 divergências vista↔intermédio em POPULAÇÃO TOTAL** (24 337 comparados; `la`/`lo` com tolerância 1e-6 e coerência de nulos) [C].
- Na amostra de 20 sinalizadas 14 divergências, **todas de derivação/canonização na vista, nenhuma corrupção de dados** [C]:
  - `p` = fallback para `i` quando não há código: `cant:{BA85…}`, `eiv:0089`, `ext:n7`, `rio:003` (coerente com os 2 865 do ponto 3).
  - `d` canonicalizado na vista: `clm:162` «CIUDAD REAL»→«Ciudad Real»; `eiv:0089` «Illes Balears (Eivissa)»→«Illes Balears»; `eus:196-01059`/`eus:91-01031` «Araba/Álava»→«Álava»; `eus:108-20059` «Gipuzkoa»→«Guipúzcoa»; `gva:12.032-9999-000012` «CASTELLÓN»→«Castellón».
  - `d` **ausente no intermédio e derivado em build**: cat 2161/2161 vazios no interm (vista preenche 2150, ficam 11 vazios) e cyl 2479/2479 vazios (vista preenche 2478, fica 1 vazio) — ex.: `cat:1170-1062-MH` → «Girona», `cyl:221775` → «Soria». A província mostrada ao utilizador não é auditável a partir do intermédio [C o vazio; I a derivação, provável via código INE/município].
- Amostra OK integral: `arg:1191`, `ast:150803001000`, `cant:{FE91…}`, `gal:BIC.000.096`, `mad:RBIC-2025-000006`, `nav:2006`, `ten:143` [C].

### 5. Duplicados (chave = nome normalizado + município) [C]

**a) Cross-fonte (≥2 CCAA diferentes):** 96 grupos, **todos** o par `eus`↔`cyl` e **todos com `m` vazio** — cyl, eus e eiv não têm município no intermédio (100 % vazio; eus traz antes `cm` = código INE, ex. «01001») [C]. Com `m` preenchido exigido: **0 grupos cross** (idem na vista). Conclusão: **não há duplicados cross-CCAA reais detetados**; os 96 grupos são falsos positivos do método sobre nomes genéricos («castillo», «iglesia de san andres», …) por ausência de município [C].

**b) Internos com `m` preenchido** (grupos / registos excedentes):

| Fonte | Grupos | Excedentes | Classificação |
|---|---|---|---|
| **cant** | **291** | **2101** | **NOVO — crítico** (ver c) |
| eus | 0 | 0 | — (com m; sem m: 624 grupos/1633 exc., não qualificável) |
| **arg** | 17 | 17 | **Conhecido**: «42 numera dupl. sufixados» (`relatorio-proveniencia.md:55`) — confirmado: **42 `i` com sufixo numérico**, todos com base existente [C]; por n+m são 17 grupos (ex. `arg:79` tem sufixos -2…-5; 1 grupo **sem** sufixo: `arg:444`/`arg:613`, «Castillo de los Condes de Ribagorza», Estopiñán) |
| **nav** | 20 | 22 | **Conhecido**: «CBIC nav (cross-layer)» (`relatorio-proveniencia.md:93`) — 18 sufixados `-2`, todos com base [C] |
| **cat** | 2 | 2 | 1 conhecido (bem/entorn mesmo IPAC: `cat:37037-4013-MH`/`-MH-2`, documentado em `relatorio-proveniencia.md:93` «CODI_INVENTARI+REGISTRE cat… ≥1 par exacto»; os «311 entorns flagados» = campo `en=1` em 311 items, confirmado [C]) + **1 novo**: `cat:10047-1056-MH`/`cat:10047-4471-MH` («El Mercèr de Toralles», Montagut i Oix) — mesmo n+m com **IPAC distinto** |
| **gva** | 1 | 1 | **Novo** (n+m): `gva:12.100-9999-000013`/`…000020` («Escudo», Sant Mateu) — códigos distintos. O documentado «igpcv ×3» refere o raw: confirmado no CSV 1269 linhas, `igpcv` com **3 pares duplicados** (`00.095-9999-000003`, `03.065-9999-000035`, `46.250-9999-000545`) [C]; no interm só sobrevive 1 par de código (`00.095-9999-000003` + sufixo `-2`) — os restantes terão sido absorvidos pela filtragem das 84 linhas *muebles* [I] |
| **ext** | 1 | 1 | **Conhecido**: «código autonómico ext ×≥1» (`relatorio-proveniencia.md:93`) — `ext:15653`/`ext:RI-53-0000080-00000` («Conjunto Histórico», Jerez de los Caballeros), mesmo BIC com dois esquemas de id |
| cyl | 0 (com m) | 0 | sem m: 100 grupos/504 exc. (ex. «castillo» ×24) — falsos positivos prováveis por falta de m [I] |
| eiv | 0 (com m) | 0 | sem m: 5 grupos/5 exc. (ex. «zona de proteccio arqueologica 02/03/04» ×2 GUIDs cada) — suspeitos reais mas pouco verificáveis sem município [I] |

**c) Cantabria — duplicação massiva (NOVO, não documentado):** 2392 dos 2400 registos (99,7 %) pertencem a grupos duplicados por n+m: **287 grupos de 8 cópias, 2 de 16, 1 de 24 e 1 de 40** («Monasterio de Santo Toribio», Camaleño) [C]. As cópias são idênticas em `code_local`, estado, tipologia, província e coordenadas (ex.: «Abrigo del Cubular», Valderredible, ×8 com `code_local=R.G.-I-Z.A.-0002` idêntico) [C]. Só existem **47 `code_local` distintos em 2400 registos, todos com >1 registo**; 18 valores de `code_ri` são partilhados por >1 registo (1 par em ast, 17 valores em cant ×8) [C]. Padrão de múltiplos de 8 sugere camadas do serviço INSPIRE ingeridas sem dedup [I].

**d) `code_ri` duplicados:** ver c) — 18 valores com >1 registo [C].

**e) Na vista:** o *placeholder* `(sin denominación)` cria **55 colisões artificiais** nome+município (17 grupos entre os 99 registos assim rotulados) — não são erros dos dados-fonte, mas poluem pesquisa/contagem por denominação [C].

## Desvios

**Novos (não documentados — apenas registados aqui, não corrigidos):**
1. **cant:** duplicação massiva — 291 grupos / 2101 excedentes; 47 BICs distintos vs 2400 registos na vista (sect. 5c). É o desvio mais grave encontrado neste eixo.
2. **rio:** 2 registos sem denominação (`rio:002`, `rio:062`), sem menção na doc consultada.
3. **Formato `code_ri`:** 16 violações em ast (`ARI-…` ×13, `BI…` ×3) e 7 valores distintos de 8 dígitos em cant (×8 cópias).
4. **`i` compostos:** 12 registos com `i` multi-código/irregular (10 cat, 1 ast, 1 ext).
5. **gva:** 1 par n+m com códigos distintos («Escudo», Sant Mateu).
6. **cat:** 1 par n+m com IPAC distinto (`10047-1056`/`10047-4471`).
7. **Vista:** `d` (província) de cat/cyl não existe no intermédio (2161 + 2479 vazios) — derivada em build, quebra a auditabilidade camada a camada; 11 (cat) + 1 (cyl) ficam mesmo assim vazias na vista.
8. **Vista:** fallback `p` = `i` em 2865 registos sem código (cant 1888, eiv 728, clm 160, ext 24, rio 65) — degrada a identidade legível do registo.
9. **Vista:** 55 colisões artificiais de nome via `(sin denominación)`.
10. **eiv:** 5 pares suspeitos de duplicado (zonas de proteção arqueológica) não qualificáveis por falta de `m`.

**Vs. documentação (reconciliação):**
- arg «42 sufixados» [C confirmado — 42 `i` sufixados; 17 grupos por n+m, compatível porque há sufixos -2…-5 do mesmo base].
- gva «×3 duplicados» no raw [C confirmado no CSV] — no intermédio sobrevive 1 par de código + 1 par n+m novo; reconciliável com a filtragem de 84 *muebles* [I].
- cat «311 entorns flagados» [C confirmado — `en=1` ×311]; «≥1 par exacto» [C confirmado — par `37037-4013`].
- gal «97 Inmoble sem NOME» [C confirmado — 97 vazios no intermédio].
- ast «ca_codigo ×3» vs encontrado: 5 `i` sufixados + 1 par de `code_ri` + 0 grupos n+m — parcialmente consistente, merece revisão [I].

## Recomendações

(não implementadas — para decisão do painel/proponente)
1. **Dedup de Cantabria** antes de qualquer uso analítico do dataset (2400 → ~47+registos únicos com código); entretanto, marcar `cant` com bandeira de qualidade na vista.
2. Normalizar ou documentar os formatos alternativos de `code_ri` (`ARI-…`, `BI…`, serial de 8 dígitos) — hoje a regex oficial falha em 23 valores.
3. Persistir `d` e `m` nos intermédios de cat, cyl, eus e eiv (para eus, decodificar `cm` INE) — eliminaria os falsos positivos cross-fonte e tornaria a província auditável.
4. Documentar a política de `i` multi-código (12 casos) ou atribuir `i` sintético e guardar os códigos alternativos em campo próprio.
5. Substituir o fallback `p`=`i` por vazio explícito ou bandeira «sem código».
6. Marcar os registos `(sin denominación)` com flag própria em vez de os fazer passar por denominação real.
7. Actualizar `docs/relatorio-proveniencia.md` com: rio ×2 sem nome, gva (3 raw → 1 interm), cat par `10047`, e a duplicação de cant.

## Fontes

Ficheiros locais do repositório (verificados por execução) [C]:
- `C:\Users\Atelier X\Nextcloud\Documents\00_Website\MonumentosES\www\explorer\bic_data.json`
- `C:\Users\Atelier X\Nextcloud\Documents\00_Website\MonumentosES\raw\interm\*.json` (15 ficheiros: aragon, asturias, baleares-eivissa, canarias-tenerife, cantabria, castilla-la-mancha, castilla-y-leon, cataluna, extremadura, galicia, la-rioja, madrid, navarra, pais-vasco, valenciana)
- `C:\Users\Atelier X\Nextcloud\Documents\00_Website\MonumentosES\raw\gva_bic_inmuebles.csv` (reconciliação dos 3 duplicados igpcv)

Documentação de proveniência citada [C doc]:
- `docs/relatorio-proveniencia.md` (linhas 22, 29, 55, 69, 93 — dups arg 42, gva ×3, ast ×3, cat bem/entorn, nav cross-layer, ext ×≥1, 311 entorns)
- `docs/fuentes/galicia.md` (linha 30 — «109/785 registos com NOME vazio (97 Inmoble…)»)
- `docs/fuentes/valenciana.md` (linhas 13, 16, 31 — 1269 linhas = 1185 inmuebles + 84 muebles; 3 duplicados igpcv)
- `docs/fuentes/cataluna.md` (linhas 14, 23–24 — REGISTRE_BCIN/`-EN`=entorn, campo ENTORN SI/NO, 2161 BCIN_ARQUITEC)

Sem fontes web — auditoria totalmente local.

---

## Reprodutibilidade (comandos/queries corridos)

Python 3, a partir da raiz do projecto `C:\Users\Atelier X\Nextcloud\Documents\00_Website\MonumentosES`. Scripts completos usados: `%TEMP%\audit01_identidade.py`, `%TEMP%\audit01_refine.py`, `%TEMP%\audit01_refine2.py` (cópia das queries essenciais):

```python
import json, re, glob, unicodedata, random, csv
from collections import Counter, defaultdict
view = json.load(open("www/explorer/bic_data.json", encoding="utf-8")); recs = view["records"]
interm = {json.load(open(f, encoding="utf-8"))["src"]:
          {it["i"]: it for it in json.load(open(f, encoding="utf-8"))["items"]}
          for f in glob.glob("raw/interm/*.json")}

def norm(s):  # chave de duplicados
    s = unicodedata.normalize("NFKD", str(s or ""))
    s = "".join(c for c in s if not unicodedata.combining(c))
    return re.sub(r"\s+", " ", s.lower()).strip()

# 1. unicidade de i
ids = [r["i"] for r in recs]
len(ids), len(set(ids)), {k: v for k, v in Counter(ids).items() if v > 1}          # 24337, 24337, {}
[i for i in ids if not re.fullmatch(r"[a-z]{2,4}:\S+", i)]                          # 12 i irregulares

# 2. n vazio por fonte
{src: sum(1 for it in m.values() if not (it.get("n") or "").strip()) for src, m in interm.items()}
# vista: sum(1 for r in recs if not (r["n"] or "").strip())  -> 0; Counter(r["n"] for r in recs if r["n"]=="(sin denominación)")

# 3. códigos + formato RI
RI = re.compile(r"^RI-\d{2}-\d{5,7}(-\d{5})?$")
{src: (sum(1 for it in m.values() if (it.get("code_ri") or "").strip() or (it.get("code_local") or "").strip()),
       sum(1 for it in m.values() if (it.get("code_ri") or "").strip() and not RI.fullmatch(it["code_ri"].strip())))
 for src, m in interm.items()}

# 4. amostra estratificada seed 42 + população total n/la/lo
random.seed(42)
alloc = {s: max(1, round(20 * sum(1 for r in recs if r["src"] == s) / len(recs))) for s in interm}
# (ajuste manual para soma 20: arg1 ast1 cant2 cat2 clm1 cyl2 eiv1 eus3 ext1 gal1 gva1 mad1 nav1 rio1 ten1)
# por registo: comparar r=by_i[it["i"]] em i, p=(code_ri or code_local), n, d, m e abs(la-la)<1e-6, abs(lo-lo)<1e-6
# população total: n diferente 0; la/lo divergentes 0 (24337 comparados)

# 5. duplicados
key = lambda it: (norm(it.get("n")), norm(it.get("m")))
cross = defaultdict(set)
for src, m in interm.items():
    for it in m.values():
        if key(it)[0]: cross[key(it)].add(src)
{k: v for k, v in cross.items() if len(v) >= 2}                                     # 96, todos eus∪cyl, m=''
# internos: agrupar por fonte com key(); repetir exigindo key(it)[1] != ''
# cant: Counter(len(v) for v in dup_groups.values()) -> {8: 287, 16: 2, 24: 1, 40: 1}
# code_ri duplicados: Counter de code_ri não vazio; 18 valores com >1 registo

# reconciliação gva raw
rows = list(csv.DictReader(open("raw/gva_bic_inmuebles.csv", encoding="utf-8-sig"), delimiter=";"))
len(rows)                                                                            # 1269
Counter(r["igpcv"] for r in rows if r["igpcv"].strip()).most_common(3)
# [('00.095-9999-000003', 2), ('03.065-9999-000035', 2), ('46.250-9999-000545', 2)]
```

Comandos de apoio: `grep -n "97 Inmoble" docs/fuentes/galicia.md`; `grep -n "42 numera dupl" docs/relatorio-proveniencia.md`; `grep -n "3 duplicados" docs/fuentes/valenciana.md`.
