Files
Investidor-V2/tools/portal-audit-geometry-gaps.py
nala 01405cbf8a
tests / ci (push) Canceled after 0s
oportunidades 5
2026-10-10 14:15:28 +02:00

76 lines
7.6 KiB
Python

"""Reconcile existing named gaps with authenticated QA proof; retain earlier names/counts."""
import hashlib
import json
from pathlib import Path
root = Path(__file__).resolve().parent.parent
private = root / 'storage/app/private/investmoz-v2/portal-audit'
docs = root / 'docs/investmoz/v2/auditoria-portal'
proof = json.loads((private / 'geometry-crosswalk-http-qa.json').read_text(encoding='utf-8-sig'))
preservation = json.loads((private / 'geometry-crosswalk-preservation-qa.json').read_text(encoding='utf-8-sig'))
consumer_http = json.loads((private / 'geometry-crosswalk-consumers-http-qa.json').read_text(encoding='utf-8-sig'))
assert proof['applied'] and not proof['published'] and not proof['interface_observed'] and proof['active_writes'] == 0
assert preservation['target'] == proof['target'] and preservation['catalogue_preserved'] and preservation['original_geometries_preserved'] == 361
assert consumer_http['target'] == proof['target'] and not consumer_http['applied'] and not consumer_http['interface_observed']
assert len(consumer_http['consumer_recovery']) == len(consumer_http['public_recovery']) == 32
plans = {p['level']: p for p in proof['plans']}
names = {1: 'Províncias', 2: 'Distritos', 3: 'Postos administrativos'}
gaps, explicit = [], []
for level in [1, 2, 3]:
path = private / f'moz-adm{level}-ine-20190607.geojson'
assert hashlib.sha256(path.read_bytes()).hexdigest() == plans[level]['source_sha256']
source = json.loads(path.read_text(encoding='utf-8-sig'))
rows = {r['external_id'].rsplit(':', 1)[1]: r for r in plans[level]['rows']}
for feature in source['features']:
p = feature['properties']
r = rows[p[f'ADM{level}_PCODE']]
if r.get('correspondence') and r['status'] in ['insert', 'reuse']:
explicit.append({'pcode': p[f'ADM{level}_PCODE'], 'source_name': r['name'], 'id': r['location_id'], **r['correspondence']})
if r['status'] in ['insert', 'reuse']:
continue
reason = {'parent_unresolved': 'Pai do exemplar sem correspondência; não associa por nome isolado',
'name_unresolved': 'Nome sem correspondência única sob o pai resolvido',
'ambiguous_name': 'Mais de um cadastro com mesmo nome/pai; ambos preservados',
'invalid_geometry': 'Geometria inválida; original sem reparação silenciosa'}.get(r['status'], r['status'])
gaps.append({'level': level, 'name': r['name'], 'pcode': p[f'ADM{level}_PCODE'], 'parent_name': p.get(f'ADM{level-1}_PT'),
'parent_pcode': p.get(f'ADM{level-1}_PCODE'), 'province': p['ADM1_PT'], 'reason': reason, 'status': r['status'], 'candidates': r['candidates']})
resolved = sum(p['resolved'] for p in plans.values())
expected = sum(p['expected'] for p in plans.values())
assert expected - resolved == len(gaps) and preservation['total_active'] == resolved
text = ['# Correspondências cartográficas — estado atual e histórico', '',
'10/10/2026. Complemento de AUD-GEO-02/03 na mesma matriz. QA18097/desk_restore_20261009_174726_test:55441. Esperado refere o exemplar INE/UNOCHA de07/06/2019 aceite, não uma divisão administrativa atual certificada.', '',
'| Nível | Esperado | Resolvido QA | Pendente | Incremento sobre09/10 |', '| --- | ---: | ---: | ---: | ---: |']
for level in [1, 2, 3]:
p = plans[level]
text.append(f"| {names[level]} | {p['expected']} | {p['resolved']} | {p['pending']} | +{p['resolved'] - {1:9,2:120,3:232}[level]} |")
text += ['', f'**{resolved} resolvidos/{len(gaps)} pendentes, de {expected} elementos.** As 32 novas geometrias são 2+8+22; as 361 anteriores e os 1990 registos territoriais foram preservados no retrato. O seletor recupera 1920 referências de quatro níveis:11+148+437+1324; os 70 países do cadastro não integram esse denominador. Sem geometrias de localidades neste exemplar. Cadastro, geometria autorizada e apresentação funcional são critérios independentes.', '',
'## Correspondências explícitas', '', '| INE | Nome do exemplar | Cadastro/código | ID existente | Fundamento |', '| --- | --- | --- | --- | --- |']
for r in explicit:
text.append(f"| {r['pcode']} | {r['source_name']} | {r['target_name']}/{r['target_source_code']} | {r['id']} | {r['basis']} |")
text += ['', 'Configuração versionada config/geometry-correspondences.php; fonte/hash/edição/nome/código/pai entram na prévia assinada. CódigosINE e workbook não são convertidos aritmeticamente. Zero renomeações, novos territórios, mudanças de pai ou publicação. Cidade De Maputo/MZ0601 é agregado de2019: não se associa a um dos distritos urbanos cadastrados.', '',
'## Mandie — ambiguidade preservada', '',
'MZ040402/Mandie sob Guro tem candidatos4944a651-23ec-5d51-a5ff-14b0b53ed672 (source_code02) e ea7be18f-2a93-5416-a07a-c207a700e070 (03). Workbook aceite:MANDIE nas linhas11117 e11138, códigos diferentes. Sufixo do P-code não prova equivalência de códigos. Conferir correspondência documental específica; não fundir cadastros. Não bloqueia correspondentes únicos independentes.', '']
for level in [1, 2, 3]:
items = [r for r in gaps if r['level'] == level]
text += [f'## {names[level]} — {len(items)} pendentes atuais', '', '| Código | Nome exato | Pai | Causa |', '| --- | --- | --- | --- |']
for r in items:
text.append(f"| {r['pcode']} | {r['name']} | {r['parent_name'] or '—'} | {r['reason']} |")
text.append('')
text += ['## Provas e critérios abertos', '',
'- geometry-crosswalk-http-qa.json: aplicação autenticada/MFA, replay e API do mapa para22postos novos. A tentativa anterior aplicou2províncias+8distritos antes de parar na ambiguidadeMandie; recibos/eventos originais estão na prova de preservação. O tratamento foi corrigido para manter ambiguidades pendentes e importar as correspondências únicas.',
'- geometry-crosswalk-preservation-qa.json: alvo guardado,393 internos,32novos,361anteriores/1990cadastros preservados. geometry-consumers-path-qa.json recupera393/393 por serviço READ ONLY e0limites privados públicos. Não é prova de publicação ou desenho.',
'- geometry-crosswalk-consumers-http-qa.json: nova consulta dos 32 limites inseridos, pelo endpoint usado pelo componente, recupera 32 para gestor/MFA e nenhum para público; as duas províncias Maputo têm correspondência única por nome exato. Não aplicou outro lote e não demonstra renderização.',
'- TerritoryFields e ProvinceSelect partilhados usam os mesmos IDs/endpoints/preview. ProvinceSelect exige nome exato/único; conserva texto histórico e explica ausência. TypeScript/build passam; seleção/gravação/recuperação e Leaflet por consumidor exigem interface.',
'- Reversão recusa alteração/publicação/FK e descendentes ativos da edição. Ordem:postos→distritos→províncias, só lotes novos; originais/cadastros/histórico preservados. Identidades revertidas não são ressuscitadas por replay.', '']
destination = docs / 'correspondencias-cartograficas.md'
old = destination.read_text(encoding='utf-8-sig')
marker = '## Histórico09/10 — nomes e contagens anteriores'
history = old.split(marker, 1)[1].strip() if marker in old else old.strip()
destination.write_text('\n'.join(text) + '\n' + marker + '\n\n' + history + '\n', encoding='utf-8')
named = private / 'geometry-named-gaps.json'
archive = private / 'geometry-named-gaps-20261009.json'
if named.exists() and not archive.exists():
archive.write_bytes(named.read_bytes())
named.write_text(json.dumps(gaps, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({'expected': expected, 'resolved': resolved, 'pending': len(gaps), 'named_gaps': 'correspondencias-cartograficas.md', 'active_writes': 0}))