"""A read-only, searchable view of every open requirement in the existing CSV."""
import collections
import csv
import hashlib
import html
import json
import sys
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
csv.field_size_limit(16 * 1024 * 1024)
root = Path(__file__).resolve().parent.parent
docs = root / 'docs/investmoz/v2/auditoria-portal'
private = root / 'storage/app/private/investmoz-v2/portal-audit'
matrix = docs / 'matriz-requisitos.csv'
rows = list(csv.DictReader(matrix.open(encoding='utf-8-sig', newline='')))
opened = [r for r in rows if r['Estado'] != 'concluído e demonstrado']
prepared = {'AUD-COUNTRY-01', 'AUD-COUNTRY-02', 'AUD-COUNTRY-03', 'AUD-COUNTRY-04', 'AUD-COUNTRY-06', 'AUD-COUNTRY-07', 'AUD-STATS-01',
'AUD-GEO-01', 'AUD-OP-01', 'AUD-OP-02', 'AUD-OP-05', 'AUD-DESK-01', 'AUD-DESK-02', 'AUD-CRM-01', 'AUD-GUIDE-01', 'AUD-SIM-01', 'AUD-PART-01', 'AUD-LAND-01', 'AUD-LIB-02'}
# C37 QA18 is manually closed; QA21 dashboard access is separately demonstrated.
# Keep functional gaps under their canonical state instead of relabelling them as access failures.
access_failure = set()
prepared.update({'AUD-DASH-01', 'AUD-UI-01', 'AUD-GEO-02', 'AUD-GEO-03', 'AUD-SIM-02'})
prepared.update({'AUD-OP-03','AUD-OP-04','AUD-PROFILE-19'})
prepared.update({'AUD-LEG-01', 'AUD-LEG-02', 'AUD-LEG-03', 'AUD-LEG-05', 'AUD-LEG-06', 'AUD-LEG-07'})
prepared.update(f'AUD-PROFILE-{number:02d}' for number in [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,23,24,25,30,31,32])
groups = ['Não implementados', 'Parciais', 'Preparados em QA', 'Instalados com falha de acesso']
def group(row):
if row['Estado'] == 'não implementado':
return groups[0]
if row['ID'] in access_failure:
return groups[3]
if row['ID'] in prepared or row['Estado'] == 'implementado mas não ativado':
return groups[2]
return groups[1]
next_actions = {
'AUD-DASH-01': 'Atalhos, rascunho, gravação, recuperação, edição, pedido de arquivo e arquivo independente QA21/25 já demonstrados; não repetir. Completar política e ação de restauração pelo perfil competente, sem conceder arquivo direto ao Promotor por inferência. Restantes categorias/workflow exigem provas próprias.',
'AUD-UI-01': 'C37 encerrado exclusivamente para autenticação QA18 por confirmação manual, com provas anteriores preservadas. Não repetir o ciclo concluído. Completar A/B móvel, critérios especializados e restantes consumidores/perfis; auditoria e ativação permanecem abertas.',
'AUD-OP-01': 'Executar C43 a partir do dashboard com Promotor QA21/MFA; conferir gravação, recuperação, coedição autorizada, pedidos/decisões de arquivo e restauro pelos perfis competentes, sem confundir o dossier privado com o catálogo público.',
'AUD-OP-02': 'Conferir operações visuais por tipo/perfil e scans próprios de derivadas; após aceitação/backuprestaurado, aplicar autenticadamente030000 apenas se ausente. Não repetir020000.',
'AUD-OP-03': 'Executar ciclo visual com PromotorQA21, Validador24, Publicador25 e recurso29; API/persistência do ciclo já demonstradas. Completar pareceres/incidentes/coordenação editorial e política SLA/agenda sem inventar responsáveis ou prazo.',
'AUD-OP-04': 'Completar editores/validações semânticas §25 e Response integral; seis contratos estruturais aceites em QA não encerram todos os gates. Quatro categorias conservam drafts, mas exigem perfil modular aprovado para submissão/publicação.',
'AUD-OP-05': 'Selecionar/guardar/reabrir7/8 na interface com versão do catálogo explícita; conferir legado6sem converter significados e fases de revisão/publicação.',
'AUD-GEO-01': 'Pesquisar cada província, guardar/reabrirID/nome e confirmar o limite no seletor partilhado para cada perfil.',
'AUD-GEO-02': 'Resolver as 173 correspondências restantes (26 distritos/147 postos), por pai, edição e identidade inequívoca; Mandie/Guro mantém-se ambíguo. Preservar as 408 associações demonstradas, aplicar privadamente em QA com replay e testar cada seletor.',
'AUD-GEO-03': 'Testar cada mapa/consumidor com os 408 limites privados e permissões; distinguir território, geometria e apresentação. Permanecem 26 distritos/147 postos e polígonos de localidades. Publicação das camadas separada.',
'AUD-REL-01': 'Ligar as relações existentes a citações e fonte/edição específicas; não inferir pares oportunidade–projeto/cluster nem criar ligações sem evidência.',
'AUD-REL-02': 'Pergunta concreta:quais são os pares identificados e documentados entre as seis oportunidades existentes e âncoras/clusters? Preservar relações atuais enquanto indeterminados.',
'AUD-SIM-02': 'Completar correspondência diploma/artigo/âmbito/período/parâmetro nas fontes oficiais aceites; testar cenários rastreáveis. Taxa ausente permanece indeterminada; importação não ativa cálculo.',
'AUD-LIB-01': 'Completar consulta do acervo/autorização por ficheiro a partir do inventário253ficheiros+60anexos; preservar original/hash/versão e estados consulta,jurídico e cálculo independentes.',
'AUD-LIB-02': 'Completar aplicação por mecanismo autenticado, prévia/hash/duplicação/conflito/escrita privada/scan; CLIapply continuaQA-only. Conferir partilha/revogação/tipos na interface.',
'AUD-SCAN-01': 'Verificar scan próprio de cada operação/derivada restante e arranque/reboot/assinaturas ClamD/GD no servidor de futura ativação; provaClamD real existente mantida.',
'AUD-DESK-01': 'Percorrer os três serviços e seus anexos/estados por perfil; guardar/reabrir campos/país/origem e mostrar resultado/pendências/recibo.',
'AUD-DESK-02': 'A/B internos concluídos em QA, com preparação/recuperação do pedido e C88. Restam outros vínculos empresariais/legados e conferência documental por consumidor; não repetir A/B/C37.',
'AUD-CRM-01': 'Tarefa paralela S02/S10 A/B demonstrada. Restam conversão sem duplicar investidor, responsável/próxima ação e atribuições/filas por perfil; sem dependência de CRM externo.',
'AUD-GUIDE-01': 'Conferir cada recomendação contra âmbito/período/elementos da fonte oficial aceite; exibir fundamento/próxima ação sem score de aprovação inventado.',
'AUD-SIM-01': 'A/B contextuais já guardados/recuperados com resultado no dossier correto e permissões. Não repetir; verificar variantes anónimas/não contextuais, expiração/purga e consumidores restantes.',
'AUD-PART-01': 'Completar rascunho/candidatura/sala/marcos por perfis, partilha/revogação e downloads/renderização na interface, preservando originais e consentimentos.',
'AUD-LAND-01': 'Percorrer formulário fundiário/país/origem/localização/anexo/partilha com gravação e nova sessão; vincular direitos existentes sem venda automática deDUAT.',
'AUD-BIZ-01': 'Completar negócio existente com direitos/mandatos/documentos e geografia/país, revisar/publicar por ação própria e confirmar recuperação/perfis na interface.',
'AUD-EDU-01': 'Retomar somente após as falhas prioritárias:reutilizar atores/cadastros e demonstrar procura/capacidade/investimento com dados reais e permissões.',
'AUD-EDU-02': 'Depois deI0/C43/contratos prioritários, permitir guardar procura/capacidade incompletas como rascunho privado nas entidades existentes, sem exigir os campos finais.',
'AUD-EDU-03': 'Não iniciar agora; depois das prioridades, jornadas/painéis por ator reutilizando19etapas/5fases e contas existentes, sem renumerar roteiros.',
'AUD-OPS-01': 'Concluir critérios visuais→fixar candidata→renovar backup/restauro→disponibilizar código/flags→instalar somente migrations ausentes→percurso/importação/publicação separadas, com reversão preservadora.',
'AUD-LEG-01': 'C71 recuperado; 18/50 versões cadastradas (17 com texto, uma vazia privada). Reconciliar as restantes 32 versões, paginação/sitemap e anexos/C68; prévia, hash, scan real, replay e divulgação separada.',
'AUD-LEG-02': 'Desktop dos setores demonstrado no Chromium; completar móvel/acessibilidade sistemática e demais perfis. Não inferir aceitação integral a partir de tipografia/imagens carregadas.',
'AUD-LEG-03': 'Quatro dossiers setoriais QA21 demonstrados na interface, nova sessão e retorno. A/B internos e C37 QA18 têm fechos próprios preservados; não repetir. Completar os restantes consumidores, conteúdos/variantes setoriais, móvel/acessibilidade e perfis, sem extrapolar a cobertura do legado.',
'AUD-LEG-05': 'Preservar Energia e Mineração distintas; Turismo editorial sem associação geográfica automática. Fundamentar relações territoriais/por etapa com citação específica, reutilizando a mesma fonte da biblioteca.',
'AUD-LEG-06': 'URLs e retornos dos setores demonstrados; completar o restante mapa de migração/aliases e cobertura do acervo. C37/QA18 e A/B mantêm critérios independentes.',
'AUD-LEG-07': '22 originais/hash/ClamD/replay demonstrados; 21 divulgados separadamente e Mercado Financeiro vazio privado. Completar as 32 versões restantes, C68, links/anexos, móvel/acessibilidade e visual do editor Assunto opcional C74.',
'AUD-DOC-01': 'Reconciliar individualmente cláusulas/versões com prova datada; conservar os estados históricos e atualizar somente o critério demonstrado na matriz única.',
'AUD-UX-01': 'Percorrer interface/teclado/móvel e remover somente informação sem utilidade demonstrada, preservandoIDs/URLs e rastreabilidade.',
'AUD-HUMAN-01': 'Pergunta concreta:qual é a entidade operadora, contacto oficial e texto/versão dos termos a apresentar? Não atribuir a entidade/responsável inexistente.',
'AUD-HUMAN-02': 'Pergunta concreta:quais URLs, serviços e responsáveis institucionais devem ser associados? Percursos internos continuam independentes destas integrações.',
'AUD-HUMAN-03': 'Pergunta concreta:qual modelo multicritério, indicadores ebaseline/pilotos estão aprovados? Até haver definição, apresentar evidências/cenários sem score de sucesso.'}
for id in ['AUD-COUNTRY-01', 'AUD-COUNTRY-02', 'AUD-COUNTRY-03', 'AUD-COUNTRY-04', 'AUD-COUNTRY-06', 'AUD-COUNTRY-07', 'AUD-STATS-01']:
next_actions[id] = next(r['Lacuna'].split(' | Histórico preservado')[0] for r in rows if r['ID'] == id)
# The current matrix lists only remaining criteria; do not re-run accepted A/B,
# dashboard access or the visual investment_project approval/publication cycle.
for id in ['AUD-GEO-01', 'AUD-GEO-02', 'AUD-GEO-03', 'AUD-DOC-01',
'AUD-OP-01', 'AUD-OP-02', 'AUD-OP-03', 'AUD-OP-04',
'AUD-DESK-01', 'AUD-UI-01']:
next_actions[id] = next(r['Lacuna'] for r in rows if r['ID'] == id)
def action(row):
if row['ID'].startswith('AUD-PROFILE-'):
return row['Lacuna'] + ' Ver plano e roteiro em perfis-v1-reconciliacao.md; continuar QA sem afetar ativo.'
if row['ID'] in next_actions:
return next_actions[row['ID']]
if row['ID'].startswith('AUD-FIELD-'):
return 'Executar neste consumidor específico:pesquisar→selecionar→guardar→reabrir→apresentar no campo/mapa; trocar pai→limpar filhos; verificar autorização. Corrigir falha reproduzida usando os componentes territoriais existentes.'
return 'Conferir e completar emQA o critério específico abaixo nas rotas/entidades existentes, preservandoIDs e versões; registar interface/API/gravação/recuperação/resultados separados. Correspondência ainda não provada exige localizar a implementação antes de atribuir conclusão.'
escape = html.escape
counts = collections.Counter(group(r) for r in opened)
for label in groups:
counts.setdefault(label, 0)
cards = []
for r in sorted(opened, key=lambda x: (groups.index(group(x)), x['Prioridade'], x['Módulo'], x['ID'])):
body = ''.join('
' + escape(label) + '' + escape(r[key]) + '' for label, key in [('Pedido', 'Pedido/trecho'), ('O que falta', 'Lacuna'), ('Rotas', 'Rotas'), ('Componentes encontrados', 'Implementação encontrada')])
cards.append(''+escape(r['ID'])+' · '+escape(r['Módulo'])+'
'+escape(group(r))+' · '+escape(r['Estado'])+' · '+escape(r['Prioridade'])+'
'+body+'- Próxima ação
- '+escape(action(r))+'
- Critério de aceitação
- '+escape(r['Critério de aceitação'])+'
Fontes, versões e evidência do requisito
'+''.join('- '+escape(k)+'
- '+escape(r[k])+'
' for k in ['Fonte/ocorrências', 'IDs das linhas de origem', 'Quantidade de linhas de origem', 'Versões divergentes', 'Critérios e versões por origem (JSON)', 'Disponibilidade ativa', 'Evidência funcional', 'Interface', 'API', 'Gravação', 'Recuperação', 'Resultado apresentado'])+'
')
summary = '; '.join(g+': '+str(counts[g]) for g in groups)
page = '''InvestMoz — pedidos abertosPedidos anteriores ainda abertos
Vista de leitura gerada exclusivamente da matriz-requisitos.csv. Não é outra matriz nem permite editar estados. 3.354 requisitos/linhas;5.830 ocorrências;5 concluídos de âmbito restrito;3.349 abertos.
''' + escape(summary) + '''
O agrupamento indica a frente de trabalho; mantém o estado canónico em cada cartão. “Preparados em QA” identifica provas de escopos concretos e conserva as lacunas do requisito inteiro. “Instalados com falha de acesso” reúneC43/C37 ainda abertos, com correções/backendQA e aceitação visual pendente; não confirma o perfil da sessão reportada. As4decisões concretas permanecem emParciais com o seu estado original.
''' + ''.join(cards) + ''''''
page = page.replace('3.354 requisitos/linhas;5.830 ocorrências;5 concluídos de âmbito restrito;3.349 abertos.',
f'{len(rows):,} requisitos/linhas;{sum(int(r["Quantidade de linhas de origem"]) for r in rows):,} ocorrências;{len(rows)-len(opened)} concluídos de âmbito restrito;{len(opened):,} abertos.')
page = page.replace('3.349 requisitos apresentados', f'{len(opened):,} requisitos apresentados')
page = page.replace('“Instalados com falha de acesso” reúneC43/C37 ainda abertos, com correções/backendQA e aceitação visual pendente; não confirma o perfil da sessão reportada.',
'“Instalados com falha de acesso” contém apenas casos de acesso ainda demonstradamente abertos. C37 foi encerrado para autenticação QA18; o acesso ao dashboard QA21 tem prova própria. Os outros critérios funcionais continuam abertos e a disponibilidade ativa é separada.')
decision_count = sum(r['Estado'] == 'dependente de decisão concreta' for r in opened)
page = page.replace('As4decisões concretas', f'As{decision_count}decisões concretas')
(private / 'pedidos-abertos.html').write_text(page, encoding='utf-8')
proof = {'matrix_sha256': hashlib.sha256(matrix.read_bytes()).hexdigest(), 'canonical_requirements': len(rows), 'source_occurrences': sum(int(r['Quantidade de linhas de origem']) for r in rows),
'open_requirements': len(opened), 'group_counts': dict(counts), 'decision_requirements': [r['ID'] for r in opened if r['Estado'] == 'dependente de decisão concreta'],
'open_ids': [r['ID'] for r in opened], 'one_card_per_open_id': len(cards) == len(opened) == len({r['ID'] for r in opened}),
'report_sha256': hashlib.sha256(page.encode()).hexdigest(), 'qa_authenticated_url': 'http://127.0.0.1:18097/qa-auditoria/pedidos-abertos'}
assert proof['one_card_per_open_id'] and sum(counts.values()) == len(opened)
(private / 'pedidos-abertos-projection.json').write_text(json.dumps(proof, indent=2, ensure_ascii=False), encoding='utf-8')
print(json.dumps({k: v for k, v in proof.items() if k != 'open_ids'}, ensure_ascii=False))