Files
Investidor-V2/tools/portal-audit-open-requests.py
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

126 lines
17 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.
"""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('<dt>' + escape(label) + '</dt><dd>' + escape(r[key]) + '</dd>' for label, key in [('Pedido', 'Pedido/trecho'), ('O que falta', 'Lacuna'), ('Rotas', 'Rotas'), ('Componentes encontrados', 'Implementação encontrada')])
cards.append('<article data-group="'+escape(group(r))+'" data-module="'+escape(r['Módulo'])+'"><h2>'+escape(r['ID'])+' · '+escape(r['Módulo'])+'</h2><p>'+escape(group(r))+' · '+escape(r['Estado'])+' · '+escape(r['Prioridade'])+'</p><dl>'+body+'<dt>Próxima ação</dt><dd>'+escape(action(r))+'</dd><dt>Critério de aceitação</dt><dd>'+escape(r['Critério de aceitação'])+'</dd></dl><details><summary>Fontes, versões e evidência do requisito</summary><dl>'+''.join('<dt>'+escape(k)+'</dt><dd>'+escape(r[k])+'</dd>' 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'])+'</dl></details></article>')
summary = '; '.join(g+': '+str(counts[g]) for g in groups)
page = '''<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>InvestMoz — pedidos abertos</title><style>body{font:16px/1.55 system-ui;margin:0;background:#f5f7fa;color:#172332}main{max-width:1050px;margin:auto;padding:24px}header{position:sticky;top:0;background:#f5f7fa;padding:12px 0;border-bottom:1px solid #9aa}input,select{font:inherit;padding:9px;max-width:100%;margin:4px}article{background:white;border:1px solid #ccd5df;border-radius:8px;padding:20px;margin:18px 0}h1{font-size:1.7rem}h2{font-size:1.15rem}dt{font-weight:650;margin-top:12px}dd{margin-left:0;white-space:pre-wrap;overflow-wrap:anywhere}details{margin-top:16px}article[hidden]{display:none}</style><main><h1>Pedidos anteriores ainda abertos</h1><p>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.</p><p>''' + escape(summary) + '''</p><p>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.</p><header><label>Pesquisar <input id="q" type="search" placeholder="ID, pedido, campo ou fonte"></label><label>Grupo <select id="g"><option value="">Todos</option>''' + ''.join('<option>'+escape(g)+'</option>' for g in groups) + '''</select></label><label>Módulo <select id="m"><option value="">Todos</option>''' + ''.join('<option>'+escape(m)+'</option>' for m in sorted({r['Módulo'] for r in opened})) + '''</select></label><p id="result" role="status">3.349 requisitos apresentados</p></header>''' + ''.join(cards) + '''</main><script>const cards=[...document.querySelectorAll('article')];const q=document.querySelector('#q'),g=document.querySelector('#g'),m=document.querySelector('#m');const search=cards.map(c=>c.textContent.toLocaleLowerCase('pt'));function filter(){const term=q.value.toLocaleLowerCase('pt');let n=0;cards.forEach((c,i)=>{c.hidden=!!((g.value&&c.dataset.group!==g.value)||(m.value&&c.dataset.module!==m.value)||(term&&!search[i].includes(term)));if(!c.hidden)n++;});document.querySelector('#result').textContent=n+' requisitos apresentados';}[q,g,m].forEach(c=>c.addEventListener('input',filter));</script></html>'''
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))