"""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 abertos

Pedidos 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.

3.349 requisitos apresentados

''' + ''.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))