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

215 lines
33 KiB
Python

"""Seal only the demonstrated QA delta and production assets; no activation."""
from pathlib import Path
from datetime import datetime, timezone
import hashlib, json, shutil, zipfile, re, sys, xml.etree.ElementTree as ET
root=Path(__file__).resolve().parent.parent
private=root/'storage/app/private/investmoz-v2/portal-audit'
plan=json.loads((private/'portal-stability-latest.json').read_text(encoding='utf-8-sig'))
source=Path(plan['source']); assert source.is_relative_to(private)
consolidated='--consolidated' in sys.argv
final_documentary='--final-documentary' in sys.argv
assert not final_documentary or consolidated
out=root/('outputs/investmoz-opportunity-workflow-consolidated-final-20261011' if final_documentary else 'outputs/investmoz-opportunity-workflow-consolidated-20261011' if consolidated else 'outputs/investmoz-opportunity-workflow-candidate-20261011')
assert not (out/'manifesto.json').exists(), 'A sealed candidate must not be overwritten'
out.mkdir(exist_ok=True)
sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
read=lambda n:json.loads((private/n).read_text(encoding='utf-8-sig'))
def prose(text):
units='tabelas|ficheiros|sequências|seq|MFA|contas|organizações|representações|territórios|geometrias|horas|rotas|requisitos|referências|linhas|consultas|filtros|registos|ativos|assets|itens|serviço|colunas|provas|gates|envelopes'
parts=re.split(r'(`[^`]*`)',text)
for i in range(0,len(parts),2):
parts[i]=re.sub(r'(\d+)('+units+r')(?=\b|\d)',r'\1 \2',parts[i])
parts[i]=re.sub(r'('+units+r')(?=\d)',r'\1 ',parts[i])
parts[i]=parts[i].replace('COUNTRY02ON','COUNTRY02 ON').replace('COUNTRY02OFF','COUNTRY02 OFF').replace('COUNTRY02 permaneceON','COUNTRY02 permanece ON').replace('A/B eC37','A/B e C37').replace(' e0',' e 0')
parts[i]=re.sub(r'\b(Promotor|Validador|Publicador|Conta|conta|documento|arrendamento|venda|Venda|Land|metadata|renda)(\d+)',r'\1 \2',parts[i])
parts[i]=re.sub(r'(\d+)(MZN|ocorrências|novosIDs|hashes|px)\b',r'\1 \2',parts[i])
parts[i]=re.sub(r'\b(de|em|da|nos|os|as)(?=\d)',r'\1 ',parts[i])
return ''.join(parts)
expected={
'app/Services/OpportunityCanonicalContract.php','app/Services/OpportunityCanonicalResponse.php',
'app/Services/OpportunityProfileWorkflow.php','app/Services/OpportunityReviewConditions.php',
'app/Services/OpportunityReviewMateriality.php','app/Services/PortalProfileAccess.php',
'resources/js/components/opportunity-canonical-fields.tsx','resources/js/components/opportunity-parcel-geometry.tsx',
'resources/js/components/opportunity-profile-actions.tsx','resources/js/components/territory-geometry-preview.tsx',
'resources/js/pages/management/opportunities.tsx','resources/js/pages/management/opportunity-draft.tsx'}
delta={f.relative_to(source).as_posix():sha(f) for d in ['app','bootstrap','config','database','resources','routes']
for f in (source/d).rglob('*') if f.is_file() and
(not (root/f.relative_to(source)).exists() or sha(f)!=sha(root/f.relative_to(source)))}
assert set(delta)==expected, 'Unreviewed source difference'
build=read('functional-production-build-20261011.json')
assets={f.relative_to(source/'public/build').as_posix():sha(f) for f in (source/'public/build').rglob('*') if f.is_file()}
assert build['result']=='passed' and assets==build['assets'] and build['active_assets_preserved']
for name,h in read('functional-active-files-before-20261011.json').items():assert sha(root/name)==h, 'Active baseline changed'
proof_names=[
'functional-workflow-chromium-20261011.json','functional-land-publication-chromium-20261011.json',
'functional-coedit-resources-chromium-20261011.json','functional-documents-chromium-20261011.json',
'functional-filters-chromium-20261011.json','functional-filters-persistence-qa-20261011.json',
'functional-accessibility-chromium-20261011.json','functional-joint-chromium-20261011.json',
'functional-materiality-semantic-qa-20261011.json','functional-category-semantics-qa-20261011.json',
'functional-conditions-semantic-qa-20261011.json','functional-final-persistence-qa-20261011.json',
'functional-backup-restoration-qa-20261011.json','functional-production-build-20261011.json',
'functional-country-compatibility-qa-20261011.json']
if consolidated:
proof_names.remove('functional-backup-restoration-qa-20261011.json')
proof_names.remove('functional-country-compatibility-qa-20261011.json')
proof_names += ['functional-lease-editor-chromium-20261011.json','functional-lease-persistence-qa-20261011.json',
'functional-lease-workflow-chromium-20261011.json','functional-lease-workflow-persistence-qa-20261011.json','functional-lease-visible-chromium-20261011.json',
'functional-consolidated-backup-restoration-qa-20261011.json','functional-consolidated-country-compatibility-qa-20261011.json','functional-lease-consolidation-20261011/verification.json']
for name in proof_names:assert read(name)['result']=='passed', name
backup=read('functional-consolidated-backup-restoration-qa-20261011.json' if consolidated else 'functional-backup-restoration-qa-20261011.json')
assert backup['build_manifest_sha256']==build['manifest_sha256']
assert sha(Path(backup['dump_path']))==backup['dump_sha256'] and sha(Path(backup['files_path']))==backup['files_sha256']
dest=read('functional-destination-services-readonly-20261011.json')
dependency_names=['database/migrations/2026_10_09_020000_extend_opportunity_management.php',
'database/migrations/2026_10_09_030000_extend_opportunity_resources.php',
'database/migrations/2026_10_10_040000_create_portal_profile_authorization.php',
'app/Services/OpportunityResourceInstallation.php','app/Services/PortalProfileInstallation.php',
'app/Services/OrganizationCountryInstallation.php','composer.lock','package-lock.json']
dependencies={n:sha(source/n) for n in dependency_names}
assert all(sha(root/n)==h for n,h in dependencies.items()),'Dependency changed outside scope'
payload=out/'payload'; payload.mkdir(exist_ok=True)
for name in delta:
target=payload/name;target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(source/name,target)
for name in assets:
target=payload/'public/build'/name;target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(source/'public/build'/name,target)
payload_hashes={f.relative_to(payload).as_posix():sha(f) for f in payload.rglob('*') if f.is_file()}
assert len(payload_hashes)==len(delta)+len(assets),'Stale payload file'
code_revision=hashlib.sha256(json.dumps(delta,sort_keys=True,separators=(',',':')).encode()).hexdigest()
core={
'format':'investmoz-opportunity-workflow-candidate-1','base_revision':'485ee04c40b2754ab668f48fd84d8559fb19a8b98f4b9af50fc0517bb488ca76',
'code_revision':code_revision,'git_commit':False,'payload':payload_hashes,
'base_hashes':{n:sha(root/n) if (root/n).exists() else None for n in delta},
'unchanged_dependencies':dependencies,'production_manifest_sha256':build['manifest_sha256'],
'profile_fingerprint':backup['profile_fingerprint'],
'included_categories':['investment_project','real_estate','land_rights','business_sale','business_partnership','franchise'],
'scope':'Six tested base branches, existing compatible Response, conditions/materiality, resources, filters and nine-route accessibility',
'excluded':['integrated journey/190000','legacy','experimental modules','offers','SLA','opportunity restoration pending decision','full normative Response/27','all category modalities','individual opportunity-document ACL','real devices/full accessibility'],
'effects':{'code_files':12,'new_service_files':1,'assets':len(assets),'new_routes':0,'configuration_files':0,'new_runtime_services':0,
'prospective_installations':['030000','040000'],'prospective_new_tables':7,'prospective_new_columns':5,
'automatic_new_accounts':0,'automatic_organizations':0,'automatic_representations':0,'automatic_publications':0,'active_writes_this_increment':0},
'preserved':['COUNTRY02 ON/050000','HTTPS/session cookie/history encryption','A/B and C37 closures','existing URLs/IDs/classifications/relations/audit'],
'COUNTRY02_compatibility':read('functional-consolidated-country-compatibility-qa-20261011.json' if consolidated else 'functional-country-compatibility-qa-20261011.json'),
'QA_backup':{k:backup[k] for k in ['at','source','restored','tables','sequences','files','dump_sha256','files_sha256','MFA_envelopes_readable','intentional_negative_controls','historical_cipher_bytes_unchanged','no_runtime_key_changes']},
'QA_evidence':{name:sha(private/name) for name in proof_names},
'destination_service_proof_sha256':sha(private/'functional-destination-services-readonly-20261011.json'),
'activation_authorized':False,'activation_ready':False,
'activation_blocks':['Destination ClamD signatures_stale (>48h)','Destination supervised queue worker not yet demonstrated','Legitimate role holders/designations and authorization scope not assigned','Final target-specific backup/restoration/readiness previews required','Administrative map limits require separate geometry activation: destination has zero geometries'],
'destination_inventory':read('functional-target-readonly-20261011.json'),
'restoration_proposals':{'R-ARCHIVE':'Independent authorized Publisher, reason/audit -> needs_information/nonpublic new review, never automatic republication',
'R-DRAFT':'Author/authorized organization Promotor only never-submitted/never-published logical draft -> draft; separate policy decision'},
'QA_without_integrated_journey':True,
}
if consolidated:
previous=root/'outputs/investmoz-opportunity-workflow-candidate-20261011'
prior=json.loads((previous/'manifesto.json').read_text(encoding='utf-8'))
assert sha(previous/'investmoz-workflow-3d9d392e0a0f.zip')=='6366c49d44cdc4242f4f93da5f5c1a53f2292d9eb485f70ef2b4e9a9f88589e9'
assert sha(previous/'manifesto.json')=='7cd603d8810a7633b1315345f3ecbd217a35519fa420c05438e2f9a3b2d71e07'
assert code_revision==backup['code_revision']=='f07f34a11eabc35d13d3bae88df2c10f1420f582e4600dafa88628dde6195612'
changes=[name for name,h in payload_hashes.items() if prior['payload'].get(name)!=h]
assert changes==['app/Services/PortalProfileAccess.php'], changes
core['scope']='Six demonstrated base branches, plus own-document lease54 correction/independent approval/restricted publication and C125; compatibility Response, materiality/conditions/resources/filters/scoped accessibility'
core['previous_candidate']={'revision':prior['revision'],'code_revision':prior['code_revision'],'archive_sha256':'6366c49d44cdc4242f4f93da5f5c1a53f2292d9eb485f70ef2b4e9a9f88589e9','manifest_sha256':sha(previous/'manifesto.json'),'preserved':True}
core['additional_changes']={'C125':{'file':changes[0],'effect':'Deduplicate visible organization/type creation options without changing grants, representations or authorization'},'code_delta':'f07f34a11eab','lease54':'New browser/API/SQL evidence and three own synthetic documents; no additional application code change','recompilation_required':False,'reason':'Only PHP differs from previous sealed candidate; all React/TypeScript/CSS and all 158 production assets match exactly'}
core['QA_backup']['code_revision']=backup['code_revision']
core['QA_backup']['acervo']=backup['acervo']
core['documentary_reconciliation']={'identity_confirmation_sha256':sha(private/'segmentation-identity-confirmation-20261011/verification.json'),'source_sha256':'bf26caeee0437a8d292bf29be59247e2862c14e79b5ae465c00322747af72af5','QA_resource_id':29,'metadata_version':3,'same_exemplar_aliases':['segmentar processos v1(3).docx','segmentar processos v1.docx'],'new_import':False,'original_in_public_package':False,'automatic_active_import':False,'private_original_in_QA_backup':True}
core['production_build_reused_from_previous_candidate']=True
if final_documentary:
core['documentary_manifest_format']='consolidated-final-2: explicit current backup/file breakdown; historical backup count retained separately'
revision=hashlib.sha256(json.dumps(core,sort_keys=True,separators=(',',':'),ensure_ascii=False).encode()).hexdigest()
manifest={**core,'revision':revision,'sealed_at':datetime.now(timezone.utc).isoformat()}
(out/'manifesto.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
xml=ET.parse(source/'storage/functional-regression-20261011.xml').getroot().find('testsuite')
quality={'recorded_at':datetime.now(timezone.utc).isoformat(),'code_revision':code_revision,
'production_manifest_sha256':build['manifest_sha256'],
'regression':{k:int(xml.attrib[k]) for k in ['tests','assertions','errors','failures','skipped']},
'regression_xml_sha256':sha(source/'storage/functional-regression-20261011.xml'),
'typescript':{'command':'npm.cmd run types:check','exit_code':0,'source':'Successful tool result in this QA increment'},
'phpstan':{'scope':'Six affected PHP services','exit_code':0,'errors':0,'memory_limit':'512M CLI only','source':'Successful tool result after scoped query typing correction'},
'criterion_evidence':core['QA_evidence'],'no_test_count_as_acceptance_substitute':True}
(out/'evidencias-por-criterio.json').write_text(json.dumps(quality,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
report=(root/'docs/investmoz/v2/auditoria-portal/workflow-criterios-saida-20261011.md').read_text(encoding='utf-8').split('<!-- functional-candidate-link-20261011 -->')[0].rstrip()+'\n'
if consolidated:
current=(root/'docs/investmoz/v2/auditoria-portal/workflow-criterios-saida-20261011.md').read_text(encoding='utf-8')
report+='\n<!-- workflow-lease-consolidated-20261011 -->\n'+current.split('<!-- workflow-lease-consolidated-20261011 -->')[1]
report=report.replace('Restauro QA de12:10:22UTC','Restauro QA anterior de12:10:22UTC')
report+=f'''\n## Revisão selada\n\nRevisão de conteúdo **{revision[:12]}** (não é commit Git); código **{code_revision[:12]}**. Delta:12ficheiros, incluindo1serviço novo; bundle de produção:158ficheiros, manifesto SHA-256 `{build['manifest_sha256']}`. Instalações030000/040000 são dependências futuras, não executadas no ativo neste incremento.\n\nRestauro QA real de{backup['at']}:295tabelas,31sequências,270ficheiros.16envelopes MFA normais recuperáveis; controlo negativo19 conservou a rejeição esperada. Chaves correspondentes conservadas apenas no backup privado; nenhuma chave/cipher/linha histórica alterada. Os295 incluem7tabelas de030000/040000 ausentes no ativo288. Este backup **não substitui** o backup/restauro atual do alvo para uma ativação.\n\nOs seis filtros coincidem também com SQL independente (`functional-filters-persistence-qa-20261011.json`). A qualidade estática e a regressão apoiam as provas por critério, sem encerrar modalidades não ensaiadas. Estado: **candidata QA delimitada para revisão, disponibilização ativa condicionada**.\n'''
report+='\n**Efeito futuro no fingerprint de COUNTRY02:** o componente comum PortalProfileAccess muda o fingerprint da prévia, de162daff6ce08 para b244311e5b28. QA confirmou extensão instalada/habilitada; nenhuma mudança no schema/dados/cifra de país. No ativo atual permanece162daff6ce08. Depois de eventual disponibilização desta candidata, renovar prévias/chaves de operação; não reaplicar050000 nem reutilizar readiness da revisão anterior.\n'
if consolidated:
report=report.replace(f"Restauro QA real de{backup['at']}:295tabelas,31sequências,270ficheiros",f"Restauro QA real de{backup['at']}:{backup['tables']} tabelas,{backup['sequences']} sequências,{backup['files']} ficheiros")
report=report.replace('b244311e5b28',core['COUNTRY02_compatibility']['candidate_COUNTRY02_fingerprint'][:12])
report=report.replace('bundle de produção:158ficheiros','bundle de produção reutilizado e verificado:158ficheiros')
report += f'''\n## Delta adicional e arrendamento\n\nC125/delta **f07f34a11eab** integrado. Face à candidata3d9d392e0a0f, somente PortalProfileAccess.php muda: uma opção visível por organização/tipo, preservando concessões e autorização. Código **{code_revision[:12]}**; nenhum React/TypeScript/CSS alterado e todos os158assets iguais. **Não exige recompilação**. A candidata anterior permanece com manifesto/ZIP intactos.\n\nQA54: três PDFs próprios — título/registo, licença de utilização e avaliação — identificados como simulações, scaneados pelo ClamD real e recuperados por hash. Promotor21 submeteu; Validador24 solicitou correção; Promotor21 corrigiu e resubmeteu; Validador24 reviu os três gates e aprovou; Publicador25 disponibilizou exclusivamente no âmbito restrito. Renda4500MZN/mês preservada em todas as transições, incluindo novas sessões; outra organização não acede ao contrato/anexo. Nada confere direito real ou publicação pública.\n\nBackup/restauro privado renovado de{backup['at']}, associado ao código/bundle acima e ao acervo efetivo: todos os originais QA referidos, documento29 com metadata3/alias confirmado e três PDFs próprios54. Os12ficheiros do delta também foram recuperados por hash. Não transfere fixtures, credenciais, dados ou originais privados para o pacote público. Não renova o backup do alvo ativo nem autoriza importação ou ativação.\n'''
if final_documentary:
counts={prefix:sum(n.startswith(prefix) for n in backup['file_manifest']) for prefix in ['private/','build/','runtime/','candidate-code/']}
category_table=current.split('<!-- workflow-lease-consolidated-20261011 -->')[1].split('| Categoria |')[1].split('\n\nComuns:')[0]
report=f'''# Revisão consolidada de Gestão de Oportunidades — {revision[:12]}
**Candidata QA delimitada; ativação não autorizada nem pronta no destino.** Código `{code_revision}`. COUNTRY02 ativo, A/B e C37 conservam os fechos. A base ativa não foi alterada. A candidata anterior `3d9d392e0a0f` permanece com manifesto e ZIP intactos.
## Resultado do arrendamento
Em `https://portal-investidor.test:18445`, QA isolada `stability_consumers_20261011_084534_test:55441`, a oportunidade 54 demonstrou: documentos próprios → submissão → solicitação fundamentada de correção → correção/resubmissão → três revisões independentes → aprovação → disponibilização com acesso restrito → recuperação noutra sessão.
Promotor QA21 prepara e corrige; Validador QA24 revê/aprova; Publicador QA25 publica. Os três documentos de título/registo, licença de utilização e avaliação são **simulações QA sem efeitos jurídicos**, com bytes distintos, scan real ClamD, armazenamento cifrado e recuperação por SHA-256. Não são fontes oficiais nem atos externos reais. A publicação não integra o catálogo público. Outra organização recebeu HTTP403 no contrato e no anexo; o autor não dispõe das ações de aprovação/publicação.
**Renda 4.500 MZN / mês:** o valor canónico `4500` e o código `monthly` permaneceram iguais em 14 recuperações de interface/Response, desde o rascunho até à publicação. Nova sessão apresenta esses valores; SQL, snapshots cifrados de edição, versão assinada e auditoria corroboram. Venda 31 e os seis casos anteriores mantêm versões/hashes; coedição e segregação com múltiplos papéis reutilizam as provas existentes. As diferenças de maiúsculas e a seleção de fieldsets corrigidas no roteiro eram falhas do teste, sem alteração do portal.
Evidências por critério: `functional-lease-workflow-chromium-20261011.json`, `functional-lease-workflow-persistence-qa-20261011.json` e `functional-lease-visible-chromium-20261011.json`, ligadas por hash no manifesto. Cookies, passwords, segredos e códigos MFA estão ausentes dos recibos.
## Alteração de código e compilação
C125 / delta **f07f34a11eab** altera somente `PortalProfileAccess::navigation` face à candidata anterior: uma opção por organização/tipo, mesmo com concessões sobrepostas. Promotor QA21 tem 10 opções únicas; QA22 conserva a única opção autorizada. Nenhum papel, representação, permissão, classificação, schema ou dado foi convertido.
O conjunto face ao ativo conserva 12 ficheiros de código, incluindo um serviço novo, e 158 assets. **Não é necessária nova recompilação:** os ficheiros React/TypeScript/CSS e todos os assets são byte a byte iguais aos da candidata anterior. Manifesto de produção `{build['manifest_sha256']}`; lint PHP atualizado passou. Regressão, análise estática e provas anteriores são reutilizadas somente onde o código/critério não mudou; a aceitação não decorre da contagem global de testes.
## Critérios atuais das seis categorias
| Categoria |{category_table}
Comuns ainda fora do fecho: Response normativo §27 integral (o contrato compatível expõe `normative_response_complete=false`), ACL documental individual de oportunidades, restauração da oportunidade e modalidades não ensaiadas. R-ARCHIVE propõe Publicador independente, motivo e auditoria, para revisão não pública; nunca repõe publicação. R-DRAFT requer política própria para rascunho nunca submetido/publicado. Ambas aguardam decisão, sem implementação silenciosa.
Acessibilidade das nove rotas e apresentações 320/390 px anteriores mantidas. Leitor de ecrã real, dispositivos físicos e PDFs etiquetados continuam separados. Legado, ofertas, competências modulares e SLA não integram este fecho. Cinco fases/19 etapas e os roteiros existentes preservados.
## Backup, restauro e acervo efetivo
Restauro QA renovado em **{backup['at']}**: **{backup['tables']} tabelas, {backup['sequences']} sequências e {backup['files']} ficheiros**, com comparação de tabelas/sequências/bytes e hashes. Os ficheiros correspondem a {counts['private/']} originais privados, {counts['build/']} assets de produção, {counts['runtime/']} ficheiros privados de configuração/cifra e {counts['candidate-code/']} ficheiros do delta. Código `{code_revision[:12]}` e bundle acima foram recuperados.
Inclui o original do documento 29, SHA-256 `bf26caeee0437a8d292bf29be59247e2862c14e79b5ae465c00322747af72af5`, metadata 3 e a identidade confirmada dos nomes `segmentar processos v1(3).docx` / `segmentar processos v1.docx`; inclui também os três PDFs próprios da oportunidade 54. Não houve reimportação ou novo documento por causa do alias. Dezasseis envelopes MFA normais são recuperáveis com as chaves correspondentes conservadas apenas no backup privado; o controlo negativo 19 mantém a rejeição esperada. Nenhuma chave ou cifragem histórica foi alterada.
O restauro anterior de 12:10:22 UTC continha 270 ficheiros e permanece preservado; não corresponde aos novos anexos/delta. A diferença 295 QA / 288 ativo são as sete tabelas das extensões 030000/040000 presentes no clone. **Este ensaio não renova o backup/restauro do alvo ativo.** O ZIP público contém código, bundle e relatórios; não contém bases, chaves, credenciais ou originais privados. Acervo e fixtures QA não se transferem automaticamente para o ativo.
## Dependências, limites e reversão
Sequência futura: 030000 → 040000, prévias e ações autenticadas correspondentes, bootstrap e habilitações legítimos separados. 190000 entra apenas com o percurso integrado; não bloqueia o workflow isolado. COUNTRY02 continua instalado/ON: não reaplicar 050000. O fingerprint da sua prévia muda para `{core['COUNTRY02_compatibility']['candidate_COUNTRY02_fingerprint'][:12]}` com o código comum; renovar prévias/chaves quando necessário, sem reutilizar readiness anterior.
Bloqueios do destino: manutenção/scan real de ClamD (a última leitura registou assinaturas obsoletas), worker supervisionado, responsáveis/representações legítimos e novo backup/restauro/prévias do alvo. A última leitura ativa encontrou zero geometrias; mapas administrativos demonstrados em QA exigem importação cartográfica separadamente autorizada. A ausência de geometria deve continuar explícita. Nada cria automaticamente contas, organizações, representações ou publicações.
Falha futura: interromper antes de aplicação com prévia divergente. Depois de uso, conter pelas flags do pacote, preservando COUNTRY02, HTTPS, chaves, tabelas, dados e histórico. Não executar `down` ou restauro completo; eventual retorno de código exige compatibilidade com os dados/revisões v3. Restauro completo é decisão separada. Procedimento único e hashes no pacote.
Mesma matriz: 3.401 requisitos/linhas, 7.739 ocorrências, nove requisitos existentes atualizados em cada projeção; nenhum novo ID. Os estados gerais permanecem, porque as lacunas acima não foram encerradas pelo caso de arrendamento.
'''
(out/'relatorio.md').write_text(prose(report),encoding='utf-8')
procedure=f'''# Procedimento da candidata {revision[:12]} — preparação, sem ativação autorizada\n\nCOUNTRY02 permaneceON. A/B eC37 não se repetem. Esta revisão difere da485ee04c40b2 apenas nos12ficheiros descritos no manifesto e no bundle recompilado. Não muda HTTPS, cookie, APP_KEY, PHP, DLL ou vhosts. Não inclui190000/importações/publicações automáticas.\n\n## Condições antes de pedir disponibilização\n\n1. Conferir pacote,12hashes de código,158assets e dependências inalteradas. Confirmar020000/050000 instalados,030000/040000 ausentes ou íntegros; interromper com schema parcial/hash diferente.\n2. Resolver manutenção das assinaturas do ClamD destino3310 e comprovar scan real de original+derivada. A prova de12:01:25UTC revelou51,59h e `signatures_stale`; a QA13310 fresca não substitui esta condição. Confirmar GD/WebP e worker supervisionado da fila `database`, processamento até `clean` e recuperação do mesmoSHA. Configuração de fila não comprova execução.\n3. Designar legitimamente dois titulares existentes com MFA e terceiro gestor independente para bootstrap. Separadamente habilitar Promotor, Validador e Publicador por tipo/âmbito; independência funcional não exige que os revisores sejam superutilizadores. Para caso organizacional usar organização/representação reais; para individual `organization_id=null` exige o perfil autorizado. Não criar fixtures nem nomear pessoas por inferência. Sem habilitações não há percurso completo.\n4. Renovar backup/restauro **investemoz:5432**, incluindo originais/chaves/configuração/bundle e comparação com esta revisão.295tabelas da QA não representam as288atuais do alvo. Gerar os recibos específicos030000/040000 com seus fingerprints atuais, destino, validade e hashes; não copiar o recibo QA como prontidão ativa. Ensaiar no clone final a sequência030→040 com o estado anterior e o bundle fixo, reutilizando provas frias/idempotência/auditoria anteriores apenas se hashes/condições corresponderem.\n5. Obter autorização da revisão/âmbito futuros. As autorizações de COUNTRY02 não abrangem esta candidata.\n\n## Sequência exata após cumprir condições e autorização própria\n\n1. Disponibilizar os12ficheiros+bundle mantendo `PORTAL_ORGANIZATION_COUNTRY_ENABLED=true`, e flags de oportunidades/perfis inicialmenteOFF; preservar toda a configuração atual de HTTPS/cookie/cifra. Não executar `migrate` global. Atualizar caches correspondentes somente no procedimento autorizado e conferir os hashes efetivamente servidos. Não reiniciar Apache sem necessidade concreta; se necessário, usar o controlo normal Laragon e verificar vhosts.\n2. Gestor verificado/MFA: GET `/management/opportunity-resources-install`. Conferir destino, fingerprint030, mudanças e `ready`; confirmar na interface. POST **mesma rota** com `hash` da prévia e `confirm=true`; aplica somente `2026_10_09_030000_extend_opportunity_resources`. Esperado:3tabelas+`documents.storage_encryption`, ledger e1audit; nenhum dado/publicação. Se já instalado e íntegro, pular aplicação. Prévia divergente interrompe antes de escrita.\n3. GET `/management/profile-install` pelo gestor/MFA. Conferir020+030, schema/ledger, destino/fingerprint040, backup e `ready`. POST **mesma rota**, pela interface: `hash`, `confirm`, UUID estável de operação e `reason`. Aplica somente `2026_10_10_040000_create_portal_profile_authorization`:4tabelas+1coluna users+3opportunities. Repetição do conteúdo com mesma chave retorna recibo; chave com conteúdo diferente409. Sem contas/papéis automáticos. Não instalar190000.\n4. Bootstrap separado POST `/management/profile-install/bootstrap`, com dupla designada, gestor independente/MFA, prévia/hash, motivo, confirmação e chave estável. Repetição sem nova dupla; par existente exige substituição atómica, não novo bootstrap. Habilitações funcionais pelo fluxo existente com autoria/competência/organização/tipo válidos.\n5. Habilitar somente `PORTAL_PROFILE_ACCESS_ENABLED`, `PORTAL_OPPORTUNITY_DASHBOARD_ACCESS_ENABLED` e `PORTAL_OPPORTUNITY_SPECIALIZED_DRAFTS_ENABLED` previstos. Mantermodulares/legado/percurso integrado/importações separados eOFF. Mostrar Minhas oportunidades/Nova oportunidade aos Promotores; outros utilizadores veem pedido de perfil.\n6. Sucesso:dashboard→rascunho incompleto recuperável→recursos privados→submissão→correção→parecer/condição→aprovação independente→publicação explícita com âmbito→nova sessão/recuperação. Conferir recusas e proteção documental, indicadores/SQL, auditoria, COUNTRY02ON e nenhum pacote extra. Aprovação interna/limite administrativo não representam ato externo/direito/parcela. Documentar a modalidade efetivamente utilizada; não declarar todas as variantes demonstradas.\n\n## Contenção e reversão\n\nAntes de uso, falha de instalação deve manter transação/ledger coerentes. Depois de uso, desligar só as3flags deste pacote, conservar COUNTRY02ON/HTTPS/chaves/colunas/tabelas/originais/histórico e restringir o acesso; não executar `down`, drop ou restauro completo. Código/bundle485 só podem voltar depois de verificar compatibilidade com dados/assinaturas v3. O fingerprint novo impede reutilizar prévias antigas. Restauro completo, se necessário, é decisão específica com preservação das escritas posteriores e teste de recuperação.\n\nRestauração de oportunidade ainda não integra a candidata: R-ARCHIVE eR-DRAFT aguardam decisões distintas. A retirada/restauro do recurso documental ensaiado é operação diferente.\n'''
procedure+='\n## Limites cartográficos no destino\n\nLeitura READONLY de12:14:31UTC:1990territórios e0geometrias. A candidata não inclui importação/divulgação cartográfica. Se a disponibilização incluir apresentação dos limites administrativos demonstrados em QA, a importação correspondente é uma dependência e requer pacote/ativação próprios; cadastro territorial não comprova mapa funcional. Até aí, a interface deve apresentar a indisponibilidade real sem inventar limites.\n\n## Prévia COUNTRY02\n\nA alteração de PortalProfileAccess muda o fingerprint da prévia COUNTRY02 para b244311e5b28 nesta candidata. A extensão permanece instalada/ON: não reaplicar050000. Renovar a prévia e chave de operação quando necessário, sem reutilizar readiness162daff6ce08. No ativo atual o fingerprint162daff6ce08 continua intacto; isto descreve o efeito de uma futura disponibilização autorizada.\n'
if consolidated:
procedure=procedure.replace('b244311e5b28',core['COUNTRY02_compatibility']['candidate_COUNTRY02_fingerprint'][:12])
procedure=procedure.replace('bundle recompilado','bundle de produção anteriormente compilado e novamente verificado por hash')
procedure += f'''\n## Âmbito consolidado adicional\n\nFace à3d9d392e0a0f, C125 altera somente PortalProfileAccess; a prova específica de arrendamento54 acrescenta documentos próprios e o workflow completo no âmbito restrito. Os hashes de produção são idênticos: recompilação adicional dispensada por ausência de alterações frontend. O backup QA renovado em{backup['at']} corresponde a{code_revision[:12]}, inclui a metadata3 da fonte segmentar processos, todos os originais QA e os12ficheiros do delta. Antes de disponibilização futura, renovar o backup/restauro **do alvo real**, comprovar as dependências e obter autorização específica.\n\nO acervo e as fixtures QA não se instalam com o código. O documento29 conserva a mesma identidade/bytes; uma futura inclusão no acervo ativo tem a sua prévia/importação autenticada/idempotente, original privado e scan próprio, sem divulgação automática. Publicação da oportunidade continua decisão explícita de perfil/âmbito. O presente ZIP não contém originais privados, bases, chaves ou credenciais.\n'''
(out/'procedimento.md').write_text(prose(procedure),encoding='utf-8')
archive=out/f'investmoz-workflow-{revision[:12]}.zip'
entries=[*payload.rglob('*'),out/'manifesto.json',out/'relatorio.md',out/'procedimento.md',out/'evidencias-por-criterio.json']
with zipfile.ZipFile(archive,'w',zipfile.ZIP_DEFLATED)as z:
for f in sorted(entries):
if f.is_file():z.write(f,f.relative_to(out).as_posix())
with zipfile.ZipFile(archive)as z:
assert z.testzip() is None
# Public password-input/login chunks are legitimate assets. Check exact
# payload membership instead of confusing UI filenames with secret files.
assert set(z.namelist())=={'payload/'+n for n in payload_hashes}|{'manifesto.json','relatorio.md','procedimento.md','evidencias-por-criterio.json'}
assert all(not any(part in {'private','vendor','node_modules'} or part.startswith('.env') for part in Path(n).parts) for n in z.namelist())
receipt={'revision':revision,'code_revision':code_revision,'archive':archive.name,'sha256':sha(archive),'manifest_sha256':sha(out/'manifesto.json'),'code_files':len(delta),'assets':len(assets),'QA_restore_verified':True,'activation_ready':False,'activation_authorized':False,'active_writes':0}
(out/'pacote.json').write_text(json.dumps(receipt,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps(receipt,ensure_ascii=False))