Files
Investidor-V2/tools/portal-audit-legacy-update.py
T
nala 20971d523f
tests / ci (push) Canceled after 0s
oportunidades 6
2026-10-10 21:17:46 +02:00

152 lines
23 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
"""Extend the sole audit matrix; preserve every previous source/criterion/state."""
import collections
import csv
import hashlib
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOC = ROOT / 'docs/investmoz/v2/auditoria-portal'
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
csv.field_size_limit(5000000)
matrix = DOC / 'matriz-requisitos.csv'
rows = list(csv.DictReader(matrix.open(encoding='utf-8-sig')))
headers = list(rows[0])
protected = ['ID', 'Pedido/trecho', 'Fonte/ocorrências', 'Estado', '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 = {r['ID']: {k: r[k] for k in protected} for r in rows if not r['ID'].startswith('AUD-LEG-')}
tag = '[Legado QA 10/10/2026] '
evidence = 'legado-piloto-qa.md; legacy-content-http-qa.json: recursos6/5 públicos, hashes verificados, pesquisa, login/MFA21, criação/continuação, logout/nova sessão, recuperação, replay único e isolamento. Interface não observada.'
items = [
('01', 'Reconciliar páginas, imagens, anexos, categorias e URLs do legado com biblioteca/importações e auditoria', '92 URLs recolhidos,50 versões de artigos e2.826 referências inventariados;30.170 entradas de sitemap de outro domínio preservadas. Dois recursos únicos importados/repetição sem duplicação.', 'Cobertura histórica/paginação/destinos externos e restantes versões por reconciliar. Imagem histórica404 identificada.', 'Cada conteúdo/versão tem identidade, origem, autoria/datas conhecidas, hash, duplicados/conflitos e destino; inventário cobre as fontes acessíveis, sem omissão das não importadas.'),
('02', 'Adaptar cores, tipografia, imagens e padrões reais do legado aos componentes existentes, com acessibilidade e móvel', 'CSS/chunkwelcome e fotografia reais recuperados; Noto Sans/Serif locais, paleta vermelho/verde/fundo claro, cartões e foco/labels preparados no buildQA.', 'Render móvel/desktop, teclado e contraste efetivo no navegador pendentes.', 'Comparar com referências reais e demonstrar leitura/ações em desktop/móvel/teclado; sem imagens quebradas ou controlo inacessível.'),
('03', 'Piloto página de agronegócio → artigo → localizações/oportunidades → iniciar/continuar ideia no dossier', 'Rotas públicas e fontes da biblioteca; uma oportunidade publicada no setor; links da carteira/mapas; criação e continuação com referência ao artigo no payload cifrado e evento existentes.', 'Percurso visual completo pendente; seis referências territoriais não delimitadas/associadas por inferência.', 'Percorrer as ações na interface, guardar, recuperar noutra sessão, continuar e voltar ao conteúdo; dados/contextos separados e próxima ação existente.'),
('04', 'Consulta pública sem login; autenticação apenas para guardar/acompanhar dados pessoais', 'Artigo, imagem, biblioteca e pesquisa respondem sem login; início/associação ao dossier requer sessão verificada. Outra conta403, contexto privado ausente em leitura pública/alheia.', 'Verificação visual dos pontos de autenticação e sessão pendente; C37 permanece aberto.', 'Leitor anónimo consulta tudo o que foi autorizado; apenas gravação privada pede autenticação; nenhum dado do dossier vaza para anónimos/outras contas.'),
('05', 'Fonte única associada a setores, territórios e etapas com fundamento; artigo não vira oportunidade concreta', 'Documento único6 e imagem5; três links com fundamento: tema, thumbnail e apoio editorialS01. Relações de oportunidades pelo setor já cadastrado; zero oportunidade ou geometria criada pelo artigo.', 'Correspondências territoriais concretas e ampliação por etapa/setor ainda pendentes; não inventar ligações.', 'Mesma fonte/versionamento reutilizados; relações têm citação/âmbito; cadastros reais e artigos distintos; ausência de relação é apresentada explicitamente.'),
('06', 'Preservar contexto ao entrar/regressar ao percurso, URLs existentes e correspondências de migração', 'Mantido /agronegocio; /explorar/agronegocio novo. Referência comID/hash/versão/ator/data e retorno autorizado ao mesmo dossier; replay sem duplicação; cinco fases/19etapas conservadas.', 'Mapa final de migração de outros URLs e demonstração visual de retorno pendentes.', 'URL antigo recupera a mesma fonte; login/retorno preservam a escolha; nova sessão recupera; nenhuma mistura entre projetos nem conversão de roteiro legado.'),
('07', 'Verificar imagens, anexos, links, pesquisa, permissões e duplicações, registando aceitação visual separadamente', 'Dois originais com ClamD real/hash; links dos consumidores200; pesquisahit/miss; permissões; repetição do importador sem novas linhas e associação idempotente.', 'Visual pendente; anexos/referências de restantes conteúdos e erro404 do legado por resolver individualmente.', 'Cada consumidor apresenta ficheiro/link/resultado íntegro; importação/pesquisa/recuperação/permissões verificadas e resultados visuais registados por ação.')
]
rows = [r for r in rows if not r['ID'].startswith('AUD-LEG-')]
for suffix, request, implementation, gap, acceptance in items:
ident = 'AUD-LEG-' + suffix
origin = 'USR-LEG-20261010-' + suffix
row = {k: '' for k in headers}
row.update({'ID': ident, 'Pedido/trecho': request, 'Fonte/ocorrências': 'Pedido direto do responsável,10/10/2026, integração do legado emQA §' + str(int(suffix)) + '; https://investemoz.tempo.co.mz/; legado-piloto-qa.md',
'Módulo': 'biblioteca / conteúdo público / percurso do investidor', 'Rotas': '/explorar/agronegocio; /agronegocio; /documentos/6; /investimentos/a-partir-de/agronegocio; /investimentos/{id}/conteudos',
'Implementação encontrada': implementation, 'Disponibilidade ativa': 'Não disponibilizado; guarda QA/flagfalse. Nenhuma escrita ou divulgação ativa autorizada.',
'Evidência funcional': evidence, 'Estado': 'parcial', 'Lacuna': gap, 'Dependência': 'Biblioteca/documentos existentes; Balcão+190000 para referência ao dossier; navegador para aceitação visual. Perfis/SLA são decisões independentes.',
'Critério de aceitação': acceptance, 'Prioridade': 'P1', 'Versões divergentes': 'Legado post171985 modificado2026-04-20; categoria Oportunidades preservada como metadado, entidade guia. Metadados/sitemapInfopublica e área de investimento distintos; não remapear por nome.',
'IDs das linhas de origem': origin, 'Quantidade de linhas de origem': '1',
'Critérios e versões por origem (JSON)': json.dumps([{'id': origin, 'pedido': request, 'fontes': ['Pedido direto10/10/2026 §' + str(int(suffix))], 'aceitacao': acceptance, 'estado': 'parcial', 'variantes': 'IntegraçãoprogressivaQA, sem substituição de pendências anteriores', 'modulo': 'legado/biblioteca/percurso'}], ensure_ascii=False),
'Interface': 'React preparado e buildQA; dados Inertia recuperados. Navegador indisponível; roteiro manual em legado-piloto-qa.md. Aceitação visual pendente.',
'API': 'GET públicos e escritas autenticadas no servidor18097, alvo real55441 confirmado; publicação separada do importador.',
'Gravação': 'Dois recursos na biblioteca existente e referências no payload cifrado do dossier; sem nova tabela/cadastro paralelo.',
'Recuperação': 'Logout/nova sessão FortifyMFA21: fonte/versão/hashes recuperados; outra conta negada e projeto independente sem referência.',
'Resultado apresentado': 'Dados retornam ao artigo/dossier/consumidores; render/cliques ainda não observados.',
'Consolidação': 'Novo pedido verificável incorporado à matriz existente; não duplica nem encerra requisitos históricos.'})
rows.append(row)
for row in rows:
if row['ID'] in ['AUD-LIB-01', 'AUD-LIB-02', 'AUD-DOC-01']:
for key, value in {'Evidência funcional': evidence, 'Lacuna': 'Legado:50 versões observadas e sitemap30.170 entradas inventariados; cobertura histórica não encerrada. Visual/C37 pendentes.'}.items():
if tag not in row[key]: row[key] += ' | ' + tag + value
if row['ID'] in ['AUD-OP-04', 'AUD-PROFILE-28']:
modular = '[Experiência modular QA 10/10/2026] '
if modular not in row['Evidência funcional']:
row['Evidência funcional'] += ' | ' + modular + 'QA42–48:sete modalidades dos quatro módulos percorrem rascunho→submissão→correção→revisão/gates→aprovação→divulgação restrita e nova sessãoMFA. QA49 entidade semexercícios com declaração/projeções identificadas. Recibos opportunity-modular-http-qa.json e opportunity-history-http-qa.json; C67 projeção de resumo corrigida. Extensões explicitamenteQA; legados36–39 preservados. Visual, materialidade, Response normativo e aprovação funcional/compromissosSLA pendentes; confirmação do legado não aprova esses pontos.'
assert before == {r['ID']: {k: r[k] for k in protected} for r in rows if not r['ID'].startswith('AUD-LEG-')}
assert len({r['ID'] for r in rows}) == len(rows)
with matrix.open('w', encoding='utf-8-sig', newline='') as f:
w = csv.DictWriter(f, fieldnames=headers); w.writeheader(); w.writerows(rows)
counts = {'requirements': len(rows), 'matrix_rows': len(rows), 'source_occurrences': sum(int(r['Quantidade de linhas de origem']) for r in rows), 'states': dict(collections.Counter(r['Estado'] for r in rows))}
(PRIVATE / 'legacy-audit-update-qa.json').write_text(json.dumps(counts, ensure_ascii=False, indent=2), encoding='utf-8')
marker = 'qa-legacy-20261010'
def prepend(name, content):
import re
path = DOC / name
text = path.read_text(encoding='utf-8-sig')
text = re.sub(r'<!-- ' + marker + r':begin -->.*?<!-- ' + marker + r':end -->\s*', '', text, flags=re.S)
first, _, rest = text.partition('\n')
replacements = {'exclusivamenteQA': 'exclusivamente em QA', 'um sitemap de outro domínio': 'um sitemap de outro domínio', 'Perfismodulares/SLA': 'Perfis modulares/SLA',
'Legado:50': 'Legado: 50', 'ClamD real nos dois': 'ClamD real nos dois', 'imagemhistórica404': 'imagem histórica 404',
'I0–I3/C37': 'I0–I3/C37', 'comPromotorQA21': 'com Promotor QA21', 'LEG07': 'LEG07', 'porperfil': 'por perfil',
'links não recolhidos antesde': 'links não recolhidos antes de', 'imagemlegada': 'imagem legada',
'original recuperado comfundamentoou': 'original recuperado com fundamento ou', 'artigosconsumidores': 'artigos consumidores',
'visual/materialidade/Response/aprovaçãoformal': 'visual/materialidade/Response/aprovação formal',
'50 exemplares': '50 exemplares', 'perfilmodular': 'perfil modular', 'folgaQA': 'folga QA',
'173lacunasgeo': '173 lacunas geo', '(Mandie/Guroambíguo)': '(Mandie/Guro ambíguo)', '030000→040000→190000conservado': '030000 → 040000 → 190000 conservado',
'recursoslegado/cartografia/divulgação': 'recursos do legado/cartografia/divulgação', 'revisãocandidata/config/bundle': 'revisão candidata/config/bundle',
'backup/restauroda baseativa correspondente': 'backup/restauro da base ativa correspondente', 'rasculhosprivados': 'rascunhos privados',
'hash/idjáexistirem': 'hash/ID já existirem', 'conflitosaguardamrevisão': 'conflitos aguardam revisão', 'Cartografiaéoutrolote, nãoautopublicado': 'Cartografia é outro lote, sem publicação automática',
'ocultarflag/bundlecompatível': 'ocultar a flag/usar bundle compatível', 'originais, versões, fontes dosdossiers': 'originais, versões, fontes dos dossiers',
'baseativa+candidata': 'base ativa + candidata', 'Decisõesperfis/SLA,C37,Promotor21visual,A/Bcontinuamabertas': 'Decisões de perfis/SLA, C37, Promotor21 visual e A/B continuam abertas',
'ClamD real/hash': 'ClamD real/hash', 'doexemplar doartigoJSONno importadorexistente': 'do exemplar do artigo JSON no importador existente',
'MIMEapplication/json': 'MIME application/json', 'uploadJSONguardadoQA': 'upload JSON guardado em QA',
'originalpreservado': 'original preservado', 'hashverificado': 'hash verificado', 'publicaçãoautenticada': 'publicação autenticada',
'Doisrecursos; repetição semduplicação': 'Dois recursos; repetição sem duplicação', 'imagem original; observado404': 'imagem original; observado 404',
'Identificado noinventário': 'Identificado no inventário', 'PNGdoagronegócio': 'PNG do agronegócio', 'emartigosconsumidores dessaimagem': 'em artigos consumidores dessa imagem',
'summary`de500carateres': 'summary` de 500 carateres', 'a499+reticência': 'a 499 + reticência',
'depoisQA42–48divulgadas/recupe­radas noutraMFA. Visualpendente': 'depois QA42–48 divulgadas/recuperadas noutra sessão MFA. Visual pendente',
'conforme autorizaçãoexistente; gestorQA14executou': 'conforme autorização existente; gestor QA14 executou',
'C37nãofoiretestadonemencerrado': 'C37 não foi retestado nem encerrado'}
for source, replacement in replacements.items(): content = content.replace(source, replacement)
path.write_text(first + '\n\n<!-- ' + marker + ':begin -->\n' + content + '\n<!-- ' + marker + ':end -->\n\n' + rest.lstrip(), encoding='utf-8')
summary = f'''## Frente atual — legado agronegócio, exclusivamenteQA
Piloto: **http://127.0.0.1:18097/explorar/agronegocio → /agronegocio → /investimentos/a-partir-de/agronegocio**. Artigo6 e imagem5 reutilizam biblioteca; leitura/pesquisa/imagem públicas, gravação privada autenticada. HTTP/persistência demonstrados: criação e continuação, logout/novaMFA21, recuperação, replay, isolamento e retorno com contexto. ClamD real nos dois originais. Uma oportunidade publicada no setor permanece independente; nenhuma oportunidade/geo inferida do artigo.
Inventário: **92URLs**, **50 versões de artigos**, **2.826 referências**, **30.170 entradas anunciadas num sitemap de outro domínio**, sem afirmar recolha integral. Dois recursos importados; repetição sem duplicação. Fonte/autoria/datas/versão/hash e diferenças preservadas. C68:imagemhistórica404. Cobertura documental histórica permanece aberta.
Mesma matriz: **{counts['requirements']} requisitos únicos = {counts['matrix_rows']} linhas lógicas; {counts['source_occurrences']} ocorrências de origem**. Estados: ''' + '; '.join(f'{n} {state}' for state, n in counts['states'].items()) + '''. Sete pedidos novosLEG01–07 incorporados, todos parciais. Nenhum critério anterior foi encerrado por esta frente.
Referências visuais reais: CSS/chunkwelcome, fotografia original, Noto Sans/Serif locais no buildQA, paleta/cartões adaptados. **Navegador integrado indisponível; aceitação visual pendente**, com roteiro executável. C37/QA18, Promotor21 visual e A/B continuam abertos. Perfismodulares/SLA têm decisões independentes; a confirmação do legado não os aprova.
Relatório e critérios: [legado-piloto-qa.md](legado-piloto-qa.md); inventários `legado-inventario.csv`, `legado-conteudos-reconciliados.csv`, `legado-referencias.csv`, `legado-sitemap-anunciado.csv`. Recibos privados sanitizados por percurso. Ativo não alterado; candidata ainda não fixada. Blocos anteriores são evidências históricas datadas, não conclusão global atual. Merece distinção o ensaio modularQA42–49 já demonstrado porHTTP, comvisual/materialidade/Response/aprovaçãoformal pendentes.'''
prepend('relatorio-executivo.md', summary)
prepend('backlog.md', '''## Próximo lote — expansão do legado sem substituirI0–I3/C37
| Prioridade / requisito | Próxima ação concreta | Critério de fecho |
| --- | --- | --- |
| P1 / AUD-LEG-02/03/04/06/07 | Roteiro visual do piloto comPromotorQA21: leitura anónima, pesquisa, fonte, criação/continuação, nova sessão, retorno e móvel/teclado. | Ações observadas por perfil/consumidor; mesmosIDs/dados, recuperação/negações e resultado visível. C37 continua independente. |
| P1 / AUD-LEG-01/05; LIB01/02; DOC01 | Reconciliar os50 exemplares observados, paginação/URLs externos e sitemap misto; importar apenas próximo lote com hash/versão/scan e divulgação separada. | Inventário/deduplicação/citações e destinos explícitos; sem omitir nãoimportados nem publicar automaticamente. |
| P1 / AUD-LEG-03/05; GEO | Conferir seis nomes/âmbitos de corredores com cadastros/publicações existentes. | Relação territorial inequívoca com citação/IDs; geometria disponível distinguida de uso visual. Nada de inferir parcelas ou coberturas pelo nome. |
| P1 / AUD-LEG-07 / C68 | Rever imagemlegada GOVERNADOR-in.gif404 e outroslinks não recolhidos antesde expandir esses artigos. | Original recuperado comfundamentoou ausência identificada; não declarar ficheiro inexistente como válido. |
| P1 / OP04 / PROFILE28 | Continuar Response integral/materialidade e cerimónia funcional; quatro módulosQA42–48 e nohistóricoQA49 têm provaHTTP, não fecho visual/formal. | Critérios especializados por categoria; revisões afetadas, decisões/external/publicação distintas. Confirmação legado não aprova perfis/SLA. |
Países/origem, estatísticas,173lacunasgeo (Mandie/Guroambíguo), documentos por consumidor, recuperação e acessos/workflow restantes permanecem no backlog anterior. Próximo trabalho executável não depende de designar pessoas reais ou aprovar compromissosSLA. Plano global030000→040000→190000conservado; recursoslegado/cartografia/divulgação separados. Ver `legado-piloto-qa.md`.''')
prepend('plano-ativacao.md', '''## Complemento legado — preparaçãoQA, sem autorização ativa
Preservar **030000→040000→190000**, verificar ledger/schema/dependências no alvo antesde aplicar; não repetir instalações. Leitura editorial usa a biblioteca existente; associação ao dossier exige Balcão+190000. Perfismodulares não são pré-condição da leitura pública.
Ordem deste complemento: demonstração visualQA/correções→revisãocandidata/config/bundle→backup/restauroda baseativa correspondente→upload/importação autenticados como rasculhosprivados→hash/duplicados/ClamD/metadados→associações fundamentadas→divulgação explícita por recurso→disponibilização pública/percurso. O CLI importador sóaplicaQA; a guarda atual do piloto impede ativo, mesmo comflagtrue. Habilitação ativa requer revisão/autorização própria.
Contagem prevista: **1artigo+1imagem**, **3associações**, **0oportunidades ou geometrias criadaspeloartigo**, **0contas/dossiersQAtransferidos**; noopssehash/idjáexistirem, conflitosaguardamrevisão. Cartografiaéoutrolote, nãoautopublicado. Reversão:ocultarflag/bundlecompatível e retirar divulgação auditada; preservar originais, versões, fontes dosdossiers e histórico. BackupQA não substitui prova da baseativa+candidata. Decisõesperfis/SLA,C37,Promotor21visual,A/Bcontinuamabertas.''')
prepend('casos-reproducao.md', '''## Casos adicionais preservados — fontes e consumidor novo
- **C67 / OP04:** publicar a chamada de concessãoQA42 após aprovação. Esperado: projeção restrita persistida e legível. Observado inicial:HTTP503; rótulo de modalidade adicionado à síntese excedia coluna `summary`de500carateres. Causa no novo adaptador, não no middleware. Correção limita projeção a499+reticência e preserva texto canónico completo. Tentativa falhada arquivada; depoisQA42–48divulgadas/recupe­radas noutraMFA. Visualpendente.
- **C68 / LEG07:** GET público legado `/images/ANO-2017/JUNHO/21/GOVERNADOR-in.gif`. Esperado: imagem original; observado404. Identificado noinventário; não afeta PNGdoagronegócio. Falha permanece emartigosconsumidores dessaimagem; não inventar substituição ou autenticidade.
- **C69 / LIB02 / LEG01:** prévia doexemplar doartigoJSONno importadorexistente. Esperado: cadastro privado, scan e adaptação segura; observado MIMEapplication/json não suportado. Lista doimportador reconciliada e uploadJSONguardadoQA; originalpreservado, apresentação sanitizada, hashverificado, publicaçãoautenticada separada. Doisrecursos; repetição semduplicação. Não alargar divulgação ativa.
Superutilizador27 recebeu403 em gestão documental, conforme autorizaçãoexistente; gestorQA14executou divulgação. Esse ensaio negativo comprova segregação e não é uma falha a contornar. C37nãofoiretestadonemencerrado.''')
proposal = DOC / 'propostas-perfis-modulares-sla.md'
text = proposal.read_text(encoding='utf-8')
text = text.replace('a decisão QA seguinte tem precedência nos pontos identificados.', 'o ensaio técnico QA seguinte identifica as recomendações adotadas experimentalmente, sem decisão formal de ativação.')
text = text.replace('## Recomendações aceites para execução exclusivamente em QA — 10/10/2026', '## Recomendações ensaiadas tecnicamente em QA — decisões próprias pendentes')
text = text.replace('Esta decisão permite implementação e ensaios sintéticos; não declara os módulos completos nem aprova ativação.', 'A implementação e os ensaios sintéticos decorrem da autorização QA existente. As recomendações não são compromissos operacionais aprovados; a confirmação posterior sobre o legado refere-se somente ao legado e não decide perfis/SLA.')
proposal.write_text(text, encoding='utf-8')
sources = DOC / 'inventario-fontes.csv'
source_rows = list(csv.DictReader(sources.open(encoding='utf-8-sig')))
for path in [proposal, DOC / 'legado-piloto-qa.md', DOC / 'legado-inventario.csv', DOC / 'legado-conteudos-reconciliados.csv', DOC / 'legado-referencias.csv', DOC / 'legado-sitemap-anunciado.csv']:
data = path.read_bytes(); sha = hashlib.sha256(data).hexdigest()
record = next((r for r in source_rows if r['Exemplar/caminho'].replace('\\', '/').endswith('/' + path.name)), None)
if record is None:
record = {k: '' for k in source_rows[0]}; source_rows.append(record)
record['Exemplar/caminho'] = str(path)
elif record['SHA256'] and record['SHA256'] != sha:
record['Edição/versão identificada'] += ' | exemplar anterior SHA256=' + record['SHA256']
record.update({'Bytes': str(len(data)), 'SHA256': sha, 'Tipo': 'reconciliação auditoriaQA', 'Nota de leitura': 'Legado inventariado/pilotoQA; cobertura histórica e decisões separadas explicitamente preservadas.', 'Disponibilidade': 'Relatório/inventário para consulta; não habilita importação/publicação ativa', 'Uso em cálculo': 'Nenhum cálculo ativado', 'Situação jurídica/documental': 'Documento de programação/reconciliação, não fonte oficial', 'Edição/versão identificada': record['Edição/versão identificada'] + ' | atualizaçãoQA10/10/2026'})
with sources.open('w', encoding='utf-8-sig', newline='') as f:
w = csv.DictWriter(f, fieldnames=list(source_rows[0])); w.writeheader(); w.writerows(source_rows)
print(json.dumps(counts, ensure_ascii=True))