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

313 lines
38 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Current QA increment in the existing matrix and reports, with source conservation."""
import collections
import csv
import hashlib
import json
import re
import sys
from pathlib import Path
from datetime import datetime, timezone
ROOT = Path(__file__).resolve().parent.parent
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
DOCS = ROOT / 'docs/investmoz/v2/auditoria-portal'
csv.field_size_limit(16 * 1024 * 1024)
sys.stdout.reconfigure(encoding='utf-8')
def read(name):
return json.loads((PRIVATE / name).read_text(encoding='utf-8-sig'))
def readable(text):
replacements = {
'ClamDreal': 'ClamD real', 'HTTP18097': 'HTTP 18097', 'HTTP/MFA': 'HTTP com MFA',
'dump consistente→nova base': 'dump consistente → nova base', 'MFA/bytes': 'MFA / bytes',
'15inseridos/393reutilizados': '15 inseridos / 393 reutilizados', '15inserts/393reuse': '15 inseridos / 393 reutilizados',
'15limites': '15 limites', '393associações': '393 associações', '1990cadastros': '1.990 cadastros',
'361geometrias': '361 geometrias', '361originais': '361 originais', '47incrementaisprivados': '47 incrementais privados',
'26distritos': '26 distritos', '147postos': '147 postos', '173pendentes': '173 pendentes',
'0contas/grants/flags': 'zero contas, atribuições ou flags', '0documentos': 'zero documentos',
'3coleções': 'três coleções', '4tabelas+1colunausers+3colunasoportunidades': 'quatro tabelas + uma coluna em users + três em opportunities',
'2colunas+1definição publicada19etapas/5fases;0dossiers': 'duas colunas + uma definição publicada de 19 etapas / cinco fases; zero dossiers',
'190000faltantes': '190000 faltantes', '0fixtures QA noativo': 'zero fixtures QA no ativo',
'2contas explicitamente designadas,3.ºgestor comMFAaprova': 'duas contas explicitamente designadas; um terceiro gestor com MFA aprova',
'porperfil': 'por perfil', 'porformulário': 'por formulário', 'porconsumidor': 'por consumidor',
'futura candidata/ativo apósvisual': 'futura candidata e do ativo após demonstração visual',
'apósvisual': 'após demonstração visual', 'Semvisual': 'Sem demonstração visual', 'semvisual': 'sem demonstração visual',
'porcritério': 'por critério', 'nolote': 'no lote', 'nolhistórico': 'no histórico',
'todosos': 'todos os', 'Todasasfontes': 'Todas as fontes', 'fontes,nomes econtagensanteriores seguemabaixo': 'fontes, nomes e contagens anteriores seguem abaixo',
'umdos': 'um dos', 'usadosimutáveis': 'usados imutáveis', 'perfís': 'perfis',
'nopróprio': 'no próprio', 'doPublicador': 'do Publicador', 'doPromotor': 'do Promotor',
'contas/grants/titulares': 'contas, atribuições ou titulares', 'scanClamD': 'scan ClamD',
'emUI': 'na interface', 'UI.': 'interface.', 'ledger+duascolunas': 'ledger + duas colunas',
'backupDB+originais+runtime/chaveestável': 'backup da base + originais + runtime e chave estável',
'VisualC37/QA21/A-B ecorreções→fixar candidata/hash/manifesta/config': 'Demonstração visual de C37, QA21 e A/B e correções → fixar candidata, hashes, manifesto e configuração',
'→backup': ' → backup', '→restauro': ' → restauro', '→scan': ' → scan', '→download': ' → download',
'→novaMFA': ' → nova sessão com MFA', '→retry': ' → repetir', '→rollback completo': ' → reversão completa',
'aprovado/configurado': 'aprovado ou configurado', 'mfa': 'MFA',
'prévia/schema/dependências030+020': 'prévia, schema e dependências 030000 + 020000',
'dependências/preview/hash/MFA': 'dependências, prévia, hash e MFA', 'noopsemDDL': 'operação sem alterações DDL',
'noop auditado': 'operação sem alterações, auditada', 'Runtimeestável': 'runtime estável',
'Publicador porversão': 'Publicador por versão', 'arquivo privado QA+runtime': 'arquivo privado QA + runtime',
'confiabilidade': 'credibilidade', 'mostraJSON': 'mostra JSON', 'falha auditiva': 'falha de auditoria',
}
for old, new in replacements.items():
text = text.replace(old, new)
return text
checks, http, office = [read(n) for n in ['profile-installation-checks-qa.json', 'profile-installation-http-qa.json', 'opportunity-office-documents-http-qa.json']]
independent = read('independent-documents-http-qa.json')
document_verify = read('independent-documents-verification-qa.json')
geo = read('geometry-crosswalk-v2-http-qa.json')
preserve = read('geometry-crosswalk-preservation-qa.json')
active = read('dashboard-active-readonly.json')
backup = json.loads((PRIVATE.parent / 'portal-profile-installation/readiness.json').read_text(encoding='utf-8-sig'))
assert all(x['result'] == 'passed' and x['active_writes'] == 0 for x in [checks, http, office])
assert all(x['result'] == 'passed' and x['active_writes'] == 0 and not x['interface_observed'] for x in [independent, document_verify])
assert len(independent['land']) == len(independent['room']) == 3 and len(document_verify['documents']) == 6
assert checks['code_hash'] == http['code_hash'] == backup['code_hash']
assert geo['applied'] and geo['active_writes'] == 0 and not geo['published']
assert preserve['catalogue_preserved'] and preserve['original_geometries_preserved'] == 361 and preserve['total_active'] == 408
assert sum(r['inserted'] for r in geo['results']) == 15 and backup['restore_verified']
assert active['identity']['read_only'] == 'on' and active['active_writes'] == 0
# Preserve a dedicated receipt of the enlarged scope and all source associations.
(PRIVATE / 'geometry-crosswalk-v2-preservation-qa.json').write_bytes((PRIVATE / 'geometry-crosswalk-preservation-qa.json').read_bytes())
plans = {p['level']: p for p in geo['plans']}
gaps = []
for level in [1, 2, 3]:
source_file = PRIVATE / f'moz-adm{level}-ine-20190607.geojson'
assert hashlib.sha256(source_file.read_bytes()).hexdigest() == plans[level]['source_sha256']
source = json.loads(source_file.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['status'] in ['insert', 'reuse']:
continue
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'], 'status': r['status'], 'candidates': r['candidates'],
'reason': {'parent_unresolved': 'Pai ainda sem correspondência; não associar por nome isolado', 'name_unresolved': 'Nome sem correspondência única sob o pai resolvido',
'ambiguous_name': 'Múltiplos cadastros sob o mesmo pai; preservar ambos'}.get(r['status'], r['status'])})
assert len(gaps) == 173
named = PRIVATE / 'geometry-named-gaps.json'
archive = PRIVATE / 'geometry-named-gaps-v1-188.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')
tag = '[Instalação/cartografia QA 10/10/2026] '
matrix = DOCS / 'matriz-requisitos.csv'
rows = list(csv.DictReader(matrix.open(encoding='utf-8-sig', newline='')))
protected = ['ID', 'Pedido/trecho', 'Fonte/ocorrências', 'Critério de aceitação', 'Versões divergentes', 'IDs das linhas de origem', 'Quantidade de linhas de origem', 'Critérios e versões por origem (JSON)']
before = [{k: r[k] for k in protected} for r in rows]
def append(row, column, value):
old = re.sub(r'\s*\|\s*' + re.escape(tag) + r'.*?(?=\s*\|\s*\[|$)', '', row[column], flags=re.S)
row[column] = old + ' | ' + tag + readable(value)
entries = {
'AUD-PROFILE-31': ('Instalador autenticado040000: prévia/schema/dependências030+020, hash/reason/MFA, idempotência UUID e auditoria transacional; bootstrap separado e explícito reutiliza PortalSuperusers. Schema parcial bloqueado; rollback vazio/reinstalação e falha de auditoria recuperados em teste isolado.',
'Visual da instalação/bootstrap, designação real dos dois titulares e política da recuperação simultânea; backup da futura candidata/ativo apósvisual. Schema040 jáQA, sem reinstalação destrutiva ou novos titulares no servidor18097.'),
'AUD-OPS-01': ('030→040→190 preservada:030/040/09-190 QA instaladas e ausentes no ativo lido READONLY. GET/POST/management/profile-install disponíveisQA; preparação não instala nem disponibiliza o percurso.',
'Candidata não fixada; visualC37/QA21/A-B, prova ativa de backup/restauro e autorização de ativação futura; não aplicar ativos nesta autorização.'),
'AUD-GEO-02': ('Configcrosswalkv2 conserva Maputo/Matola e acrescenta cinco distritos e três aliases de postos; sete postos exatos resolvem pelos novos pais. Importação autenticada+MFA+replay:15inseridos/393reutilizados, zero publicação/cadastro/reparenting.',
'173 correspondências nomeadas:26distritos+147postos. Mandie e agregados/edições divergentes permanecem ambíguos. Fonte2019 não fornece geometrias de localidades ou de parcelas.'),
'AUD-GEO-03': ('QA408 geometrias=11províncias+133distritos+264postos; API do consumidor recupera os15 novos limites. 1990cadastros e361geometrias originais preservados;393associações prévias reutilizadas sem alteração.',
'Seleção/gravação/recuperação/render por consumidor continuam sem ensaio visual. Países/origem/filtros/estatísticas transversais não encerrados pela cartografia ou pelos contratos.'),
'AUD-OP-02': ('DOCX/XLSX OOXML válidos:upload18097→scanClamDreal→download21/24/25;23negado403;delete404/opção retirada→restore→novaMFA/bytes idênticos. PDFs/imagens/derivadas anteriores mantidos, originais preservados.',
'Partilhas/exportações/PDFs derivados e cobertura de todos os consumidores e perfis ainda exigem provas próprias; validação de pacoteOOXML não demonstra renderWord/Excel. Visual documental pendente.'),
'AUD-OP-03': ('Apresentação distingue aprovação interna, versão disponibilizada sob acesso restrito e catálogo público; estado técnico published preservado. Scope efetivo consulta política real do catálogo, não só valor do enum.',
'Visual por perfil/workflow completo emUI pendente; incidentes, reatribuição e recurso integral continuam no backlog existente.'),
'AUD-OP-04': ('Seis contratos semânticos anteriores preservados; DOCX/XLSX e rótulos/Response de âmbito efetivo completados neste lote. Quatro propostas concretas de campos/documentos/gates/versões preparadas no documento de decisão, sem ativá-las.',
'Response integral/§27 e condições por tipo/unidades, urbanismo/materialidade e entidade sem exercícios ainda parciais. Propostas D-M1–M4/D-S1–S2 requerem decisão; quatro categorias não submetem/publicam até perfil aprovado e adaptador demonstrado.'),
'AUD-PROFILE-22': ('Proposta SLA versionada:triagem2, revisõestécnica/jurídica10 emparalelo, parecer5, correção15, revisão5, editorial3 e recurso2+10 diasúteis; coordenação/responsabilidades e regras de pausa/lembrete/escalamento explícitas.',
'D-SLA:aprovar/rever prazos, calendário/fuso, tolerância e terceirociclo, responsabilidades/suplentes. Nenhuma due_at ou atribuição real alterada; motor/visual do SLA apósdecisão.'),
'AUD-PROFILE-28': ('Propostas inativas concession/financing/supply/other baseadas em§25.4(P2101–2105), reutilizam campos,documentos,geo eworkflow existentes; decisões regra/fundamento/efeito individualizadas.',
'Perfis não aprovados/nem carregadospelo motor; guardar/editar4drafts anterior provado, submissão/revisão/publicação mantidas bloqueadas. Seis contratos completos emUI ainda não demonstrados.'),
}
evidence = 'profile-installation-checks-qa.json (testes isolados por critério),profile-installation-http-qa.json,opportunity-office-documents-http-qa.json,geometry-crosswalk-v2-http-qa.json/preservation,readinessQAbackup e dashboard-active-readonly.json; fontes/propostas-perfis-modulares-sla.md. Sem visual/sem escrita ativa.'
for row in rows:
if row['ID'] in entries:
implementation, gap = entries[row['ID']]
append(row, 'Implementação encontrada', implementation)
append(row, 'Lacuna', gap)
append(row, 'Evidência funcional', evidence)
append(row, 'API', 'HTTP18097/MFA e alvoQA guardado; ver recibo específico. TestesHTTP de feature eminvestmoz_test separados do servidor usado.')
append(row, 'Gravação', 'Migração/ledger/auditoria atómicosPG no ensaio isolado; servidor já040usado:noopsemDDL. Recursos/originais e15geometrias gravados somenteQA.')
append(row, 'Recuperação', 'Instalador/âmbito eDOCX/XLSX recuperados noutra sessãoFortify/MFA; geometrias porendpoint do componente. SQLrestauro comparado por tabela e ficheiros porhash, com lacunas declaradas na prova.')
append(row, 'Interface', 'Código ecompilaçãoQA preparados; nenhum clique/render observado. C37/PromotorQA21/A-B permanecempendentes.')
append(row, 'Resultado apresentado', 'JSON/HTMLInertia ebinários demonstrados; apresentação visual não demonstrada.')
append(row, 'Disponibilidade ativa', 'AtivoREADONLY:030/040/09-190ausentes,0geometrias,flagsfalse/manifestopreservado; não ativado.')
append(row, 'Consolidação', 'Pedido10/10/2026:040autenticado,188correspondênciasbaseline,propostasmodulares/SLA epublicaçãointerna. Estado atual deste incremento prevalece sobre relatos anteriores conservados.')
document_entries = {
'AUD-LAND-01': ('PDF, PNG e JPEG sintéticos: upload pelo servidor QA, scan ClamD real, original recuperado pelo autor QA21 noutra sessão MFA. Gestor QA14 e outro investidor QA23 recebem 404 no percurso privado. País MZ e origem PT persistidos e recuperados. Modelo de impressão responde com aviso de ausência de decisão externa.',
'Aspeto da interface e PDF gerado pelo navegador pendentes; não demonstra todos os perfis, filtros, estatísticas ou todos os campos fundiários.'),
'AUD-PART-01': ('Sala QA1: PDF, PNG e JPEG privados do autor; candidato recebe 404 e não pode alterar partilha (403). Partilha explícita permite recuperar bytes noutra sessão MFA; revogação retira listagem e download (404), mantendo original do autor. Gestor não participante e organização alheia recebem 403; seis transições de partilha auditadas.',
'Resultado visual, impressão/exportações e restantes ações da bolsa continuam com critérios próprios pendentes.'),
'AUD-SCAN-01': ('Seis originais fundiários/parceria possuem scan real ClamAV limpo, data de assinaturas e hash analisado igual ao original armazenado e recuperado por HTTP. Não foram alterados originais nem simulados scans.',
'ClamD real demonstrado nestas operações; arranque operacional, PDFs gerados, novos tipos e derivadas não cobertos continuam pendentes por operação.')
}
for row in rows:
if row['ID'] in document_entries:
implementation, gap = document_entries[row['ID']]
append(row, 'Implementação encontrada', implementation)
append(row, 'Lacuna', gap)
append(row, 'Evidência funcional', 'independent-documents-http-qa.json e independent-documents-verification-qa.json; servidor QA 18097, API/persistência demonstradas, interface não observada.')
append(row, 'API', 'Rotas reais de terra-duat e parcerias/salas; sessões Fortify, verificações permitidas/negadas e cabeçalho do alvo QA antes de escrever.')
append(row, 'Gravação', 'Três originais por consumidor e eventos de partilha persistidos exclusivamente na base QA isolada.')
append(row, 'Recuperação', 'Nova sessão MFA recupera os mesmos hashes; revogação mantém os originais e remove o acesso do candidato.')
append(row, 'Interface', 'Modelo Inertia de impressão recuperado; nenhum clique, mapa ou PDF de navegador observado.')
append(row, 'Resultado apresentado', 'JSON, modelo de impressão e binários demonstrados; resultado visual pendente.')
append(row, 'Disponibilidade ativa', 'Este ensaio é exclusivamente QA; não inspecionou nem alterou documentos ou partilhas ativos.')
assert before == [{k: r[k] for k in protected} for r in rows]
assert len(rows) == 3386 and sum(int(r['Quantidade de linhas de origem']) for r in rows) == 6838
with matrix.open('w', encoding='utf-8-sig', newline='') as f:
w = csv.DictWriter(f, fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)
marker = 'qa-installation-cartography-20261010'
def prepend(path, block):
old = path.read_text(encoding='utf-8-sig')
old = re.sub(r'<!-- ' + marker + r':begin -->.*?<!-- ' + marker + r':end -->\s*', '', old, flags=re.S)
old = old.replace('Este bloco é a evidência mais recente desta matriz;', 'Este bloco conserva a revisão semântica anterior; o estado atual está no início do documento;')
first, _, rest = old.partition('\n')
path.write_text(first + '\n\n<!-- ' + marker + ':begin -->\n' + readable(block.strip()) + '\n<!-- ' + marker + ':end -->\n\n' + rest.lstrip(), encoding='utf-8')
files_ready = 'todos os ficheiros referenciados recuperados por hash' if backup['files_verified'] else f"{len(backup['missing_document_files'])} ficheiros referenciados ausentes; readiness bloqueada até esclarecer/restaurar"
summary = f'''## Estado atual — instalador, documentos e cartografia
QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Ativo inspecionado apenas por leitura em {active['captured_at']}: 030000, 040000 e 09/190000 ausentes; zero geometrias; manifesto preservado. **C37, Promotor QA21 e A/B permanecem visualmente pendentes. Candidata à ativação ainda não fixada.**
| Requisitos | Código preparado / prova QA | Critério ainda aberto |
| --- | --- | --- |
| PROFILE-31 / OPS-01 | GET/POST `/management/profile-install`, bootstrap separado; prévia, dependências, hash, MFA, repetição e auditoria. Instalação vazia revertida/reinstalada; falha de auditoria reverte DDL e ledger, permitindo repetir. Uso/histórico impedem remoção. No servidor já utilizado, `changed=false`; nenhum titular ou privilégio alterado. Promotor, Auditor e organização alheia recebem 403. | Instalação/bootstrap visual e titulares reais designados; recuperação simultânea da dupla depende de D-R1. Backup do schema não recupera MFA perdido. |
| GEO-02/03 | **15 novas**: cinco distritos e dez postos; **393 reutilizadas**. Total **408**: 11 províncias, 133 distritos e 264 postos. API do mapa recupera os 15 novos limites. 1.990 cadastros e 361 geometrias originais preservados; sem publicação. | **173 lacunas nomeadas**: 26 distritos e 147 postos; ambiguidades e edições distintas. Seletores/mapas por consumidor continuam com aceitação visual pendente. Localidades/parcelas não inferidas. |
| OP-02 / documental | DOCX/XLSX: pacote válido, scan ClamD real, acesso de QA21/24/25 e QA23 negado; exclusão lógica, restauro e original idêntico noutra sessão MFA. | Renderização de Office, exportações/PDFs derivados e restantes consumidores continuam pendentes por operação. |
| LAND-01 / PART-01 / SCAN-01 | PDF, PNG e JPEG em fundiário e sala de parceria: seis scans reais com hashes iguais. Autor recupera originais; partilha explícita permite nova sessão; revogação retira acesso/listagem, sem apagar original. Gestor não ganha acesso por esse papel. **{len(independent['events'])} pedidos** no recibo próprio. | Modelo de impressão recuperado, mas PDF gerado no navegador e interface não demonstrados. Estes seis scans não encerram outros tipos, derivadas ou permissões. |
| OP-03 / OP-04 | Código da interface e resposta distinguem aprovação interna, disponibilização restrita e catálogo público. Casos #30 restrito, #33 público e #32 aprovado sem publicação recuperados pela API; consulta da política pública efetiva. | Interface; Response integral/§27 e condições sem regra inequívoca continuam parciais. Testes dos seis contratos não encerram a demonstração visual. |
| PROFILE-22/28 | [Quatro perfis e SLA com regra, fundamento, efeito e decisões concretas](propostas-perfis-modulares-sla.md). | Propostas **inativas**; quatro categorias continuam com rascunhos recuperáveis, mas submissão/publicação bloqueadas. Nenhum prazo ou responsável real configurado. |
Prova de recuperação QA: dump consistente → nova base `{backup['restored_target']['database']}:55441` → **{len(backup['tables'])} tabelas com linhas/hashes iguais e {len(backup['files'])} ficheiros fisicamente recuperados por hash**; {files_ready}. Arquivo privado inclui o runtime e a chave QA estável. Readiness só atesta preparação técnica desta revisão. Repetir após fixar a candidata e preparar backup ativo apenas numa futura ativação autorizada.
**Matriz única:** 3.386 requisitos/linhas e 6.838 ocorrências de origem; todas as fontes, critérios e versões preservados; sem novos fechos globais. Contagens por estado abaixo e na projeção dos pedidos abertos.
**Próximo lote executável:** completar Response/§27 reutilizando entidades; verificar exportações e PDFs derivados por consumidor; prosseguir correspondências territoriais por nome, pai e edição; completar países/origem, filtros e estatísticas por formulário. Incorporar provas visuais quando retomadas. Decisões D-M1–M4, D-S1/S2, D-SLA e D-R1 estão individualizadas e não bloqueiam o trabalho independente.
**Histórico preservado:** os blocos seguintes descrevem revisões anteriores. 393 geometrias/188 lacunas eram o estado antes deste lote; “instalador 040000 em falta” deixou de descrever o código QA. Esses estados anteriores não provam esta revisão nem autorizam instalação ativa.
'''
prepend(DOCS / 'relatorio-executivo.md', summary)
prepend(DOCS / 'backlog.md', summary)
prepend(DOCS / 'workflow-oportunidades-qa.md', summary)
prepend(ROOT / 'docs/investmoz/v2/gestao-oportunidades-dashboard.md', summary.replace('(propostas-perfis-modulares-sla.md)', '(auditoria-portal/propostas-perfis-modulares-sla.md)'))
activation = '''## Sequência reconciliada — preparada, não executada no ativo
**030000 → 040000 → 190000** mantém-se. QA já tem as três; **não repetir instalações QA**. No ativo lido READ ONLY, as três extensões estão ausentes; Balcão07/190000 eorientação08/120000 estão instalados. A extensão **09/190000 não foi substituída**: faltam ledger,duascolunas e definição19etapas. Fonte:dashboard-active-readonly.json eopportunity-semantic-environment-qa.json.
| Ordem | Ação futura, efeito econtagem prevista | Dependência / reversão |
| --- | --- | --- |
| 0 | VisualC37/QA21/A-B ecorreções→fixar candidata/hash/manifesta/config→backupDB+originais+runtime/chaveestável→restauro emQA nova, tabelas/bytes/acessos comparados. | QA backup deste lote não é backup ativo nem candidata definitiva. Ativação continua fora da autorização. |
| 1 | Disponibilizar código/Composer/build compatíveis, flagsfalse eURLs preservadas. | Não retirar privilégios reais ou migraridentidades automaticamente. Reverter flags/build/código compatível preservando dados novos. |
| 2 —030 | GETprévia→POST `/management/opportunity-resources-install`, hash/confirm/MFA/provas atuais, sóse ausente/coerente. **3coleções+metadado cifra;0documentos importados/publicados.** | Base020/documentos existentes. Não drop apósuso; restaurar/reconciliar backup em vez de apagar originais. |
| 3 —040 | GETprévia→POST `/management/profile-install`; **4tabelas+1colunausers+3colunasoportunidades,0contas/grants/flags.** BootstrapPOST `/management/profile-install/bootstrap` é **separado**:2contas explicitamente designadas,3.ºgestor comMFAaprova. | Instalador exige020+030/schema/organização/auditoria, hash ebackup/restauro atuais. Idempotência eDDL/ledger/auditoria atómicos. Schema parcial bloqueia aplicação; down apenassemqualqueruso/contexto/revisão/histórico. Recuperação deMFA simultânea exigeD-R1,sembootstraprepetido. |
| 4 —09/190 | GETprévia→POST `/management/balcao-install`, sómigraçãofaltante: **2colunas+1definição publicada19etapas/5fases;0dossiers automáticos.** | BaseBalcão/orientação jáinstaladas: não repetir. SemdepDDLde040,masordem operacional preservada. Template eversões usados imutáveis; reverterdisponibilização,preservar snapshots/roteiros. |
| 5 —cartografia | GETprévia→POST `/management/territory-geometry-import`, níveis1→2→3 ehash/idempotência. **408 correspondências candidatas** se cadastro/hash coincidem:11/133/264;ativo0na leitura. **173pendentes** não importadas. | Fontes/hash/pai/nome/código vinculados;resultado/contagem medida no alvo,semfusões ougeometrias inventadas. Reversão só lotesnovos nãoalterados/usados,postos→distritos→províncias comaudit;seFK/uso,preservar/reconciliar,nãoforçar. |
| 6 —disponibilização | Aprovar perfis/reconciliação depermissões reais econfiguração; habilitar navegação/editores/percurso porincremento comprovado. **0fixtures QA noativo.** | I1–I3 não concluídos semA/B/UI. Contratos4categorias eSLA sóapósdecisão própria. Importação não ativa cálculo fiscal. |
| 7 —publicação | Revisão dedivulgação eação doPublicador porversão; **camadas cartográficas públicas são ação separada** daimportação. | Aprovaçãointerna≠acessorestrito≠catálogopúblico≠decisãoexterna. Desabilitar publicação/flags antes derestauro;preservar originais,consentimentos eauditoria. |
Sharedcode/fingerprint muda:renovar provas da revisão ealvo;ready=true não instala,não escolhe titulares,não publica enão disponibiliza percurso. Guarda ativa/QA conferida antes decada escrita futura. A prova040 emschema vazio foi isolada; no servidorQA jáusado sónoop auditado.
'''
prepend(DOCS / 'plano-ativacao.md', activation)
cases = '''## Casos adicionais deste lote — sem reiniciar o inventário
| Caso / requisito | Reprodução e resultado esperado | Observado, causa e correção | Prova / estado |
| --- | --- | --- | --- |
| C59 — PROFILE-31 / OPS-01 | Gestor confirmado com MFA consulta e aplica apenas 040000, após030000; repetir o mesmo pedido. Esperado:prévia, dependências e provas, uma instalação e um recibo, sem contas/grants automáticos. | A migração já tinha guarda, mas faltava rota/serviço/interface autenticados. Reutilizados Fortify, audit_events e cerimónia da dupla. Plano obsoleto/chave alterada409; falta de provas/dependência422; Auditor/Promotor403. | Testes de feature em schema vazio, repetição e bootstrap separados; servidor18097 já instalado:noop, replay idêntico, novaMFA e negações. Visual pendente. |
| C60 — PROFILE-31 / recuperação | Em base isolada e schema vazio, down→instalar; depois impedir insert na auditoria e instalar. Esperado:reversão total deDDL/ledger se não houver recibo; retry possível; nenhum drop com uso/histórico. | No desenvolvimento do incremento foi introduzida remoção duplicada da colunausers; o ensaio encontrou Undefined column. Corrigida. Também reforçado down para contexto/organização sem autor e revisões de sessão; falha de auditoria devolve503 e não deixa schema/ledger parcial. | Testes específicos de reinstalação e falha auditiva; históricos/contextos conservados. Não executado down no servidorQA utilizado, nem no ativo. |
| C61 — OP-03 / publicação | Recuperar #30,#33,#32 e comparar estado da preparação, âmbito efetivo e catálogo. Esperado:restrito,público,aprovado sem disponibilização respetivamente. | Rótulo genérico “Publicado”/estado published não explicava o âmbito. Novo label/projeção consulta política efetiva do catálogo; botões distinguem disponibilização restrita de publicação pública. | JSON/Inertia e política pública verificados; nenhuma fuga pública observada. Perceção/ação visual pendente. |
| C62 — PROFILE-31 / bootstrap | Propor duas contas distintas e terceiro gestor; candidato com MFA cifrado ilegível ou Auditor. Esperado:recusa controlada e nenhum titular criado. | A cerimónia conferia presença/confirmaçãoMFA, sem comprovar leitura do segredo no ambiente. Agora exige cifragem válida/estruturaMFA e recusa Auditor, mesmo com flags ainda desabilitadas. Sem key:generate/fallback ou remoção deMFA. | Teste422 e dupla permanece vazia; inicialização positiva com identidades sintéticas distintas. Não substitui C37/login visual. |
| C63 — ambiente de testes / documental | Executar IndependentDocumentOperationsTest com o PHPCLI padrão. Esperado:GD disponível para gerar as imagens do ensaio. | Dois testes falharam por imagecreatetruecolor inexistente noCLI; o servidorQA já tinhaGD. Runner passa a carregar explicitamente a mesma extensãoGD para o processo de testes. | Regressão posterior27/600 semskips, scans reais e testes finais separados. Não atribuir esta falha ao servidorweb ou declarar GD ativo verificado. |
| C64 — PROFILE-31 / recuperação e proteção | Em schema isolado com perfis habilitados, retirar a coluna de revisão e tentar desativar MFA/excluir conta. Esperado:sem alteração de identidade, recuperação e logout acessíveis. | Esquema parcial podia tornar o serviço de perfis indisponível e deixar middleware de proteção passar à frente. Agora recusa escritas com503, preserva MFA/conta e deixa o instalador apresentar a incoerência409; logout permanece permitido. | Teste específico, schema parcial não aplicado ao servidorQA nem ao ativo. Preserva leitura permitida e não cria bypass de recuperação. |
C55 continua corrigida por operação:PDF eagoraDOCX/XLSX excluídos dão404, scanpendente continua423; restore preserva original. C37/C43/QA21/A-B permanecempendentes. Cada prova mantém API/persistência/interface separadas.
'''
prepend(DOCS / 'casos-reproducao.md', cases)
manual = f'''## Ensaio visual futuro deste lote — contas QA, sem credenciais
Servidor exclusivo **http://127.0.0.1:18097**. Para a instalação, usar o **Superutilizador sintético QA27** já designado na dupla27/30; obter credenciais eMFA somente no ficheiro privado local. **Promotor QA21 continua a conta do dashboard, não substituí-lo por27. C37/QA18 e A/B continuam pendentes; não é pedida nova tentativa agora.**
1. ComoQA27, login/MFA normal e abrir **http://127.0.0.1:18097/management/profile-install**. Conferir oalvo apresentado `desk_restore_20261009_174726_test:55441`, schema instalado, dependências020/030 presentes, provas atuais e dupla existente. Não executarbootstrap ou escolher titulares novos neste servidor usado.
2. Atualizar a página, terminar sessão einiciar outra. Esperado:mesmo ledger e dupla,semnova instalação ou atribuição. Os testes de instalação desde schema vazio/reversão pertencem àbaseisolada do runner; não remover o040 do servidorQA emuso para demonstrar o botão.
3. Quando retomarQA21, dashboard→Minhas oportunidades; #30 apresenta acesso restrito e não catálogo; #33 apresenta catálogo público; #32 apresenta aprovação interna,sempublicação. Os originais/contatos privados não entram na projeção pública.
4. ComoQA21, abrir **http://127.0.0.1:18097/minhas-oportunidades/36/resources**:consultar PDF,DOCX eXLSX sintéticos. Guardar original, excluir logicamente um recurso, esperar404/downloadindisponível; restaurar einiciar outra sessão para recuperar os mesmosbytes. QA24/25 podemconsultar; organizaçãoQA23 énegada.
5. Selecione CahoraBassa/Chitima ouCidadeMaxixe no consumidor territorial. Esperado:mesmoIDexistente, geometria administrativa comedição2019, legenda eárea selecionada. Guardar/recuperar seleção; não confundir limite administrativo comparceladeinvestimento. Inspecionar cadaoutroconsumidor no roteiroexistente.
6. Registar URL,conta/ação/resultado visível eestado recuperado. Omitir Cookie,X-XSRF-TOKEN,password,segredo/códigoMFA. Incorporar falhasàmesma matriz antes defixar candidata.
Documentos fundiários QA21: **http://127.0.0.1:18097/investidor/terra-duat/{independent['receipts']['case_id']}**. Conferir PDF/PNG/JPEG e país MZ/origem PT; abrir impressão e gerar PDF no navegador. A recuperação do modelo por HTTP não demonstra o PDF final.
Partilha QA21/22: **http://127.0.0.1:18097/parcerias/salas/{independent['receipts']['room_id']}**. Os três originais estão privados após o ensaio. QA21 partilha explicitamente um; QA22 recupera noutra sessão. QA21 revoga; QA22 deixa de o listar/obter; QA21 mantém o original. QA14 não participante e QA23 não têm acesso à sala.
Este roteiro não declara os passos executados. Instalação ativa e publicação de camadas permanecem ações futuras separadas.
'''
prepend(DOCS / 'roteiro-manual-perfis-v1.md', manual)
prepend(DOCS / 'perfis-v1-reconciliacao.md', summary)
cartography = '''## Correspondências atuais — v2
Esperado refere o exemplar aceite INE/UNOCHA07/06/2019, não uma certificação da divisão atual. Fonte oficial continua aceite. Nome/pai/edição são problemas de correspondência, não revalidação da credibilidade.
| Nível | Esperado | Resolvido QA | Pendente | Incremento deste lote |
| --- | ---: | ---: | ---: | ---: |
| Províncias |11|11|0|0|
| Distritos |159|133|26|5|
| Postos administrativos |411|264|147|10|
| Total |581|408|173|15|
Baseline anterior393/188 mantida nohistórico. As antigas2/39/179 eram a situação09/10; não descrevem oestadoatual. Cadastro do seletor1920referências (11+148+437+1324),mais70países;geometria eapresentação funcional têm denominadores próprios. Semgeometriasdelocalidades/parcelas desteexemplar.
Cinco aliasesdedistritos: **Cahora Bassa→CAHORA-BASSA;Ngauma→N`GAUMA;Ilha De Moçambique→CIDADE ILHA DE MOCAMBIQUE;Maxixe→CIDADE DE MAXIXE;Nacala-A-Velha→NACALA-VELHA**. Postos exatos pelospais:Chitima,Covo,Ilha De Moçambique,Itepela,Lumbo,Massangulo,Songo;aliasesapóstrofo:M'Sawize→M`SAWIZE,N'Cumpe→NCUMPE,N'Gapa→N`GAPA. Fonte,código,pai ecadastro existentes conferidos pela prévia;fundamento ehash emconfig/geometry-correspondences.phpv2 eprovaHTTP. Semrenomeação oualteraçãohierárquica.
geometry-crosswalk-v2-http-qa.json:15inserts/393reuse,replayigual e15limites recuperadospelaAPI domapa;sempublicação/semvisual. geometry-crosswalk-v2-preservation-qa.json:1990cadastros/361originaispreservados e47incrementaisprivados. Reversão existente não apaga originais;atualizaçãousada recusada. Mandie continua ambíguo entre2cadastros/códigos sobGuro;CidadeDeMaputoagregado nãoequivalea1distritourbano. Novosdistritos/edições nãoassociadosporproximidade.
'''
for level in [2, 3]:
items = [g for g in gaps if g['level'] == level]
cartography += f"\n### {'Distritos' if level == 2 else 'Postos'} — {len(items)} lacunas por nome\n\n| Código | Nome | Província / pai | Causa |\n| --- | --- | --- | --- |\n"
for g in items:
cartography += f"|{g['pcode']}|{g['name']}|{g['province']} / {g['parent_name']}|{g['reason']}|\n"
cartography += '\n**Visual pendente:**cada seletor/país/origem/mapa porformulário, gravação erecuperação;408 APIs não demonstram render. Todasasfontes,nomes econtagensanteriores seguemabaixo como história.\n'
prepend(DOCS / 'correspondencias-cartograficas.md', cartography)
states = collections.Counter(r['Estado'] for r in rows)
state_text = '## Contagens atuais por requisito\n\n| Estado | Requisitos / linhas da matriz | Ocorrências de origem |\n| --- | ---: | ---: |\n'
for state, count in states.items():
state_text += f"|{state}|{count}|{sum(int(r['Quantidade de linhas de origem']) for r in rows if r['Estado'] == state)}|\n"
state_text += '\n3.386 requisitos verificáveis e 3.386 linhas consolidadas; 6.838 ocorrências de origem. Sem novo fecho integral; provas QA descritas por critério.\n'
for name in ['relatorio-executivo.md', 'backlog.md']:
path = DOCS / name
text = path.read_text(encoding='utf-8-sig').replace('<!-- '+marker+':end -->', state_text + '<!-- '+marker+':end -->', 1)
path.write_text(text, encoding='utf-8')
counts_path = PRIVATE / 'contagens-requisitos.json'
counts = json.loads(counts_path.read_text(encoding='utf-8-sig'))
counts.update({'installation_updated_at': datetime.now(timezone.utc).isoformat(), 'requisitos': len(rows), 'linhas_matriz': len(rows),
'linhas_origem': 6838, 'por_estado': {s: {'requisitos': n, 'linhas_origem': sum(int(r['Quantidade de linhas de origem']) for r in rows if r['Estado'] == s)} for s, n in states.items()},
'browser_current_version_verified': False})
counts_path.write_text(json.dumps(counts, ensure_ascii=False, indent=2), encoding='utf-8')
inventory = DOCS / 'inventario-fontes.csv'
inv = list(csv.DictReader(inventory.open(encoding='utf-8-sig', newline='')))
proposal = DOCS / 'propostas-perfis-modulares-sla.md'
name = str(proposal)
entry = next((r for r in inv if r['Exemplar/caminho'] == name), None)
if entry is None:
entry = {k: '' for k in inv[0]}; entry['Exemplar/caminho'] = name; inv.append(entry)
entry.update({'Bytes': str(proposal.stat().st_size), 'SHA256': hashlib.sha256(proposal.read_bytes()).hexdigest(), 'Tipo': 'proposta interna QA',
'Nota de leitura': 'Documento integral; regras/fundamento/efeito/decisões efonte§25.4. Não fonteoficial,nenhuma aprovaçãoinferida.',
'Disponibilidade': 'Consulta no repositório; publicação não inferida', 'Uso em cálculo': 'Nenhuma regra/SLA/cálculo ativado',
'Situação jurídica/documental': 'Proposta para decisão, não ato jurídico', 'Edição/versão identificada': 'proposta20261010v1'})
with inventory.open('w', encoding='utf-8-sig', newline='') as f:
w = csv.DictWriter(f, fieldnames=list(inv[0])); w.writeheader(); w.writerows(inv)
(PRIVATE / 'installation-audit-update-qa.json').write_text(json.dumps({'updated_at': datetime.now(timezone.utc).isoformat(), 'requirements': 3386,
'source_occurrences': 6838, 'protected_fields_preserved': True, 'states': dict(states), 'updated': list(entries), 'geometry_total': 408,
'document_originals_verified': 6, 'document_requirements_updated': list(document_entries), 'named_pending': 173, 'active_writes': 0}, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({'requirements': 3386, 'source_occurrences': 6838, 'updated': list(entries) + list(document_entries), 'geometries': 408, 'pending': 173, 'active_writes': 0}))