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

119 lines
12 KiB
Python

"""Verify the final document increment; never alter the sealed workflow or active app."""
from pathlib import Path
from datetime import datetime,timezone
from collections import Counter
import csv,hashlib,json,re
csv.field_size_limit(20_000_000)
root=Path(__file__).resolve().parent.parent
private=root/'storage/app/private/investmoz-v2/portal-audit'
directory=private/'segmentation-reconciliation-20261011'
docs=root/'docs/investmoz/v2/auditoria-portal'
out=root/'outputs/investmoz-segmentation-reconciliation-20261011'
read=lambda p:json.loads(p.read_text(encoding='utf-8-sig'))
sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
packet=read(directory/'csv-edits.json');counts=read(directory/'counts.json')
matrix=list(csv.DictReader((docs/'matriz-requisitos.csv').open(encoding='utf-8-sig',newline='')))
by_id={r['ID']:r for r in matrix};assert len(by_id)==len(matrix)==3401
assert sum(int(r['Quantidade de linhas de origem']) for r in matrix)==7739
assert Counter(r['Estado'] for r in matrix)==Counter(counts['states'])
for row in packet['files'][0]['rows'][1:]:
previous=dict(zip(packet['files'][0]['rows'][0],row));actual=by_id[previous['ID']]
for field in ['ID','Pedido/trecho','Estado','Critério de aceitação','Disponibilidade ativa']:
assert actual[field]==previous[field]
old=json.loads(previous['Critérios e versões por origem (JSON)'] or '[]')
assert json.loads(actual['Critérios e versões por origem (JSON)'])[:len(old)]==old
source_rows=list(csv.reader((docs/'inventario-fontes.csv').open(encoding='utf-8-sig',newline='')))
assert len(source_rows)-1==604
assert len({(r[0],r[2]) for r in source_rows[1:]})==604
for old in packet['files'][2]['rows'][1:]:
current=next(r for r in source_rows[1:] if r[0]==old[0] and r[2]==old[2])
assert all(current[i]==v for i,v in enumerate(old) if i!=4)
assert current[4].startswith(old[4])
aliases=read(directory/'source-byte-aliases.json');assert len(aliases)==3
for alias in aliases:
assert not any(r[0]==alias['additional_path'] and r[2]==alias['sha256'] for r in source_rows[1:])
current=next(r for r in source_rows[1:] if r[0]==alias['canonical_path'] and r[2]==alias['sha256'])
assert alias['additional_path'] in current[4]
for name,h in read(private/'functional-active-files-before-20261011.json').items():assert sha(root/name)==h
candidate=root/'outputs/investmoz-opportunity-workflow-candidate-20261011'
package=read(candidate/'pacote.json')
zip_file=next(p for p in candidate.glob('investmoz-workflow-3d9d392e0a0f.zip'))
assert sha(zip_file)=='6366c49d44cdc4242f4f93da5f5c1a53f2292d9eb485f70ef2b4e9a9f88589e9'
assert sha(candidate/'manifesto.json')=='7cd603d8810a7633b1315345f3ecbd217a35519fa420c05438e2f9a3b2d71e07'
for mode in ['preview','apply','replay']:assert read(directory/('library-'+mode+'-qa.json'))['result']=='passed'
visual=read(private/'segmentation-library-chromium-20261011.json');assert visual['result']=='passed'
assert visual['document']['sha256']=='bf26caeee0437a8d292bf29be59247e2862c14e79b5ae465c00322747af72af5'
dates=read(directory/'source-date-correction-qa.json')
assert dates['result']=='passed' and dates['metadata_version']==2 and dates['historical_revisions']==2
assert dates['document_date'] is None and dates['issued_on'] is None
assert dates['source_sha256']==visual['document']['sha256']
replay=read(directory/'library-replay-qa.json')
assert replay['before']==replay['after'] and replay['retrieval'][0]['resource_metadata_version']==2
confirmed=read(directory/'source-provenance.json')['exact_requested_exemplar_confirmed']
def readable(text):
parts=re.split(r'(`[^`]*`)',text)
replacements={'bibliotecaQA':'biblioteca QA','acervoQA':'acervo QA','nome(3)':'nome (3)','funcional3':'funcional 3',
'PropriedadesOOXML':'Propriedades OOXML','criadorOsman':'criador Osman','revision15':'revision 15',
'criação2026':'criação 2026','alteração2026':'alteração 2026','versãoDOCX':'versão DOCX',
'gestorQA17':'gestor QA17','PromotorQA21':'Promotor QA21','ClamDreal/mesmohash':'ClamD real/mesmo hash',
'mesmosbytes':'mesmos bytes','ficheiroJSON':'ficheiro JSON','noDOCX':'no DOCX','recurso29':'recurso 29',
'criação1':'criação 1','recebe403':'recebe 403','recebe404':'recebe 404','público29':'público 29',
'nesseDOCX':'nesse DOCX','noDOCX':'no DOCX','6categorias':'6 categorias','9rotas':'9 rotas',
'8IDs':'8 IDs','39IDs':'39 IDs','327blocos':'327 blocos','74secções':'74 secções','7739ocorrências':'7739 ocorrências',
'7concluídos':'7 concluídos','3236parciais':'3236 parciais','152não':'152 não','1implementado':'1 implementado',
'5dependentes':'5 dependentes','matriz3401':'matriz 3401','préviasQA':'prévias QA','COUNTRY02ON':'COUNTRY02 ON',
'ativo,A/B':'ativo, A/B','QA295':'QA 295','alvo288':'alvo 288','ofertas/modulares':'ofertas/módulos',
'190000 só':'190000 só','SLA':'SLA','workflow6categorias':'workflow de 6 categorias',
'candidata3d9d392e0a0f':'candidata 3d9d392e0a0f','exemploOOXML':'exemplo OOXML','de08/10':'de 08/10',
'emQA':'em QA','ramoQA':'ramo QA','perfilv1':'perfil v1','porURL':'por URL','exemplarDOCX':'exemplar DOCX',
'sem030/040/190':'sem 030/040/190','temCOUNTRY02':'tem COUNTRY02','ClamD3310':'ClamD 3310',
'demonstrarworker':'demonstrar worker','suasflags':'suas flags','oZIP':'o ZIP','noZIP':'no ZIP',
'backupQA':'backup QA','12:10:22UTC':'12:10:22 UTC','rotasQA':'rotas QA','A/B/C37':'A/B/C37',
'CódigoComercial':'Código Comercial','Por8':'Por 8','simulaçãoQA':'simulação QA','cargaQA':'carga QA',
'DoisXLSX':'Dois XLSX','antes de providers':'antes de carregar os serviços','pela ferramentaweb':'pela ferramenta web',
'doBR229 noINTIC':'do BR229 no INTIC','naBVM':'na BVM','por provedores':'por provedores'}
for i in range(0,len(parts),2):
for a,b in replacements.items():parts[i]=parts[i].replace(a,b)
parts[i]=re.sub(r'([,;])(?=\S)',r'\1 ',parts[i])
parts[i]=re.sub(r'(\d+)(novos|ficheiros|requisitos|parágrafos|linhas|ligações|ocorrências|tabelas|secções|objetos|alterações|blocos|IDs|rotas|categorias|contas|organizações|geometrias)(?=\b)',r'\1 \2',parts[i])
return ''.join(parts)
report=readable((docs/'segmentacao-processos-reconciliacao-20261011.md').read_text(encoding='utf-8'))
if '## Identidade por bytes e localizações' not in report:
report+='\n## Identidade por bytes e localizações\n\nForam inventariadas 10 localizações no diretório de origem: 7 conjuntos de bytes novos e 3 já conhecidos. As 3 localizações de bytes existentes foram conservadas como aliases nas entradas originais, sem outro registo de catálogo ou importação. O inventário canónico passou de 597 para 604 entradas; o inventário do diretório conserva todos os caminhos e anexos. O controlo de integridade confirmou as 597 entradas anteriores e o histórico dos 3393 requisitos anteriores.\n'
if '<!-- segmentation-source-date-correction -->' not in report:
note='<!-- segmentation-source-date-correction -->\n\n**Datas e histórico:** o auxiliar de importação QA tinha utilizado a data de alteração OOXML como data de emissão. Esse preenchimento foi corrigido apenas neste recurso QA: `document_date` e `issued_on` ficam indeterminados, as datas OOXML permanecem na proveniência e a revisão inicial permanece recuperável. Foi acrescentada a revisão de metadados 2 e a respetiva auditoria; o original, o hash, o scan e a classificação privada não mudaram. A repetição posterior conservou as contagens e recuperou os mesmos bytes. Evidências: `source-date-correction-qa.json` e `library-replay-qa.json`. Não se atribui esta correção de preparação de dados ao funcionamento de autenticação ou ao código do portal.\n\n'
report=report.replace('## Aliases documentais para IDs existentes',note+'## Aliases documentais para IDs existentes',1)
for a,b in {'alterado.327':'alterado. 327','significam327':'significam 327','pontuaisQA':'pontuais QA','Recurso29/documento589':'Recurso 29/documento 589','workflow6 categorias':'workflow de 6 categorias','usarSimulation':'usar Simulation','dos§§':'dos §§','OsURLs':'Os URLs','do BR229':'do BR 229','privadoQA':'privado QA','de12:10':'de 12:10','assinaturasClamD':'assinaturas ClamD','workflow:020':'workflow: 020','ativados030':'ativados 030','ouSLA':'ou SLA','fechosA/B':'fechos A/B','preservandoHTTPS':'preservando HTTPS'}.items():
report=report.replace(a,b)
(docs/'segmentacao-processos-reconciliacao-20261011.md').write_text(report,encoding='utf-8')
(out/'relatorio.md').write_text(report,encoding='utf-8')
for filename in ['backlog.md','relatorio-executivo.md']:
file=docs/filename;text=file.read_text(encoding='utf-8')
end=text.index('<!-- segmentation-reconciliation-20261011:end -->')+len('<!-- segmentation-reconciliation-20261011:end -->')
file.write_text(readable(text[:end])+text[end:],encoding='utf-8')
receipt={'result':'passed','at':datetime.now(timezone.utc).isoformat(),'requirements':3401,'matrix_rows':3401,'source_occurrences':7739,
'source_inventory_entries':604,'original_requirements_preserved':3393,'old_source_entries_preserved':597,
'new_unique_source_bytes':7,'known_byte_aliases':3,'new_requirement_IDs':counts['new_IDs'],
'states':counts['states'],'original_exemplar_sha256':visual['document']['sha256'],
'actual_requested_exemplar_pending':not confirmed,'exact_requested_exemplar_confirmed':confirmed,'no_runtime_code_changed_by_document_increment':True,
'COUNTRY02_and_AB_C37_preserved':True,'active_writes':0,
'sealed_workflow_candidate':{'revision':'3d9d392e0a0f','zip_sha256':sha(zip_file),'manifest_sha256':sha(candidate/'manifesto.json')},
'document_increment_after_QA_backup':True,'rule_activation':False,'publication':False,
'unknown_issue_dates_preserved':True,'metadata_version':2,'metadata_correction_audited':True,
'files':{name:sha(docs/name) for name in ['matriz-requisitos.csv','backlog-priorizado.csv','inventario-fontes.csv','segmentacao-processos-reconciliacao-20261011.md']},
'QA_evidence':{name:sha(directory/name) for name in ['library-preview-qa.json','library-apply-qa.json','library-replay-qa.json','library-replay-before-date-correction-qa.json','source-date-correction-qa.json']}}
receipt['QA_evidence']['segmentation-library-chromium-20261011.json']=sha(private/'segmentation-library-chromium-20261011.json')
if confirmed:
confirmation_dir=private/'segmentation-identity-confirmation-20261011'
receipt['identity_confirmation']=read(confirmation_dir/'identity-confirmation.json')
receipt['metadata_version']=read(confirmation_dir/'source-identity-qa.json')['metadata_version']
receipt['QA_evidence']['source-identity-qa.json']=sha(confirmation_dir/'source-identity-qa.json')
(directory/'final-integrity.json').write_text(json.dumps(receipt,ensure_ascii=False,indent=2),encoding='utf-8')
(out/'verificacao.json').write_text(json.dumps(receipt,ensure_ascii=False,indent=2),encoding='utf-8')
manifest={'format':'investmoz-document-reconciliation-1','scope':'Confirmed v1(3) exemplar, local filename v1.docx; no active activation' if confirmed else 'Available v1 exemplar; requested v1(3) identity still pending; no active activation',
'source_sha256':visual['document']['sha256'],'files':{p.name:sha(p) for p in out.iterdir() if p.is_file() and p.name!='manifesto.json'},
'runtime_code_files_changed':0,'active_writes':0,'rule_activation':False,'publication':False,'workflow_candidate_unchanged':'3d9d392e0a0f'}
(out/'manifesto.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps({'result':'passed','requirements':3401,'source_occurrences':7739,'source_inventory_entries':604,'prior_decisions_preserved':True,'exact_requested_exemplar_pending':not confirmed,'active_writes':0}))