oportunidades 6
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-10 21:17:46 +02:00
parent 01405cbf8a
commit 20971d523f
126 changed files with 44362 additions and 3496 deletions
+1 -1
View File
@@ -254,7 +254,7 @@ class ImportDocumentBatch extends Command
throw new RuntimeException('File size is outside the 1 byte to 10 MiB import limit.');
}
$mime = (new \finfo(FILEINFO_MIME_TYPE))->file($path);
$allowed = ['application/pdf', 'text/plain', 'text/csv', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
$allowed = ['application/pdf', 'application/json', 'text/plain', 'text/csv', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'image/png', 'image/jpeg', 'image/webp'];
if (! is_string($mime) || ! in_array($mime, $allowed, true)) {
throw new RuntimeException('Detected media type is not supported by the document library.');
@@ -3,9 +3,11 @@
namespace App\Http\Controllers;
use App\Services\InvestorPath;
use App\Services\LegacyContent;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
@@ -21,7 +23,20 @@ class InvestorPathController extends Controller
public function create(Request $request, InvestorPath $path): JsonResponse|RedirectResponse
{
$id = $path->create($request->user(), $request->all());
$source = $request->input('content_resource_id');
if ($source !== null) {
abort_unless(filter_var($source, FILTER_VALIDATE_INT), 422);
app(LegacyContent::class)->articleForResource((int) $source);
}
$id = DB::transaction(function () use ($path, $request, $source): string {
$id = $path->create($request->user(), $request->all());
if ($source !== null) {
$case = $path->view($request->user(), $id)['case'];
app(LegacyContent::class)->attach($request->user(), $id, ['version' => $case['version'], 'resource_id' => (int) $source]);
}
return $id;
});
return $request->expectsJson() ? response()->json(['id' => $id], 201) : redirect('/investimentos/'.$id)->with('success', 'Investimento guardado no seu dossier.');
}
@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use App\Services\InvestorPath;
use App\Services\LegacyContent;
use App\Services\VirtualDesk;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
class LegacyContentController extends Controller
{
public function landing(Request $request, LegacyContent $legacy, string $sectorSlug = 'agronegocio'): Response
{
$filters = $request->validate(['q' => 'nullable|string|max:120']);
$articles = $legacy->articlesForGroup($sectorSlug);
$root = config('legacy_content.groups.'.$sectorSlug.'.root');
$article = $legacy->article($root);
$q = $filters['q'] ?? '';
$articles = array_values(array_filter($articles, fn ($item) => $q === ''
|| str_contains(mb_strtolower(strip_tags($item['html'].' '.$item['title'])), mb_strtolower($q))));
$match = $articles !== [];
return Inertia::render('portal/legacy', ['article' => $article, 'articles' => $articles, 'view' => 'landing', 'query' => $q, 'match' => $match]);
}
public function article(Request $request, LegacyContent $legacy, string $legacySlug = 'agronegocio'): Response
{
return Inertia::render('portal/legacy', ['article' => $legacy->article($legacySlug), 'view' => 'article', 'contentContext' => $this->context($request)]);
}
public function start(Request $request, LegacyContent $legacy, InvestorPath $path, string $legacySlug = 'agronegocio'): Response
{
abort_unless($path->available(), 404);
$article = $legacy->article($legacySlug);
return Inertia::render('investor-path/index', ['contexts' => $path->contexts($request->user()), 'contentSource' => $article]);
}
public function attach(Request $request, string $id, LegacyContent $legacy): JsonResponse|RedirectResponse
{
$legacy->attach($request->user(), $id, $request->all());
return $request->expectsJson() ? response()->json(['saved' => true]) : redirect('/investimentos/'.$id)->with('success', 'Referência guardada no dossier.');
}
/** @return array{id: string, title: string}|null */
private function context(Request $request): ?array
{
$data = $request->validate(['context' => 'nullable|uuid']);
if (! $request->user() || empty($data['context'])) {
return null;
}
try {
$case = app(VirtualDesk::class)->case($request->user(), $data['context'], true);
return $case->closed_at ? null : ['id' => $case->id, 'title' => $case->title];
} catch (HttpException $error) {
if (! in_array($error->getStatusCode(), [403, 404], true)) {
throw $error;
}
return null; // Consultation stays public; an unauthorized private context is never returned.
}
}
}
@@ -7,6 +7,7 @@ use App\Models\Opportunity;
use App\Models\Sector;
use App\Services\InvestmentGeography;
use App\Services\OpportunityCanonicalContract;
use App\Services\OpportunityCanonicalResponse;
use App\Services\OpportunityManagement;
use App\Services\OpportunityManagementInstallation;
use App\Services\OpportunityMaturity;
@@ -126,6 +127,7 @@ class OpportunityManagementController extends Controller
'canonicalOptions' => ($record?->owner_user_id !== null || str_starts_with($request->path(), 'minhas-oportunidades')) ? app(OpportunityCanonicalContract::class)->options($request->user(), $record) : null,
'workflowState' => $record ? $access->state($record) : null,
'publicationState' => $record?->publicationPresentation(),
'canonicalResponse' => $record?->owner_user_id !== null ? app(OpportunityCanonicalResponse::class)->read($request->user(), $record) : null,
'canEditDraft' => $record ? $access->canEdit($request->user(), $record) : false,
'workflowParticipants' => $record && $record->owner_user_id !== null ? app(OpportunityProfileWorkflow::class)->participants($request->user(), $record) : ['validators' => [], 'publishers' => []],
'workflowTasks' => $record && $record->owner_user_id !== null ? app(OpportunityWorkflowTasks::class)->listing($record) : [],
@@ -151,6 +153,12 @@ class OpportunityManagementController extends Controller
return response()->json(['id' => $record->id, 'etag' => $service->etag($record), 'message' => 'Rascunho guardado no servidor. Não foi submetido, aprovado ou publicado.'], $id ? 200 : 201)->header('ETag', '"'.$service->etag($record).'"');
}
public function canonicalResponse(Request $request, int $id, OpportunityCanonicalResponse $response): JsonResponse
{
return response()->json($response->read($request->user(), Opportunity::withTrashed()->findOrFail($id)))
->header('Cache-Control', 'private, no-store');
}
public function lifecycle(Request $request, int $id, OpportunityManagement $service): JsonResponse
{
abort_unless($service->installed(), 503);
@@ -138,13 +138,20 @@ class OpportunityResourceController extends Controller
$districts = [];
foreach ($locations as $location) {
$p = $location->payload;
if (empty($p['province_id']) || empty($p['district_id']) || empty($p['role'])) {
$issues[] = 'Uma localização tem província, distrito ou papel pendente.';
$mozambican = empty($p['country_code']) || $p['country_code'] === 'MZ';
if ($mozambican && (empty($p['province_id']) || empty($p['district_id']))) {
$issues[] = 'Uma localização em Moçambique tem província ou distrito pendente.';
}
if (! empty($p['district_id'])) {
if (! $mozambican && array_filter(array_intersect_key($p, array_flip(['province_id', 'district_id', 'admin_post_id', 'locality_id'])))) {
$issues[] = 'Uma localização noutro país conserva uma seleção territorial de Moçambique incompatível; corrigir explicitamente sem converter o histórico.';
}
if (empty($p['role'])) {
$issues[] = 'Uma localização tem papel pendente.';
}
if ($mozambican && ! empty($p['district_id'])) {
$districts[$p['district_id']] = true;
}
if (! empty($p['province_id'])) {
if ($mozambican && ! empty($p['province_id'])) {
$provinces[$p['province_id']] = true;
}
if (($p['role'] ?? null) === 'other' && empty($p['role_description'])) {
@@ -164,7 +171,7 @@ class OpportunityResourceController extends Controller
$issues[] = 'Multidistrito/interprovincial exige fundamento operacional de 50–1000 caracteres.';
}
if ($locations) {
$issues[] = 'Modo preparado: '.(count($locations) > 1 ? 'multiple' : 'single').'; âmbito derivado: '.(count($provinces) > 1 ? 'interprovincial' : (count($districts) > 1 ? 'multi_district' : 'district')).'. Sem conversão de contexto em implantação.';
$issues[] = 'Modo preparado: '.(count($locations) > 1 ? 'multiple' : 'single').'; âmbito administrativo moçambicano derivado: '.(count($provinces) > 1 ? 'interprovincial' : (count($districts) > 1 ? 'multi_district' : ($districts ? 'district' : 'sem seleção moçambicana'))).'. Sem conversão de contexto em implantação.';
}
foreach (['images', 'documents'] as $kind) {
foreach ($resources[$kind] as $r) {
@@ -8,6 +8,7 @@ use App\Models\Sector;
use App\Services\CountryCatalogue;
use App\Services\IdempotencyConflict;
use App\Services\InterestSubmission;
use App\Services\LegacyContent;
use App\Services\Malware\MalwareScanner;
use App\Services\VerifiedDocumentBinary;
use App\Services\VirtualDesk;
@@ -366,7 +367,7 @@ class PortalServicesController extends Controller
public function upload(Request $request): RedirectResponse
{
$this->validateManagement($request, ['file' => ['required', 'file', 'mimetypes:application/pdf,text/plain,text/csv,image/jpeg,image/png,image/webp,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.text', 'max:10240'],
$this->validateManagement($request, ['file' => ['required', 'file', 'mimetypes:application/pdf,text/plain,text/csv,image/jpeg,image/png,image/webp,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.text'.(app(LegacyContent::class)->available() ? ',application/json' : ''), 'max:10240'],
'original_source' => ['nullable', 'url:https', 'max:2000'], 'issuer' => ['nullable', 'string', 'max:180']]);
$file = $request->file('file');
$checksum = hash_file('sha256', $file->getRealPath());
@@ -635,7 +636,9 @@ class PortalServicesController extends Controller
$data = $this->validateManagement($request, [
'document_id' => ['required', 'uuid'], 'title' => ['required', 'string', 'max:200'],
'description' => ['nullable', 'string', 'max:4000'], 'category' => ['required', 'string', 'max:80'],
'topic' => ['required', 'string', 'max:80'], 'document_type' => ['required', 'in:legislation,procedure,official_form,digital_adaptation,guidance_template,guide,study,reference_data,unknown'],
// QA legacy originals may have no theme (e.g. a shared image). Do not invent a sector to disclose an authorized original.
'topic' => [app(\App\Services\LegacyContent::class)->available() ? 'nullable' : 'required', 'string', 'max:80'],
'document_type' => ['required', 'in:legislation,procedure,official_form,digital_adaptation,guidance_template,guide,study,reference_data,unknown'],
'issuer' => ['nullable', 'string', 'max:180'], 'document_number' => ['nullable', 'string', 'max:120'],
'document_date' => ['nullable', 'date_format:Y-m-d'], 'source_url' => ['nullable', 'url:https', 'max:2000'],
'applicable_from' => ['nullable', 'date_format:Y-m-d'],
@@ -656,6 +659,7 @@ class PortalServicesController extends Controller
'associations.*.applicability' => ['required', 'in:required,conditional,support,optional_template,evidence'],
'associations.*.condition_text' => ['nullable', 'string', 'max:2000'], 'associations.*.basis_reference' => ['nullable', 'string', 'max:2000'],
]);
$data['topic'] = $data['topic'] ?? '';
if (! empty($data['applicable_from']) && ! empty($data['applicable_until']) && $data['applicable_until'] < $data['applicable_from']) {
throw $this->managementValidationException(['applicable_until' => 'A data final deve ser igual ou posterior à data inicial.']);
}
@@ -144,7 +144,7 @@ class VirtualDeskController extends Controller
}
}
$this->auditRead($request, 'desk_requests', $id);
$data = ['countries' => app(CountryCatalogue::class)->all(), 'request' => [...(array) $row, 'payload' => $payload, 'submitted_snapshot' => $desk->decode($row->submitted_snapshot), 'editable_fields' => json_decode($row->editable_fields ?? '[]', true)],
$data = ['countries' => app(CountryCatalogue::class)->all(), 'toolPrefill' => $desk->requestToolPrefill($request->user(), $row), 'request' => [...(array) $row, 'payload' => $payload, 'submitted_snapshot' => $desk->decode($row->submitted_snapshot), 'editable_fields' => json_decode($row->editable_fields ?? '[]', true)],
'case' => ['id' => $case->id, 'title' => $case->title], 'caseVersion' => $case->version, 'definition' => $definition, 'territoryPaths' => $territoryPaths,
'serviceSource' => json_decode(DB::table('desk_service_versions')->where('id', $row->service_version_id)->value('source'), true),
'owner' => $desk->owns($request->user(), $case), 'manager' => $request->user()->can('manage-portal'), 'analyst' => (int) $row->analyst_id === $request->user()->id,
@@ -3,6 +3,7 @@
namespace App\Http\Middleware;
use App\Services\InvestorPath;
use App\Services\LegacyContent;
use App\Services\OpportunityManagement;
use App\Services\PortalProfileAccess;
use Illuminate\Http\Request;
@@ -51,6 +52,7 @@ class HandleInertiaRequests extends Middleware
'name' => config('app.name'),
'profileAccess' => app(PortalProfileAccess::class)->navigation($request->user()),
'portalOperator' => config('portal.operator_name'),
'legacyContentEnabled' => app(LegacyContent::class)->available(),
'educationEnabled' => (bool) config('education.enabled', false),
'aiEnabled' => (bool) config('investmoz_ai.enabled', false),
'existingBusinessEnabled' => (bool) config('existing-business.enabled', false),
+1
View File
@@ -131,6 +131,7 @@ final class InvestorPath
'canWrite' => $owner && ! $case->closed_at, 'canReview' => $manager && (int) $case->user_id !== $actor->id && ! $case->closed_at,
'next' => $next ? ['code' => $next['code'], 'title' => $next['title'], 'action' => 'Preparar', 'href' => '#'.$next['code']] : null,
'results' => $results, 'requests' => $requests, 'documents' => $documents,
'content_sources' => ($owner || $manager) ? ($path['content_sources'] ?? []) : [],
'tasks' => DB::table('tasks')->where('desk_case_id', $id)
->whereNull('deleted_at')
->when(! $owner && ! $manager, fn ($q) => $q->where('assignee_user_id', $actor->id))
+246
View File
@@ -0,0 +1,246 @@
<?php
namespace App\Services;
use App\Models\Opportunity;
use App\Models\Sector;
use App\Models\User;
use DOMDocument;
use DOMElement;
use DOMNode;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/** A reading adapter over the existing library, never a second content/opportunity register. */
final class LegacyContent
{
public function available(): bool
{
if (! config('portal.legacy_content_qa_enabled') || ! app()->environment('testing')
|| config('database.connections.pgsql.host') !== '127.0.0.1'
|| config('database.connections.pgsql.url') || (int) config('database.connections.pgsql.port') !== 55441) {
return false;
}
$actual = DB::selectOne('SELECT current_database() AS database, inet_server_port() AS port');
return $actual && str_ends_with($actual->database, '_test') && (int) $actual->port === 55441
&& $actual->database === config('database.connections.pgsql.database');
}
/** @return array<string, mixed> */
public function article(string $slug = 'agronegocio'): array
{
abort_unless($this->available(), 404);
$identity = config('legacy_content.articles.'.$slug);
abort_unless(is_array($identity) && ($identity['publishable'] ?? false), 404);
$row = $this->publicResources()->where('d.external_id', $identity['external_id'])->first();
abort_unless($row !== null, 404);
$binary = app(VerifiedDocumentBinary::class)->respond($row);
$bytes = $binary instanceof BinaryFileResponse ? file_get_contents($binary->getFile()->getPathname()) : $binary->getContent();
abort_unless(is_string($bytes) && strlen($bytes) <= 10485760 && hash_equals(trim($row->sha256), hash('sha256', $bytes)), 404);
try {
$source = json_decode($bytes, true, 64, JSON_THROW_ON_ERROR);
} catch (\JsonException) {
abort(404);
}
abort_unless(is_array($source) && ($source['format'] ?? '') === 'investmoz-legacy-article-v1'
&& ($source['source_url'] ?? '') === $row->source_url && $row->source_url === $identity['source_url']
&& is_string($source['content_html'] ?? null) && trim(strip_tags($source['content_html'])) !== '', 404);
foreach (['title', 'author', 'legacy_category', 'published_at', 'modified_at'] as $key) {
abort_unless(is_string($source[$key] ?? null), 404);
}
abort_unless(is_int($source['legacy_id'] ?? null), 404);
$image = $this->publicResources()->where('d.sha256', $source['image_sha256'] ?? '')->where('d.media_type', 'image/png')->first();
$sector = Sector::where('slug', $identity['sector_slug'])->where('is_active', true)->first();
// These are independently published records in the same sector, not claims of article/project linkage.
$opportunities = $sector ? Opportunity::published()->where('sector_id', $sector->id)->latest('published_at')
->limit(12)->get(['title', 'slug', 'province', 'summary']) : [];
return ['id' => (int) $row->resource_id, 'document_id' => $row->id, 'version' => (int) $row->metadata_version,
'sha256' => trim($row->sha256), 'title' => $source['title'], 'author' => $source['author'],
'legacy_id' => $source['legacy_id'], 'legacy_category' => $source['legacy_category'],
'published_at' => $source['published_at'], 'modified_at' => $source['modified_at'], 'source_url' => $source['source_url'],
'href' => '/'.$slug, 'html' => $this->localLinks($this->sanitize($source['content_html'])),
'landing_href' => '/explorar/'.$identity['group'], 'start_href' => '/investimentos/a-partir-de/'.$slug,
'group' => config('legacy_content.groups.'.$identity['group']),
'related_articles' => $this->related($identity['group'], $slug),
'image' => $image ? ['href' => '/documentos/'.$image->resource_id.'/view', 'sha256' => trim($image->sha256),
'alt' => $slug === 'agronegocio' ? 'Campo agrícola ao pôr do sol; fotografia ilustrativa do artigo Agronegocio'
: 'Imagem ilustrativa original do artigo '.$source['title'], 'source_url' => $image->source_url] : null,
'sector' => $sector?->only(['id', 'name', 'slug']), 'opportunities' => $opportunities,
'territorial_references' => array_values(array_filter($slug === 'agronegocio' ? ['Pemba–Lichinga', 'Nacala', 'Vale do Zambeze', 'Beira', 'Limpopo', 'Maputo'] : [],
fn ($name) => str_contains(strip_tags($source['content_html']), $name))),
'territorial_status' => 'Menções territoriais preservadas no artigo. Limites administrativos, parcelas e geometrias não associados automaticamente.',
'library_href' => '/documentos/'.$row->resource_id,
'consultation_state' => 'public', 'legal_state' => 'not_applicable', 'calculation_use' => false];
}
/** Resolve a library identity, preserving the already stored source URLs and versions. */
public function articleForResource(int $resourceId): array
{
abort_unless($this->available(), 404);
$externalId = $this->publicResources()->where('r.id', $resourceId)->first()?->external_id;
foreach (config('legacy_content.articles', []) as $slug => $identity) {
if ($identity['external_id'] === $externalId && $identity['publishable']) {
return $this->article($slug);
}
}
abort(422, 'Escolha um artigo disponível para consulta; a referência não foi guardada.');
}
/** @return list<array<string,mixed>> */
public function articlesForGroup(string $group): array
{
abort_unless($this->available() && is_array(config('legacy_content.groups.'.$group)), 404);
$articles = [];
foreach ($this->related($group) as $item) {
$articles[] = $this->article($item['slug']);
}
abort_unless($articles !== [], 404);
return $articles;
}
/** @return list<array<string,mixed>> */
private function related(string $group, ?string $except = null): array
{
$identities = array_filter(config('legacy_content.articles', []), fn ($identity, $slug) =>
$identity['group'] === $group && $identity['publishable'] && $slug !== $except, ARRAY_FILTER_USE_BOTH);
$resources = $this->publicResources()->whereIn('d.external_id', array_column($identities, 'external_id'))->get()->keyBy('external_id');
$result = [];
foreach ($identities as $slug => $identity) {
$row = $resources->get($identity['external_id']);
if ($row && $row->source_url === $identity['source_url']) {
$result[] = ['slug' => $slug, 'id' => (int) $row->resource_id, 'title' => $row->title, 'href' => '/'.$slug];
}
}
return $result;
}
private function localLinks(string $html): string
{
$public = $this->publicResources()->whereIn('d.external_id', array_column(config('legacy_content.articles', []), 'external_id'))
->get()->keyBy('external_id');
$links = [];
foreach (config('legacy_content.articles', []) as $slug => $identity) {
if ($identity['publishable'] && $public->get($identity['external_id'])?->source_url === $identity['source_url']) {
$links[rtrim($identity['source_url'], '/')] = '/'.$slug;
}
}
$doc = new DOMDocument;
$old = libxml_use_internal_errors(true);
try {
$doc->loadHTML('<?xml encoding="UTF-8"><html><body>'.$html.'</body></html>', LIBXML_NONET);
foreach ($doc->getElementsByTagName('a') as $anchor) {
$href = $anchor->getAttribute('href');
if (isset($links[rtrim($href, '/')])) { $anchor->setAttribute('href', $links[rtrim($href, '/')]); }
}
$body = $doc->getElementsByTagName('body')->item(0);
return $body ? implode('', array_map(fn ($child) => $doc->saveHTML($child), iterator_to_array($body->childNodes))) : '';
} finally { libxml_clear_errors(); libxml_use_internal_errors($old); }
}
/** @param array<string, mixed> $input */
public function attach(User $actor, string $caseId, array $input): void
{
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'resource_id' => 'required|integer|min:1'])->validate();
$article = $this->articleForResource((int) $data['resource_id']);
abort_unless(app(InvestorPath::class)->available(), 404);
DB::transaction(function () use ($actor, $caseId, $data, $article): void {
$desk = app(VirtualDesk::class);
$case = $desk->case($actor, $caseId, true, true);
abort_if($case->closed_at, 409);
abort_unless(DB::table('investor_journeys')->where('id', $case->journey_id)->whereNotNull('path_definition_id')->whereNull('deleted_at')->exists(),
409, 'Este dossier conserva outro roteiro; não foi convertido automaticamente.');
$payload = $desk->decode($case->payload);
foreach ($payload['investor_path']['content_sources'] ?? [] as $existing) {
if ($existing['resource_id'] === $article['id'] && $existing['sha256'] === $article['sha256']) {
return; // Same source/version replay: neither another reference nor another event.
}
}
abort_unless((int) $case->version === $data['version'], 409, 'O dossier foi alterado; recupere a versão atual.');
$payload['investor_path']['content_sources'][] = ['resource_id' => $article['id'], 'document_id' => $article['document_id'],
'sha256' => $article['sha256'], 'metadata_version' => $article['version'], 'title' => $article['title'],
'source_url' => $article['source_url'], 'return_href' => $article['href'], 'actor_id' => $actor->id,
'saved_at' => now()->toIso8601String()];
DB::table('desk_cases')->where('id', $caseId)->update(['payload' => $desk->encode($payload), 'version' => $case->version + 1, 'updated_at' => now()]);
$desk->event($desk->case($actor, $caseId), $actor, 'legacy_content_referenced', ['resource_id' => $article['id'], 'sha256' => $article['sha256']]);
});
}
private function publicResources(): Builder
{
return DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
->where('r.published', true)->where('r.rights_status', 'authorized')->where('d.rights_status', 'authorized')
->where('d.status', 'active')->where('d.data_classification', 'public')->whereNull('d.deleted_at')
->where('d.malware_scan_status', 'clean')->whereColumn('d.malware_scanned_sha256', 'd.sha256')
->select(['d.*', 'r.id as resource_id', 'r.metadata_version', 'r.source_url', 'r.title']);
}
/** Allow presentation markup only. No scripts, styles, events, embeds or remote image execution. */
public function sanitize(string $html): string
{
$doc = new DOMDocument;
$old = libxml_use_internal_errors(true);
try {
$doc->loadHTML('<?xml encoding="UTF-8"><html><body>'.$html.'</body></html>', LIBXML_NONET);
$body = $doc->getElementsByTagName('body')->item(0);
// Preserve the source text and relative sections, but start the displayed
// article below the page h1. Originals and their hashes remain untouched.
$sourceLevels = [];
foreach (['h1', 'h2', 'h3', 'h4'] as $heading) {
if ($doc->getElementsByTagName($heading)->length > 0) {
$sourceLevels[] = (int) substr($heading, 1);
}
}
$headingOffset = 2 - (empty($sourceLevels) ? 2 : min($sourceLevels));
$previousHeading = 1;
$clean = function (DOMNode $node) use (&$clean, $doc, $headingOffset, &$previousHeading): void {
foreach (iterator_to_array($node->childNodes) as $child) {
if (! $child instanceof DOMElement) {
continue;
}
if (preg_match('/^h([1-4])$/i', $child->tagName, $level)) {
$displayLevel = max(2, min(6, (int) $level[1] + $headingOffset, $previousHeading + 1));
$previousHeading = $displayLevel;
$section = $doc->createElement('h'.$displayLevel);
while ($child->firstChild !== null) {
$section->appendChild($child->firstChild);
}
$node->replaceChild($section, $child);
$child = $section;
}
if (! in_array(strtolower($child->tagName), ['p', 'strong', 'b', 'em', 'i', 'ul', 'ol', 'li', 'br', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'a'], true)) {
$node->removeChild($child);
continue;
}
$href = $child->tagName === 'a' ? $child->getAttribute('href') : '';
foreach (iterator_to_array($child->attributes) as $attribute) {
$child->removeAttribute($attribute->nodeName);
}
if ($href !== '') {
if (str_starts_with($href, '/') && ! str_starts_with($href, '//')) {
$href = 'https://investemoz.tempo.co.mz'.$href;
}
if (filter_var($href, FILTER_VALIDATE_URL) && strtolower((string) parse_url($href, PHP_URL_SCHEME)) === 'https') {
$child->setAttribute('href', $href);
$child->setAttribute('rel', 'noopener noreferrer');
}
}
$clean($child);
}
};
if (! $body) {
return '';
}
$clean($body);
return implode('', array_map(fn ($child) => $doc->saveHTML($child), iterator_to_array($body->childNodes)));
} finally {
libxml_clear_errors();
libxml_use_internal_errors($old);
}
}
}
+27 -6
View File
@@ -6,6 +6,7 @@ use App\Models\Opportunity;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Opis\JsonSchema\CompliantValidator;
use Opis\JsonSchema\Errors\ErrorFormatter;
@@ -29,6 +30,7 @@ class OpportunityCanonicalContract
$schema['$defs'][$name.'Create']['allOf'] = $schema['$defs'][$name]['allOf'] ?? [];
}
$schema = app(OpportunityCategoryFields::class)->enrich($schema);
$schema = app(OpportunityModularProfiles::class)->extend($schema);
$schema = $this->writeSchema($schema, $draft);
$properties = &$schema['$defs']['opportunityCreate']['properties'];
unset($properties['mutation_context']); // Actor, audit reason and revision are server-owned.
@@ -240,6 +242,14 @@ class OpportunityCanonicalContract
ksort($payload);
$snapshot = ['adapter_version' => self::VERSION, 'schema_sha256' => hash_file('sha256', resource_path('data/investmoz-opportunity-modular.schema.json')),
'payload' => $payload, 'resources' => $resources];
if (isset(OpportunityModularProfiles::PROFILES[$record->opportunity_category]) || ($payload['module_profile_version'] ?? '') === OpportunityModularProfiles::HISTORY_VERSION) {
$snapshot['qa_extension'] = ['version' => OpportunityModularProfiles::VERSION, 'profile_version' => $payload['module_profile_version'],
'functional_decision' => 'User recommendations accepted for QA, 10/10/2026; no active approval or security-holder nomination',
'gates' => $this->gates($record)];
if ($record->opportunity_category === 'other') {
$snapshot['qa_extension']['subtype'] = config('portal.opportunity_other_qa_subtypes.'.($payload['module_details']['subtype'] ?? ''), []);
}
}
$snapshot['hash'] = hash('sha256', json_encode($snapshot, JSON_THROW_ON_ERROR));
return $snapshot;
@@ -253,9 +263,7 @@ class OpportunityCanonicalContract
private function categoryRules(array $p, Opportunity $record, array $resources, array &$errors): void
{
$category = $p['opportunity_category'];
if (in_array($category, ['concession', 'financing', 'supply', 'other'], true)) {
$errors['canonical.module_profile_version'] = '§25.4: não existe perfil modular aprovado/configurado. O rascunho permanece disponível; não inventar regras.';
}
app(OpportunityModularProfiles::class)->validate($p, $resources, $errors);
foreach (['real_estate', 'land_rights', 'business_transaction'] as $module) {
$applicable = match ($module) {
'real_estate' => ['real_estate'], 'land_rights' => ['land_rights'], default => ['business_sale', 'business_partnership', 'franchise']
@@ -346,6 +354,7 @@ class OpportunityCanonicalContract
$p = $snapshot['payload'];
$errors = [];
app(OpportunityCategoryFields::class)->publication($p, $errors);
app(OpportunityModularProfiles::class)->publication($p, $errors);
foreach ($snapshot['resources']['documents'] ?? [] as $id => $document) {
if (! empty($document['payload']['valid_until']) && $document['payload']['valid_until'] < today()->toDateString()) {
$errors['documents.'.$id] = 'Documento incluído expirou; preserve o original e reveja a versão.';
@@ -400,7 +409,7 @@ class OpportunityCanonicalContract
'business_sale' => ['corporate_screening', 'financial_review', 'legal_tax_due_diligence', 'confidentiality_clearance'],
'business_partnership' => ['corporate_screening', 'financial_review', 'partnership_governance_review', 'legal_tax_due_diligence'],
'franchise' => ['brand_license_review', 'financial_review', 'franchise_legal_review', 'confidentiality_clearance'],
default => [],
default => app(OpportunityModularProfiles::class)->gates($record),
};
}
@@ -411,7 +420,17 @@ class OpportunityCanonicalContract
{
$business = isset($payload['business_transaction']) && ($payload['visibility'] ?? '') === 'public';
return ['title' => (string) ($payload['official_title'] ?? ''), 'summary' => (string) ($business ? ($payload['business_transaction']['teaser'] ?? '') : ($payload['executive_summary'] ?? '')),
$label = match ($payload['module_details']['mode'] ?? '') {
'intention_call' => 'Intenção/chamada em preparação — não é direito atribuído. ',
'awarded_right' => 'Concessão com ato competente documentado. ',
'seeking_funding' => 'Procura de financiamento. ', 'funding_offer' => 'Oferta de financiamento — sujeita a análise do beneficiário. ',
'purchase_request' => 'Pedido de compra. ', 'supply_offer' => 'Oferta de fornecimento. ', default => '',
};
if (($payload['module_details']['conditions_status'] ?? '') === 'proposed') {
$label .= 'Valores e condições propostos, sem compromisso assumido. ';
}
return ['title' => (string) ($payload['official_title'] ?? ''), 'summary' => Str::limit($label.(string) ($business ? ($payload['business_transaction']['teaser'] ?? '') : ($payload['executive_summary'] ?? '')), 499, '…'),
'description' => (string) ($business ? ($payload['business_transaction']['teaser'] ?? '') : ($payload['demand_statement'] ?? ''))];
}
@@ -427,7 +446,9 @@ class OpportunityCanonicalContract
->orWhereExists(fn ($i) => $i->selectRaw('1')->from('investor_contacts as ic')->join('investors as inv', 'inv.id', '=', 'ic.investor_id')->whereColumn('ic.contact_id', 'contacts.id')->whereIn('inv.organization_id', $orgs)->whereNull('inv.deleted_at'));
})->get(['id', 'full_name']);
return ['version' => self::VERSION, 'schema' => $this->schema(true)['$defs']['opportunityCreate'],
return ['version' => self::VERSION, 'modularProfiles' => app(OpportunityModularProfiles::class)->enabled() ? OpportunityModularProfiles::PROFILES : [],
'historyVersion' => app(OpportunityModularProfiles::class)->enabled() ? OpportunityModularProfiles::HISTORY_VERSION : null,
'schema' => $this->schema(true)['$defs']['opportunityCreate'],
'definitions' => $this->schema(true)['$defs'],
'activities' => DB::table('economic_activities')->where('status', 'published')->get(['id', 'parent_id', 'sector_id', 'label_pt_mz', 'catalog_version_id']),
'contacts' => $contacts, 'countries' => app(CountryCatalogue::class)->all(),
@@ -0,0 +1,70 @@
<?php
namespace App\Services;
use App\Models\Opportunity;
use App\Models\User;
/** Read adapter only: preserve persisted identifiers and make unresolved metadata explicit. */
class OpportunityCanonicalResponse
{
public const VERSION = 'opportunity-response-existing-ids-20261010-1';
/** @return array<string,mixed> */
public function read(User $actor, Opportunity $record): array
{
$access = app(PortalProfileAccess::class);
$access->authorize($actor, 'view', $record);
$payload = app(OpportunityManagement::class)->values($record)['canonical'] ?? [];
$context = $record->workflow_context ?? [];
$data = [...$payload, 'id' => $record->id, 'opportunity_code' => $record->reference,
'opportunity_category' => $record->opportunity_category, 'status' => $access->state($record),
'version' => (int) $record->management_version, 'is_deleted' => $record->trashed(),
'deleted_at' => $record->deleted_at?->toIso8601String()];
foreach (['submitted' => 'submission', 'approved' => 'approval', 'published' => 'publication'] as $prefix => $key) {
$data[$prefix.'_by'] = $context[$key]['actor_id'] ?? null;
$data[$prefix.'_at'] = $context[$key]['at'] ?? null;
}
$references = [];
foreach (OpportunityResources::KINDS as $kind) {
$references[$kind] = $payload[$kind] ?? [];
$rows = [];
if (app(OpportunityResources::class)->installed()) {
foreach (app(OpportunityResources::class)->listing($record->id, $kind) as $row) {
$rows[$row->id] = $row;
}
}
$data[$kind] = array_map(function (string $id) use ($rows, $kind): array {
$row = $rows[$id] ?? null;
if ($row === null) {
return ['id' => $id, 'availability' => 'unavailable'];
}
$value = [...$row->payload, 'id' => $row->id, 'version' => (int) $row->version,
'is_deleted' => $row->deleted_at !== null, 'deleted_at' => $row->deleted_at,
'created_by' => $row->created_by, 'created_at' => $row->created_at, 'updated_at' => $row->updated_at];
if ($kind !== 'locations') {
$value['original_name'] = $row->file?->file_name;
$value['mime_type'] = $row->file?->media_type;
$value['sha256'] = $row->file?->sha256;
$value['scan_recorded_status'] = $row->file?->malware_scan_status;
}
return $value;
}, $references[$kind]);
}
return ['adapter_version' => self::VERSION,
'source_schema_sha256' => hash_file('sha256', resource_path('data/investmoz-opportunity-modular.schema.json')),
'data' => $data, 'resource_references' => $references,
'validation_result' => ['scope' => 'recorded_submission_only', 'submission_hash' => $context['submission']['hash'] ?? null,
'submitted_content_revision' => $context['submission']['content_revision'] ?? null,
'current_content_revision' => $context['content_revision'] ?? 1,
'revalidated_on_read' => false, 'external_decision' => 'not_inferred'],
'publication' => $record->publicationPresentation(),
'compatibility' => ['normative_response_complete' => false, 'opportunity_actor_sector_ids' => 'existing bigint, not converted to UUID',
'opportunity_code' => 'existing reference preserved; no INV-MZ code invented',
'states' => 'existing workflow values preserved; no implicit stage equivalence',
'inline_modules' => 'management_draft canonical JSON with audited revisions; no parallel specialized tables',
'metadata_gaps' => ['completion_percentage algorithm', 'inline item IDs and mutation metadata', 'resource updated_by/deleted_by recovery']]];
}
}
+20 -1
View File
@@ -194,6 +194,15 @@ class OpportunityCategoryFields
}
if (isset($p['land_rights'])) {
$lr = $p['land_rights'];
// §27: UNIQUE (opportunity_id, cadastral_number); absent numbers remain unknown.
$cadastralNumbers = [];
foreach ($lr['parcels'] ?? [] as $i => $parcel) {
$number = $parcel['cadastral_number'] ?? null;
if ($number !== null && $number !== '') {
$fail(! in_array($number, $cadastralNumbers, true), "land_rights.parcels.$i.cadastral_number", 'Número cadastral repetido nesta oportunidade; recupere e reconcilie a parcela existente.');
$cadastralNumbers[] = $number;
}
}
if (in_array($lr['duat_status'], ['provisional', 'definitive'], true)) {
$fail(! empty($lr['issuing_authority_id']), 'land_rights.issuing_authority_id', 'DUAT provisório/definitivo exige entidade emissora.');
if (! empty($lr['duat_number']) && ! empty($lr['issuing_authority_id'])) {
@@ -220,10 +229,20 @@ class OpportunityCategoryFields
$bt = $p['business_transaction'];
$type = $bt['transaction_type'];
$allowed = match ($p['opportunity_category']) {
'business_sale' => ['full_sale', 'partial_sale'],'business_partnership' => ['partnership', 'joint_venture'],'franchise' => ['franchise'],default => []
'business_sale' => ['full_sale', 'partial_sale', 'succession'],'business_partnership' => ['partnership', 'joint_venture'],'franchise' => ['franchise'],default => []
};
$fail(in_array($type, $allowed, true), 'business_transaction.transaction_type', 'Modalidade incompatível com esta categoria.');
$years = array_column($bt['financial_periods'], 'year');
if (($bt['history_status'] ?? '') === 'no_completed_years') {
$fail(app(OpportunityModularProfiles::class)->enabled() && ($p['module_profile_version'] ?? '') === OpportunityModularProfiles::HISTORY_VERSION,
'module_profile_version', 'Ausência de histórico requer a extensão QA v2 explícita; preserve o contrato original.');
$fail($bt['available_financial_years'] === 0 && $bt['financial_periods'] === [], 'business_transaction.financial_periods', 'Não misture projeções com exercícios financeiros realizados.');
foreach (['history_declaration', 'history_declaration_document_id', 'projections'] as $field) {
$fail(! empty($bt[$field]), 'business_transaction.'.$field, 'Entidade sem exercícios completos requer declaração e projeções identificadas.');
}
} else {
$fail($bt['available_financial_years'] >= 1 && $bt['financial_periods'] !== [], 'business_transaction.financial_periods', 'Use os exercícios existentes ou a declaração versionada de ausência de histórico.');
}
$fail(count($years) >= min(3, $bt['available_financial_years']) && count($years) === count(array_unique($years)), 'business_transaction.financial_periods', 'Inclua pelo menos três exercícios quando existentes, sem repetir anos.');
foreach ($years as $i => $year) {
$fail($year <= (int) today()->format('Y'), "business_transaction.financial_periods.$i.year", 'Não apresente um exercício futuro como demonstração realizada.');
+247
View File
@@ -0,0 +1,247 @@
<?php
namespace App\Services;
use App\Models\Opportunity;
use Illuminate\Support\Facades\DB;
/** Explicit QA-only extension of the accepted contract; existing draft/resources are the storage. */
class OpportunityModularProfiles
{
public const VERSION = 'modular-qa-20261010-v1';
public const HISTORY_VERSION = 'business-history-qa-20261010-v2';
public const PROFILES = [
'concession' => 'concession-qa-20261010-v1',
'financing' => 'financing-qa-20261010-v1',
'supply' => 'supply-qa-20261010-v1',
'other' => 'other-qa-20261010-v1',
];
public function enabled(): bool
{
if (! config('portal.opportunity_modular_profiles_qa_enabled') || ! app()->environment('testing')) {
return false;
}
$connection = DB::connection();
if ($connection->getDriverName() !== 'pgsql' || (string) $connection->getConfig('port') !== '55441'
|| ! in_array($connection->getConfig('host'), ['127.0.0.1', 'localhost'], true)
|| $connection->getConfig('url') || ! str_ends_with($connection->getDatabaseName(), '_test')) {
return false;
}
$actual = $connection->selectOne('SELECT current_database() AS database, inet_server_port() AS port');
return $actual->database === $connection->getDatabaseName() && (int) $actual->port === 55441;
}
/** @param array<string,mixed> $schema
* @return array<string,mixed> */
public function extend(array $schema): array
{
if (! $this->enabled()) {
return $schema;
}
$text = ['type' => 'string', 'minLength' => 1, 'maxLength' => 2000];
$doc = ['type' => 'string', 'format' => 'uuid'];
$positive = ['type' => 'number', 'exclusiveMinimum' => 0];
$date = ['type' => 'string', 'format' => 'date'];
$properties = [
'mode' => ['enum' => ['intention_call', 'awarded_right', 'seeking_funding', 'funding_offer', 'purchase_request', 'supply_offer', 'classified_other']],
'conditions_status' => ['enum' => ['proposed', 'binding']],
'object' => $text, 'representation_document_id' => $doc, 'object_document_id' => $doc,
'terms' => $text, 'source' => $text, 'scenario' => $text, 'terms_date' => $date,
'commitment_document_id' => $doc, 'disclosure_authorization_document_id' => $doc,
'granting_entity' => $text, 'procedure_regime' => $text, 'scope' => $text,
'term_value' => $positive, 'term_unit' => ['enum' => ['days', 'months', 'years']],
'remuneration_obligations' => $text, 'participation_conditions' => $text,
'external_decision_document_id' => $doc, 'territorial_right_document_id' => $doc,
'land_required' => ['type' => 'boolean'], 'construction_required' => ['type' => 'boolean'],
'applicable_authorizations' => $text, 'construction_license_document_id' => $doc,
'start_date' => $date, 'end_date' => $date,
'instrument' => ['enum' => ['debt', 'equity', 'guarantee']],
'use_of_funds' => $text, 'disbursement_calendar' => $text, 'repayment_periodicity' => $text,
'financial_plan_document_id' => $doc, 'secured_evidence_document_id' => $doc,
'offered_capacity' => $positive, 'eligibility_criteria' => $text, 'capacity_document_id' => $doc,
'equity_percentage' => ['type' => 'number', 'exclusiveMinimum' => 0, 'maximum' => 100],
'proposed_rights' => $text, 'quantity' => $positive, 'unit' => $text, 'destination' => $text,
'acceptance_criteria' => $text, 'quotation_method' => $text, 'unit_price' => $positive, 'total_price' => $positive,
'declared_taxes' => $text, 'advance_or_guarantee' => ['type' => 'boolean'],
'subtype' => $text, 'purpose' => $text, 'recipients' => $text, 'expected_output' => $text, 'risks' => $text,
];
$schema['$defs']['opportunityCreate']['properties']['module_details'] = [
'type' => 'object', 'properties' => $properties, 'additionalProperties' => false,
];
// Opt-in v2: a declaration/projection is not a fabricated completed financial year.
$business = &$schema['$defs']['businessTransactionCreate'];
$business['properties']['available_financial_years']['minimum'] = 0;
$business['properties']['financial_periods']['minItems'] = 0;
$business['properties'] += ['history_status' => ['enum' => ['existing_accounts', 'no_completed_years']],
'history_declaration' => $text, 'history_declaration_document_id' => $doc,
'projections' => ['type' => 'object', 'properties' => ['scenario' => $text, 'source' => $text, 'date' => $date, 'document_id' => $doc],
'required' => ['scenario', 'source', 'date', 'document_id'], 'additionalProperties' => false]];
$business['allOf'][] = ['if' => ['properties' => ['history_status' => ['const' => 'no_completed_years']], 'required' => ['history_status']],
'else' => ['properties' => ['financial_periods' => ['minItems' => 1], 'available_financial_years' => ['minimum' => 1]]]];
return $schema;
}
/** @param array<string,mixed> $p
* @param array<string,mixed> $resources
* @param array<string,string> $errors */
public function validate(array $p, array $resources, array &$errors): void
{
$category = $p['opportunity_category'] ?? '';
if (! isset(self::PROFILES[$category])) {
if (isset($p['module_details'])) {
$errors['canonical.module_details'] = 'Detalhe modular incompatível com esta categoria.';
}
return;
}
if (! $this->enabled() || ($p['module_profile_version'] ?? '') !== self::PROFILES[$category]) {
$errors['canonical.module_profile_version'] = 'Use a versão QA explicitamente aprovada; versões anteriores permanecem rascunhos recuperáveis.';
return;
}
$m = $p['module_details'] ?? [];
$fail = function (bool $ok, string $path, string $message) use (&$errors): void {
if (! $ok) {
$errors['canonical.'.$path] = $message;
}
};
$require = function (array $fields) use ($m, $fail): void {
foreach ($fields as $field) {
$fail(isset($m[$field]) && $m[$field] !== '', 'module_details.'.$field, 'Preencha este campo aplicável antes de submeter.');
}
};
$require(['mode', 'object', 'representation_document_id', 'object_document_id']);
$modes = match ($category) {
'concession' => ['intention_call', 'awarded_right'], 'financing' => ['seeking_funding', 'funding_offer'],
'supply' => ['purchase_request', 'supply_offer'], default => ['classified_other'],
};
$fail(in_array($m['mode'] ?? '', $modes, true), 'module_details.mode', 'Selecione procura/oferta ou modalidade compatível com esta categoria.');
// Only references included in the current opportunity's verified resource snapshot are usable.
foreach ($m as $key => $value) {
if (str_ends_with($key, '_document_id') && $value !== null && $value !== '') {
$fail(isset($resources['documents'][$value]), 'module_details.'.$key, 'Associe documento ativo desta oportunidade incluído no contrato.');
}
}
if (! empty($m['start_date']) && ! empty($m['end_date'])) {
$fail($m['end_date'] >= $m['start_date'], 'module_details.end_date', 'O fim não pode anteceder o início.');
}
if ($category === 'concession') {
$require(['granting_entity', 'procedure_regime', 'scope', 'term_value', 'term_unit', 'remuneration_obligations', 'participation_conditions', 'land_required', 'construction_required']);
$fail(($m['term_value'] ?? 0) > 0, 'module_details.term_value', 'Declare duração positiva.');
if (($m['mode'] ?? '') === 'awarded_right') {
$require(['external_decision_document_id']);
}
if ($m['land_required'] ?? false) {
$require(['applicable_authorizations']);
if (($m['mode'] ?? '') === 'awarded_right') {
$require(['territorial_right_document_id']);
}
}
if ($m['construction_required'] ?? false) {
$require(['applicable_authorizations']);
}
}
if (in_array($category, ['financing', 'supply'], true)) {
$require(['conditions_status', 'terms']);
$fail(! empty($p['currency']), 'currency', 'Declare moeda sem converter valores implicitamente.');
if (($m['conditions_status'] ?? '') === 'proposed') {
$require(['scenario', 'source', 'terms_date']);
$fail(empty($m['terms_date']) || $m['terms_date'] <= today()->toDateString(), 'module_details.terms_date', 'A data da fonte não pode ser futura.');
} elseif (($m['conditions_status'] ?? '') === 'binding') {
$require(['commitment_document_id']);
}
}
if ($category === 'financing') {
$require(['instrument', 'disbursement_calendar']);
if (($m['mode'] ?? '') === 'seeking_funding') {
$require(['use_of_funds', 'financial_plan_document_id']);
foreach (['total_investment', 'investment_breakdown', 'secured_financing', 'funding_sought'] as $field) {
$fail(array_key_exists($field, $p), $field, 'Complete o cenário do projeto para a procura de financiamento.');
}
$total = $p['total_investment'] ?? 0;
$secured = array_sum(array_column($p['secured_financing'] ?? [], 'amount'));
$sum = array_sum(array_column($p['investment_breakdown'] ?? [], 'amount'));
$fail($total > 0 && abs($sum - $total) <= 0.010001, 'investment_breakdown', 'Rubricas devem reconciliar com o investimento total positivo.');
$fail($secured <= $total, 'secured_financing', 'Financiamento assegurado não pode exceder o investimento.');
$sought = $p['funding_sought'] ?? 0;
$fail($sought > 0 && $sought <= $total - $secured, 'funding_sought', 'Pedido positivo não pode exceder a necessidade remanescente do projeto.');
if ($secured > 0) {
$require(['secured_evidence_document_id']);
}
foreach (['investment_breakdown', 'secured_financing'] as $field) {
foreach ($p[$field] ?? [] as $i => $row) {
$fail(($row['currency'] ?? '') === ($p['currency'] ?? ''), "$field.$i.currency", 'Moedas diferentes exigem cenário explícito; não somar automaticamente.');
}
}
} elseif (($m['mode'] ?? '') === 'funding_offer') {
$require(['offered_capacity', 'eligibility_criteria', 'capacity_document_id']);
$fail(($m['offered_capacity'] ?? 0) > 0, 'module_details.offered_capacity', 'Declare capacidade oferecida positiva e documentada, sem aprovação automática de beneficiários.');
foreach (['total_investment', 'investment_breakdown', 'secured_financing', 'funding_sought'] as $field) {
$fail(! isset($p[$field]) || $p[$field] === [], $field, 'Oferta usa capacidade e critérios do financiador; preserve o cenário do projeto num percurso de procura.');
}
}
if (($m['instrument'] ?? '') === 'debt') {
$require(['term_value', 'term_unit', 'repayment_periodicity']);
$fail(($m['term_value'] ?? 0) > 0, 'module_details.term_value', 'Dívida exige prazo positivo.');
} elseif (($m['instrument'] ?? '') === 'equity') {
$require(['equity_percentage', 'proposed_rights']);
$fail(($m['equity_percentage'] ?? 0) > 0 && ($m['equity_percentage'] ?? 0) <= 100, 'module_details.equity_percentage', 'Percentagem deve estar entre zero exclusivo e 100.');
}
}
if ($category === 'supply') {
$require(['quantity', 'unit', 'destination', 'start_date', 'end_date', 'acceptance_criteria', 'advance_or_guarantee']);
$fail(($m['quantity'] ?? 0) > 0, 'module_details.quantity', 'Declare quantidade positiva.');
$fail(! empty($m['total_price']) || ! empty($m['quotation_method']), 'module_details.quotation_method', 'Declare preço total ou método de cotação.');
if (isset($m['unit_price'], $m['total_price'], $m['quantity'])) {
$fail(abs($m['quantity'] * $m['unit_price'] - $m['total_price']) <= 0.010001, 'module_details.total_price', 'Reconcilie quantidade × preço unitário, separando impostos declarados.');
}
if (($m['mode'] ?? '') === 'supply_offer') {
$require(['capacity_document_id']);
}
}
if ($category === 'other') {
$require(['subtype', 'purpose', 'recipients', 'expected_output', 'risks']);
$subtype = config('portal.opportunity_other_qa_subtypes.'.($m['subtype'] ?? ''));
$fail(is_array($subtype) && ($subtype['approved'] ?? false) && ! empty($subtype['version'])
&& ! empty($subtype['gates']) && ! empty($subtype['decision_reference']), 'module_details.subtype', 'Subtipo ainda não aprovado: guarde e desenvolva a ideia; a submissão aguarda classificação funcional.');
}
}
/** @param array<string,mixed> $p
* @param array<string,string> $errors */
public function publication(array $p, array &$errors): void
{
$m = $p['module_details'] ?? [];
if (($p['opportunity_category'] ?? '') === 'concession') {
if (($m['mode'] ?? '') === 'intention_call' && empty($m['disclosure_authorization_document_id'])) {
$errors['module_details.disclosure_authorization_document_id'] = 'Chamada/intenção exige autorização de divulgação; não representa direito atribuído.';
}
if (($m['mode'] ?? '') === 'awarded_right' && empty($m['external_decision_document_id'])) {
$errors['module_details.external_decision_document_id'] = 'Direito atribuído exige o ato competente; aprovação interna não o substitui.';
}
if (($m['construction_required'] ?? false) && ($m['mode'] ?? '') === 'awarded_right' && empty($m['construction_license_document_id'])) {
$errors['module_details.construction_license_document_id'] = 'Construção aplicável exige licença antes da disponibilização como direito atribuído.';
}
}
}
/** @return list<string> */
public function gates(Opportunity $record): array
{
$p = app(OpportunityManagement::class)->values($record)['canonical'] ?? [];
$m = $p['module_details'] ?? [];
return match ($record->opportunity_category) {
'concession' => ['technical_review', 'legal_territorial_review'],
'financing' => ['technical_review', 'financial_review', 'legal_tax_due_diligence'],
'supply' => ['technical_review', 'legal_territorial_review', ...(($m['advance_or_guarantee'] ?? false) ? ['financial_review'] : [])],
'other' => config('portal.opportunity_other_qa_subtypes.'.($m['subtype'] ?? '').'.gates', []),
default => [],
};
}
}
+4
View File
@@ -221,6 +221,10 @@ class OpportunityResources
}
$data = Validator::make($payload, $rules)->validate();
if ($kind === 'locations') {
if (! empty($data['country_code']) && $data['country_code'] !== 'MZ'
&& array_filter(array_intersect_key($data, array_flip(['province_id', 'district_id', 'admin_post_id', 'locality_id'])))) {
throw ValidationException::withMessages(['country_code' => 'Os territórios desta hierarquia pertencem a Moçambique. Escolha Moçambique ou retire a seleção territorial para guardar uma localização noutro país.']);
}
app(InvestmentGeography::class)->selection($data, true);
if (! empty($data['geometry'])) {
$geometry = $data['geometry'];
+7 -1
View File
@@ -22,7 +22,7 @@ class PortalProfileAccess
app_path('Services/PortalProfileAdministration.php'), app_path('Services/PortalSuperusers.php'),
app_path('Services/OpportunityProfileWorkflow.php'), app_path('Http/Controllers/PortalProfileController.php'), app_path('Http/Controllers/DashboardController.php'),
app_path('Http/Controllers/OpportunityManagementController.php'),
app_path('Services/OpportunityCanonicalContract.php'), app_path('Services/OpportunityCategoryFields.php'), app_path('Services/OpportunityWorkflowTasks.php'), base_path('composer.json'), base_path('composer.lock'),
app_path('Services/OpportunityCanonicalContract.php'), app_path('Services/OpportunityCanonicalResponse.php'), app_path('Services/OpportunityCategoryFields.php'), app_path('Services/OpportunityModularProfiles.php'), app_path('Services/OpportunityWorkflowTasks.php'), base_path('composer.json'), base_path('composer.lock'),
resource_path('data/investmoz-opportunity-modular.schema.json'), resource_path('js/components/opportunity-canonical-fields.tsx'), resource_path('js/components/opportunity-parcel-geometry.tsx'),
app_path('Http/Controllers/OpportunityResourceController.php'),
app_path('Http/Controllers/InvestmentTerritoryController.php'),
@@ -32,8 +32,14 @@ class PortalProfileAccess
database_path('migrations/2026_10_10_040000_create_portal_profile_authorization.php'), base_path('bootstrap/app.php'),
resource_path('js/pages/profiles/request.tsx'), resource_path('js/pages/profiles/administration.tsx'), resource_path('js/pages/profiles/audit.tsx'),
resource_path('js/components/profile-navigation.tsx'), resource_path('js/components/opportunity-profile-actions.tsx'),
resource_path('js/components/portal-grouped-menu.tsx'), resource_path('js/components/user-menu-content.tsx'),
resource_path('js/pages/management/opportunities.tsx'), resource_path('js/pages/management/opportunity-draft.tsx'),
resource_path('js/types/profile-access.ts'), resource_path('js/lib/profile-json.ts'), resource_path('js/pages/management/shared.tsx'),
resource_path('js/pages/dashboard.tsx'), resource_path('js/pages/PortalInvestidor.tsx'),
resource_path('js/layouts/portal-navigation.tsx'), resource_path('js/components/interactive-investment-map.tsx'),
resource_path('js/components/simulation-form.tsx'), resource_path('js/lib/simulation-form.ts'), app_path('Services/LegacyContent.php'),
resource_path('js/components/ui/sidebar.tsx'), resource_path('js/components/simulation-financial-tools.tsx'), resource_path('js/components/simulation-cashflow-planner.tsx'), resource_path('js/pages/investor-path/show.tsx'), resource_path('views/app.blade.php'),
resource_path('js/pages/management/opportunity-resources.tsx'),
app_path('Services/PortalProfileInstallation.php'), app_path('Http/Controllers/PortalProfileInstallationController.php'), config_path('portal_profiles.php'), resource_path('js/pages/profiles/installation.tsx')];
}
+41 -1
View File
@@ -204,6 +204,30 @@ class VirtualDesk
});
}
/** @return array<string,mixed> */
public function requestToolPrefill(User $actor, \stdClass $request): ?array
{
$case = $this->case($actor, $request->case_id);
$payload = $this->decode($request->payload);
if (! config('investor-path.enabled') || (int) $case->user_id !== $actor->id || ! $case->simulation_id || $case->closed_at
|| ! in_array($request->status, ['draft', 'returned'], true) || trim((string) ($payload['description'] ?? '')) !== ''
|| ($request->status === 'returned' && ! in_array('description', json_decode($request->editable_fields ?? '[]', true), true))
|| ! isset($this->definition($request)['fields']['description'])) {
return null;
}
$draftRequest = Request::create('/');
$draftRequest->setUserResolver(fn () => $actor);
$draft = app(SimulationFormDrafts::class)->find($case->simulation_id, $draftRequest);
$economic = SimulationFormDraft::decryptSnapshot($draft->economic_snapshot);
$description = (string) ($economic['project_summary'] ?? '');
if (trim($description) === '') {
return null;
}
return ['description' => $description, 'draft_id' => $draft->id, 'draft_version' => $draft->version_no,
'snapshot_sha256' => $draft->snapshot_sha256];
}
/** @return array<string,mixed> */
public function definition(\stdClass $request): array
{
@@ -277,7 +301,9 @@ class VirtualDesk
/** @param array<string,mixed> $input */
public function save(User $actor, string $id, array $input, bool $submit = false): \stdClass
{
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'payload' => 'required|array', 'consent' => $submit ? 'accepted' : 'nullable|boolean', 'reason' => 'nullable|string|max:3000'])->validate();
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'payload' => 'required|array', 'consent' => $submit ? 'accepted' : 'nullable|boolean', 'reason' => 'nullable|string|max:3000',
'tool_prefill' => 'nullable|array:draft_id,draft_version,snapshot_sha256', 'tool_prefill.draft_id' => 'required_with:tool_prefill|uuid',
'tool_prefill.draft_version' => 'required_with:tool_prefill|integer|min:1', 'tool_prefill.snapshot_sha256' => 'required_with:tool_prefill|string|size:64'])->validate();
return DB::transaction(function () use ($actor, $id, $data, $submit) {
$request = $this->request($actor, $id, true, true);
@@ -285,6 +311,16 @@ class VirtualDesk
return $request;
}
abort_unless(in_array($request->status, ['draft', 'returned'], true) && $request->version === (int) $data['version'], 409, 'Pedido alterado ou já submetido.');
$prefill = null;
if (! empty($data['tool_prefill'])) {
$prefill = $this->requestToolPrefill($actor, $request);
$source = $data['tool_prefill'];
if ($submit || ! $prefill || $source['draft_id'] !== $prefill['draft_id'] || (int) $source['draft_version'] !== $prefill['draft_version']
|| ! hash_equals($prefill['snapshot_sha256'], $source['snapshot_sha256'])
|| ($data['payload']['description'] ?? null) !== $prefill['description']) {
throw ValidationException::withMessages(['payload.description' => 'Recupere a descrição atual deste dossier. A reutilização só preenche um campo vazio num rascunho e não submete o pedido.']);
}
}
$payload = $this->validateFields($data['payload'], $this->definition($request), $submit);
if ($request->status === 'returned') {
$editable = json_decode($request->editable_fields ?? '[]', true);
@@ -310,6 +346,10 @@ class VirtualDesk
DB::table('desk_requests')->where('id', $id)->update($updates);
$request = DB::table('desk_requests')->where('id', $id)->firstOrFail();
$this->event($case, $actor, $submit ? 'submitted' : 'draft_saved', ['reason' => $data['reason'] ?? '', 'findings' => $findings], $request);
if ($prefill) {
$this->event($case, $actor, 'request_prefill_reused', ['draft_id' => $prefill['draft_id'], 'draft_version' => $prefill['draft_version'],
'snapshot_sha256' => $prefill['snapshot_sha256'], 'fields' => ['description'], 'scope' => 'draft_preparation'], $request);
}
return $request;
});
+32
View File
@@ -0,0 +1,32 @@
<?php
// Route/identity mapping over the existing library. Bodies and originals remain in documents.
return [
'groups' => [
'agronegocio' => ['root' => 'agronegocio', 'label' => 'Agronegócio', 'heading' => 'Investir no agronegócio'],
'energia-mineracao' => ['root' => 'energia-mineracao', 'label' => 'Energia e Mineração', 'heading' => 'Explorar Energia e Mineração'],
'turismo' => ['root' => 'turismo', 'label' => 'Turismo', 'heading' => 'Explorar Turismo'],
'infraestruturas' => ['root' => 'infraestruturas', 'label' => 'Infraestruturas', 'heading' => 'Explorar Infraestruturas'],
'mercado-financeiro' => ['root' => 'mercado-financeiro', 'label' => 'Mercado Financeiro', 'heading' => 'Explorar Mercado Financeiro'],
],
'articles' => [
'agronegocio' => ['external_id' => 'legacy-tempo-post-171985-20260420', 'source_url' => 'https://investemoz.tempo.co.mz/agronegocio', 'group' => 'agronegocio', 'sector_slug' => 'agronegocio', 'publishable' => true],
'energia-mineracao' => ['external_id' => 'legacy-tempo-post-171981-20260430', 'source_url' => 'https://investemoz.tempo.co.mz/energia-mineracao', 'group' => 'energia-mineracao', 'sector_slug' => 'energia', 'publishable' => true],
'energia' => ['external_id' => 'legacy-tempo-post-172679-20260430', 'source_url' => 'https://investemoz.tempo.co.mz/energia/', 'group' => 'energia-mineracao', 'sector_slug' => 'energia-especifica', 'publishable' => true],
'mineracao' => ['external_id' => 'legacy-tempo-post-172674-20260430', 'source_url' => 'https://investemoz.tempo.co.mz/mineracao', 'group' => 'energia-mineracao', 'sector_slug' => 'mineracao', 'publishable' => true],
'turismo' => ['external_id' => 'legacy-tempo-post-171990-20260616', 'source_url' => 'https://investemoz.tempo.co.mz/turismo', 'group' => 'turismo', 'sector_slug' => 'turismo', 'publishable' => true],
'provincia-da-zambezia-onde-os-corredores-de-mocambique-convergem-com-a-natureza-a-cultura-e-o-turismo-sustentavel' => ['external_id' => 'legacy-tempo-post-172263-20260414', 'source_url' => 'https://investemoz.tempo.co.mz/provincia-da-zambezia-onde-os-corredores-de-mocambique-convergem-com-a-natureza-a-cultura-e-o-turismo-sustentavel', 'group' => 'turismo', 'sector_slug' => 'turismo', 'publishable' => true],
'provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem' => ['external_id' => 'legacy-tempo-post-172256-20260414', 'source_url' => 'https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem/', 'group' => 'turismo', 'sector_slug' => 'turismo', 'publishable' => true],
'provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo' => ['external_id' => 'legacy-tempo-post-172261-20260414', 'source_url' => 'https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo/', 'group' => 'turismo', 'sector_slug' => 'turismo', 'publishable' => true],
'provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento' => ['external_id' => 'legacy-tempo-post-172258-20260417', 'source_url' => 'https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento/', 'group' => 'turismo', 'sector_slug' => 'turismo', 'publishable' => true],
'provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico' => ['external_id' => 'legacy-tempo-post-172253-20260414', 'source_url' => 'https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico/', 'group' => 'turismo', 'sector_slug' => 'turismo', 'publishable' => true],
'maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel' => ['external_id' => 'legacy-tempo-post-172240-20260414', 'source_url' => 'https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel/', 'group' => 'turismo', 'sector_slug' => 'turismo', 'publishable' => true],
'provincia-de-maputo-turismo' => ['external_id' => 'legacy-tempo-post-172237-20260417', 'source_url' => 'https://investemoz.tempo.co.mz/provincia-de-maputo-turismo/', 'group' => 'turismo', 'sector_slug' => 'turismo', 'publishable' => true],
'provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique' => ['external_id' => 'legacy-tempo-post-172233-20260417', 'source_url' => 'https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique/', 'group' => 'turismo', 'sector_slug' => 'turismo', 'publishable' => true],
'provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo' => ['external_id' => 'legacy-tempo-post-172230-20260414', 'source_url' => 'https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo/', 'group' => 'turismo', 'sector_slug' => 'turismo', 'publishable' => true],
'cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique' => ['external_id' => 'legacy-tempo-post-172223-20260414', 'source_url' => 'https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique/', 'group' => 'turismo', 'sector_slug' => 'turismo', 'publishable' => true],
'niassa-o-ultimo-grande-destino-virgem-do-turismo-africano' => ['external_id' => 'legacy-tempo-post-172219-20260414', 'source_url' => 'https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano/', 'group' => 'turismo', 'sector_slug' => 'turismo', 'publishable' => true],
'infraestruturas' => ['external_id' => 'legacy-tempo-post-171992-20260430', 'source_url' => 'https://investemoz.tempo.co.mz/infraestruturas', 'group' => 'infraestruturas', 'sector_slug' => 'infraestruturas', 'publishable' => true],
'mercado-financeiro' => ['external_id' => 'legacy-tempo-post-171994-20260417', 'source_url' => 'https://investemoz.tempo.co.mz/mercado-financeiro', 'group' => 'mercado-financeiro', 'sector_slug' => 'mercado-financeiro', 'publishable' => false],
],
];
+4
View File
@@ -1,10 +1,14 @@
<?php
return [
'legacy_content_qa_enabled' => env('PORTAL_LEGACY_CONTENT_QA_ENABLED', false),
'profile_access_enabled' => (bool) env('PORTAL_PROFILE_ACCESS_ENABLED', false),
// Prepared increment; enable only in QA until the authenticated rollout is approved.
'opportunity_dashboard_access_enabled' => env('PORTAL_OPPORTUNITY_DASHBOARD_ACCESS_ENABLED', false),
'opportunity_specialized_drafts_enabled' => env('PORTAL_OPPORTUNITY_SPECIALIZED_DRAFTS_ENABLED', false),
// Accepted first QA version, additionally guarded by the actual isolated database identity.
'opportunity_modular_profiles_qa_enabled' => env('PORTAL_OPPORTUNITY_MODULAR_PROFILES_QA_ENABLED', false),
'opportunity_other_qa_subtypes' => [],
'operator_name' => env('PORTAL_OPERATOR_NAME'),
'support' => [
'email' => env('PORTAL_SUPPORT_EMAIL'),
File diff suppressed because one or more lines are too long
+168 -2
View File
@@ -1,9 +1,175 @@
<!-- qa-territory-resource-c86-c87-20261010:begin -->
## Lote territorial C86/C87 — 2026-10-10T20:48:06.634373+02:00
**C37 encerrado somente para autenticação QA18, por confirmação manual expressa. Não foi repetido.** Aceitação ligada aos recibos de logout/recuperação em `c37-manual-close-20261010/acceptance.json`. A/B, auditoria integral e ativação continuam independentes.
| Requisito/caso | Correção e prova QA | Limite do fecho |
| --- | --- | --- |
| OP02; GEO01/02; C86 | API antes aceitava PT com província MZ por200; agora422 junto a country_code, preservando versão/payload/auditoria. UI limpa somente a seleção administrativa no preenchimento ao mudar país; estrangeiro incompleto pode guardar e recuperar. Conferência deixou de exigir província/distrito MZ a Portugal. | Aplicado a recursos de localização; não encerra origem, filtros ou estatísticas das solicitações. Registos anteriores não foram migrados/corrigidos silenciosamente. |
| GEO01/02/03 | QA21 oportunidade51: MZ→CaboDelgado→ANCUABE→guardar→nova sessão→campos e mapa recuperados→F5. Distrito `5b03104f-9c47-5542-8de3-e81b8d5fdd55`; dois limites disponíveis e dois polígonos renderizados. Segundo recurso PT recuperado sem IDs MZ. Troca de pai limpa descendentes. | Demais consumidores/projetos/clusters/corredores e níveis inferiores não encerrados. Tiles externos bloqueados no Chromium; mapa base não demonstrado. Não houve nova importação cartográfica ou inferência de parcelas/direitos. |
| OP02; autorização | QA23 organização alheia: consulta403 e escrita403. PostgreSQL QA READ ONLY confirmou os2recursos v1 e o parentesco geográfico; ligações territoriais aceites permaneceram 0. | Outros perfis, documentos/derivadas e lifecycle/workflow conservam provas/pendências próprias. |
| GEO03; C87 | Lista repetia “limites oficiais por verificar” mesmo para administrativos do INE. Texto agora distingue edição administrativa de parcela; fonte e edição2019-06-07 preservadas. Popup já distinguia o propósito, lista foi alinhada. Chromium390px e imagem inspecionada. | A aceitação oficial não foi reaberta. A edição não afirma atualidade jurídica ou disponibilidade de investimento. Outras camadas e geometria de parcela têm critérios próprios. |
Provas sanitizadas: `territory-resource-chromium-20261010.json`, `territory-resource-persistence-qa.json`, `territory-resource-map-shot-20261010.json` e regressões `location-country-*qa.json`. Suite de recursos5/71 passou antes do ajuste de conferência; regressão específica final1/20 passou depois. TypeScript passou após o seletor; compilação QA finalexit0. Contagens de testes não substituem os percursos acima. Falhas do harness (versão Inertia omitida, seletor por atributo incorreto e caixa dos nomes) foram preservadas como tentativas; não são falhas do portal. Nome oficial ANCUABE não foi renomeado.
Mesma matriz:3393requisitos/3393linhas/6845ocorrências; quatro requisitos recebem subcritérios, sem alteração de estados globais. Países/origem/estatísticas restantes,173 correspondências anteriores com Mandie/Guro ambíguo, documentos, Response/§27, workflow/restauração e decisões modulares/SLA permanecem abertos. Não iniciar novas jornadas educativas.
**Código preparado e demonstração QA, sem disponibilidade ativa.** Zero migrações/instalações novas e zero escritas ativas. Manifesto ativo SHA256 `75352185c2a85c34ab726613db864a598e301a5b342ebbc80ac8e678eed17a94` intacto. O backup/restauro QA18:19 liga-se ao fingerprint anterior; não foi apresentado como prova da revisão nova. Para futura candidata: renovar verificações/backup/restauro associados à revisão, testar compatibilidade/reversão no clone do ativo e preservar **030000→040000→09/190000**, sem repetir instalações QA. O backup ativo READ ONLY17:48 permanece histórico; nenhum corte autorizado.
<!-- qa-territory-resource-c86-c87-20261010:end -->
<!-- c37-manual-close-20261010:begin -->
## C37 encerrado — autenticação QA18 — 2026-10-10T20:26:29.902032+02:00
**Confirmação manual expressa do responsável registada. C37 está encerrado exclusivamente no âmbito da autenticação QA18.** Login → MFA → redirecionamento → sessão autenticada → atualização → logout → nova sessão/recuperação estão ligados às provas anteriores, sem repetir ensaios nem alterar conta, chave ou MFA. O logout manual de 20:17:15 foi confirmado pelo responsável e pelo rastreio: POST302→/, autenticação e estado persistido vazios.
Aceitação e hashes dos recibos: `c37-manual-close-20261010/acceptance.json`. Referências: `c37-qa18-chromium-20261010.json`, `logout-finalize-20261010/manual-qa18-logout.json`, `portal-logout-chromium-20261010.json` e recuperação do dossier em `investor-ab-chromium-20261010.json`. A prova QA21 do botão é complementar; a conta18 tem a sua própria confirmação. Horário local UTC+02; datas técnicas dos recibos conservadas em UTC.
O fecho **não encerra A/B, auditoria integral, móvel/acessibilidade integral, outros perfis ou ativação ativa**. Os cinco requisitos relacionados continuam parciais pelos seus outros critérios; contagens preservadas: 3393 requisitos/3393 linhas/6845 ocorrências. Menções históricas abaixo a C37/autenticação18 pendente são superadas somente por este fecho. C85 permanece corrigido em QA. Próximo trabalho independente: gravação/recuperação territorial e consistência país–hierarquia por consumidor, países/origem/estatísticas, documentos e workflow, preservando decisões próprias de perfis modulares/SLA.
<!-- c37-manual-close-20261010:end -->
<!-- qa-logout-results-20261010:begin -->
## Logout disponível e confirmação manual — 2026-10-10T18:20:44.687863+00:00
**C85 corrigido em QA; nenhuma alteração no ativo.** A ação existia no menu de utilizador do dashboard, mas não na navegação do investidor. O cabeçalho comum mostra agora a conta atual e **Terminar sessão**, sem obrigar a regressar ao dashboard. Reutiliza POST `/logout` do Fortify e limpa o cache Inertia; chaves, MFA, perfis e dados guardados foram preservados.
O responsável confirmou o botão e o logout. Rastreio QA18 às **2026-10-10T18:17:15+00:00** (20:17:15 em Moçambique): POST logout302→/, conta18→sem autenticação e estado persistido vazio. C37 recebe apenas esse critério; F5 explícito e novo login/MFA QA18 após a saída continuam pendentes. O GET/login anterior com conta autenticada não era um novo login. A declaração geral de testes efetuados não substitui evidências dos restantes critérios.
Chromium QA21, contextos isolados: ação visível em investimentos, dossier, artigo e simulador; logout direto do dossier em desktop e 320px; API privada401 e página privada→login; nova sessão com MFA normal recuperou case/results/steps/tasks com hash igual e F5 manteve acesso. Seis verificações de cabeçalho em 320/390/1440px sem violações automáticas ou overflow; foco por teclado demonstrado. Não consumiu códigos QA18. Não encerra A/B, leitor de ecrã ou acessibilidade integral.
Provas sanitizadas: `portal-logout-chromium-20261010.json` e `logout-finalize-20261010/manual-qa18-logout.json`. Cinco requisitos receberam subcritérios; permanecem **3393 requisitos/3393 linhas/6845 ocorrências**, com estados globais inalterados.
Regressão de instalador/publicação: 9 testes/93 asserções e TypeScript passaram. Backup/restauro QA renovado **2026-10-10T18:19:43+00:00**, 295 tabelas/103 ficheiros, em `profile_restore_20261010_181928_test:55441`, associado ao novo fingerprint que inclui os dois menus. O backup ativo READ ONLY de 17:48:52 UTC permanece um retrato anterior; candidata global, compatibilidade/reversão no clone do ativo e ativação continuam pendentes. Preservar **030000 → 040000 → 09/190000**, sem repetir instalações QA nem copiar contas/dossiers de teste.
As afirmações históricas abaixo sobre logout manual ainda por testar são superadas somente por esta confirmação; nenhuma aceitação adicional inferida.
<!-- qa-logout-results-20261010:end -->
<!-- qa-visual-results-20261010:begin -->
## Ensaios visuais e pendências atuais — 2026-10-10T17:57:54.604301+00:00
Trabalho exclusivo na QA da porta 18097, base `desk_restore_20261009_174726_test:55441`. Nenhuma alteração na base ativa. A mesma matriz mantém **3393 requisitos / 3393 linhas / 6845 ocorrências de origem**: 3235 parciais, 145 não implementados, 5 dependentes de decisão, 1 implementado mas não ativado e 7 concluídos e demonstrados. Os estados globais não mudaram; foram atualizados subcritérios com provas próprias. A ausência de requisitos globalmente classificados “com falha” não significa ausência de casos abertos.
| Requisito | Demonstrado em QA | Critérios ainda pendentes |
| --- | --- | --- |
| C37; UI01; PROFILE24–25 | MFA manual QA18 aceite às 17:40:21 e 17:46:42 UTC; autenticação persistida e dashboard 200. Responsável confirmou acesso. O 403 inicial era do relatório administrativo reservado. Mensagem e retorno ao painel corrigidos; Chromium QA21 provou recusa 403, retorno e F5 sem perder sessão. | F5, logout e novo login no navegador habitual QA18. Não declarar o loop anterior resolvido por inferência: o rastreio não demonstra um ciclo persistente. Uma atualização Inertia 409 isolada não é perda de autenticação. |
| A/B; GUIDE01; SIM02; DESK01–02; CRM01 | Chromium desktop QA18: criação → gravação → nova sessão → recuperação → aplicabilidade → ferramenta → resultado no dossier → preparação no Balcão → acompanhamento interno → nova recuperação. Cinco fases e 19 etapas preservadas. A dispensa S10; B aguarda informação externa em S10 enquanto S02 avança. Resultado: equilíbrio de 20 unidades e défice A=0 / B=70.000 MZN. QA23 recebe 403/404; gestor não vê resultados financeiros privados. | Balcão demonstrado em preparação DRAFT, sem submissão ou decisão externa. Restantes critérios especializados, fiscalidade, documentos e consumidores não encerrados. Repetição final A/B móvel ainda necessária. |
| DASH01; PROFILE05/32 | QA21, a partir do dashboard: “Minhas oportunidades” e “Nova oportunidade” → rascunho incompleto #50 → guardar → nova sessão → recuperar → editar. Promotor solicita arquivo; Publicador QA25 arquiva com fundamento; estado recuperado. | Restauração por perfil ausente no workflow. Definir e implementar ator/regra coerentes sem conceder arquivo direto ao Promotor por inferência. Workflow e contratos especializados ainda exigem demonstrações próprias. |
| COUNTRY01–04/06/07 | Residência PT guardada no perfil e recuperada após F5. Negócio e fundiário DRAFT: país PT / origem FR guardados e recuperados. Balcão A/B: PT / ZA recuperados noutra sessão. Quatro formulários públicos apresentam 249 países cada. Gestão: filtros ZA/unknown, quatro manifestações sem origem e zero pedidos submetidos ZA, com rascunhos excluídos. | Nos formulários públicos, a presença de opções não prova submissão ou recuperação. Concluir cada gravação e estatística positiva de origem conhecida no UI. Nos rascunhos de negócio/fundiário, F5 não substitui nova sessão. |
| GEO01–03 | Oportunidade própria QA21 #51 e criação de projeto, cluster e corredor: Cabo Delgado registado → API com uma geometria acessível → um polígono SVG visível em cada consumidor. | Seleções desse ensaio não guardadas. Recuperar relações por consumidor e testar descendentes, restantes mapas e perfis. Tiles externos bloqueados no ensaio, sem confirmação da base externa. 408 associações anteriores e 173 pendentes preservadas; Mandie/Guro ambíguo. |
| LEG02/03/06/07; C77–C83 | Corrigidos landmarks, ARIA, hierarquia de títulos, reflow, foco das tabelas e idioma HTML pt-MZ em QA. Quinze execuções dirigidas de axe, em 320/390/1440px, sem violações automáticas ou overflow. Três páginas com skiplink primeiro e tabelas financeiras operadas por teclado. | Cobertura integral, leitor de ecrã, dispositivos reais, mais perfis e editor C74. O dossier QA21 verifica o componente móvel corrigido, sem substituir a repetição final A/B. |
| DOC01; C84 | Retirada mensagem global falsa de ClamD pendente; scan e motor apresentados por ficheiro/versão. Os 22 scans reais anteriores permanecem demonstrados. | PDFs, exportações, partilhas, derivadas e operações restantes exigem provas próprias. Um original limpo não comprova a derivada. |
| OPS01 | Backup QA 2026-10-10T17:48:11+00:00: 295 tabelas e 103 ficheiros restaurados. Ativo READ ONLY, backup 2026-10-10T17:48:52+00:00: 288 tabelas restauradas numa base QA distinta e cinco ficheiros conferidos no arquivo. Instalador/publicação: 9 testes / 93 asserções, além de TypeScript. | Aplicação, compatibilidade e reversão da futura revisão candidata no clone fresco do ativo. Revisão candidata global e ativação continuam por fixar. Contagens de testes não substituem critérios funcionais. |
**C80 corrigido:** a resposta tardia do GET do rascunho podia substituir preenchimento já iniciado. Edição e avanço ficam bloqueados até a recuperação terminar; o retorno exige o snapshot completo guardado. Chromium com resposta deliberadamente atrasada comprovou bloqueio, recuperação e libertação. A/B recuperaram os inputs corretos noutra sessão, preservando versões anteriores.
**Recusa manual do mapa às 17:52:33 UTC:** o pedido à oportunidade 51 foi feito com QA18, não QA21; recebeu 403 com sessão preservada. É uma prova de autorização, não de apresentação do mapa. Usar QA21 (`profiles-v1-promoter@example.invalid`) para esse consumidor; o Chromium anterior QA21 recebeu 200. Nenhuma permissão alargada para contornar a recusa.
**Provas privadas sanitizadas:** `investor-ab-chromium-20261010.json`, `promoter-dashboard-chromium-20261010.json`, `consumers-mobile-accessibility-chromium-20261010.json`, `visual-followup-chromium-20261010.json`, `qa-report-access-chromium-20261010.json`, `c37-qa18-post-mfa-destination-20261010.json`, `visual-persistence-qa.json`, `profile-installation-backup-qa.json` e `virtual-desk/backup-proof.json`. A tentativa antiga de QA21 na oportunidade 30 recebeu 403 e não contou como mapa; foi substituída pelo consumidor próprio 51. Tentativas e falhas de harness preservadas; sem códigos, segredos ou cookies nas evidências entregáveis.
Mercado Financeiro permanece privado, sem corpo inventado; rotas públicas 404. Energia e Mineração mantêm artigos distintos e Turismo não criou associações geográficas automáticas. Fontes oficiais aceites não foram reabertas. Próximo lote: C37 manual, restauração por perfil, submissão/estatística por formulário, gravação/recuperação territorial, documentos, Response/§27 e workflow. Perfis modulares/SLA continuam decisões separadas; não iniciar novas jornadas educativas.
As versões históricas abaixo ficam preservadas. Afirmações antigas sobre falta de navegador, A/B não testados ou ClamD global pendente são superadas apenas pelos subcritérios acima. Código preparado, QA demonstrada e disponibilidade ativa são estados distintos.
<!-- qa-visual-results-20261010:end -->
# InvesteMoz — backlog do lote 3
<!-- qa-c71-sectors-result-20261010:begin -->
## Prioridades atuais após C71 — 2026-10-10T16:23:25.695297+00:00
**QA recuperada e lote setorial demonstrado; base ativa fora do âmbito.** C71 técnico corrigido, com backup/restauro final de 295 tabelas/103 ficheiros. AUD-OPS-01 volta a parcial, porque aceitação global e ativação continuam pendentes. Biblioteca: 22 novos originais importados/scaneados;21 divulgados por ação separada e 1 artigo vazio privado. Chromium desktop com QA21:4 dossiers criados, retorno, logout/novaMFA/F5/replay; QA23 negado 403. Ver relatório executivo e provas individuais.
| Prioridade / requisito | Falta e próxima ação | Critério concreto |
| --- | --- | --- |
| P0 / AUD-UI-01,C37/QA18 | Incorporar tentativa manual sanitizada; primeira rejeição atual foi de palavra-passe, antes do MFA. 2FAS continua compatível; sem nova aplicação ou reset de segredo. | MFA→`/investimentos`→F5→logout→novo login na sessão problemática; não usar QA21 como substituto. |
| P1 / AUD-LEG-01/06/07 |32 das 50 versões ainda não importadas, sitemap/paginação/acervo e C68 (imagem 404). Reconciliar próximo lote por identidade/hash/citação. | Prévia, armazenamento privado, scan real, replay/recuperação e divulgação separada; não inventar corpo de Mercado Financeiro. |
| P1 / AUD-LEG-02/03/07 | Móvel/acessibilidade/mais perfis e editor administrativo C74 sem ensaio visual próprio; restantes ligações/consumidores. | Prova de cada ação visível e permissão; preservar confirmação manual limitada do piloto e diferenças entre versões. |
| P1 / percurso,I0–I3,A/B | Cenários completos com ferramenta→resultado→Balcão→acompanhamento, e ações do dashboard/workflow continuam independentes. | Cinco fases/19 etapas, nova sessão, preparação paralela, isolamento/permissões, retorno ao dossier. |
| P1 / países e cartografia | Completar países/origem/filtros/estatísticas por formulário e 173 correspondências territoriais; Mandie/Guro permanece ambíguo. | UI→API→gravação→recuperação→resultado por consumidor; território≠geometria≠mapa. |
| P1 / documentos,Response/§27 | Completar condições semânticas, PDFs/exportações/derivadas por consumidor. | Scan próprio, originais/hash preservados, permissões e resultados recuperáveis. |
| P1/P2 / decisões e ativação | Perfis modulares/SLA, calendário/nomeações reais e candidata permanecem separados. Preparar ação autenticada de importação em lote preservando identidade:CLI `--actor` sóQA. | Decisão concreta por regra; backup/restauro da base ativa da candidata e sequência 030000 → 040000 → 09/190000 sem repetições. |
**Mesma matriz:**3393 requisitos/linhas, 6845 ocorrências; 3235 parciais, 145 não implementados, 5 dependentes de decisão, 1 implementado mas não ativado, 7 concluídos e demonstrados, 0 com falha global. O backlogCSV agora projeta todos os 3393 IDs;39 estavam omitidos. Casos/aceitações dentro dos parciais continuam abertos. As secções abaixo são histórico anterior à recuperação/importação, não o estado atual.
<!-- qa-c71-sectors-result-20261010:end -->
<!-- qa18-credential-diagnosis-20261010:begin -->
**Atualização15:41UTC:** três tentativas manuais registadas com emailQA18 correto e palavra-passe recebida sem correspondência; retorno/login antes do MFA. Não houve desencriptação nem sessão autenticada nesses pedidos. Credencial privada reconfirmada por leitura e auxiliar local sem JSON aberto para cópia exata. Próxima ação: retomar a tentativa com esse valor; C37/MFA completo continuam pendentes. Prova privada `investor-credential-manual-rejection-20261010.json`, detalhes no C73.
## Acesso QA18 — confirmação das credenciais, C73/C37 pendente
Palavra-passe privada e MFA18 reconfirmados por leitura; navegador isolado no18097 chegou ao desafio da conta18 (login302 → MFA200), sem submeter código. Rejeição manual não reproduzida; manter causa/aceitação pendentes. Rastreio sanitizado recuperado após saturação5MiB, preservando histórico; próximos POST/login distinguem correspondência por booleanos sem guardar valores. Próxima ação: incorporar a tentativa manual e só fechar C37 após MFA → `/investimentos` → atualização → logout → novo login. Contas/chave/base ativa preservadas; C71 continua aberto. Ver C73 em `casos-reproducao.md` e recibos privados `investor-credential-*`.
<!-- qa18-credential-diagnosis-20261010:end -->
<!-- legacy-manual-sectors-20261010:begin -->
## Confirmação manual e reconciliação setorial — 10/10/2026
Responsável confirmou leitura pública → autenticação → criar/escolher dossier → guardar referência → nova sessão → recuperar dossier e abrir artigo. AUD-LEG-03/04/06/07 receberam apenas os subcritérios correspondentes; estados e contagens da matriz inalterados. Conta/perfil/dispositivo não indicados. Móvel, acessibilidade, outros perfis, cobertura integral, C37/QA18 e A/B continuam pendentes.
Quatro setores reconciliados: Energia & Mineração (índice + dois artigos), Turismo (artigo +11 referências territoriais), Infraestruturas (título corrigido em código) e Mercado Financeiro (corpo vazio preservado). **17 artigos +5 imagens** preparados; prévia CLI em transação READ ONLY:22 candidatos,0 duplicados,0 conflitos; nenhuma aplicação/scan/publicação nova. C72 corrigido na transformação dos originais, sem prova visual dos novos setores. C71 continua P0: recuperar integridade/backup/restauro antes de aplicação QA. Biblioteca/entidades/componentes existentes reutilizados; rotas dos restantes setores ainda por parametrizar. Ver `legado-piloto-qa.md` e inventários existentes.
| Prioridade | Próxima ação | Aceitação exigida |
| --- | --- | --- |
| P0 / C71 | Recuperar cluster QA preservando dados e renovar backup/restauro. | Conteúdos/schema/sequências/ficheiros conferidos; guarda, chave e percursos recuperados. |
| P1 / LEG01/05/06/07 | Renovar prévia dos22 exemplares, importar como rascunhos privados após C71, scan e parametrizar leitura/contexto por identidade. | Sem duplicações; fontes/URLs preservadas; publicação separada; origem/referência recuperadas por setor. |
| P1 / LEG02/07 / C72 | Verificar o título preservado de Infraestruturas na página real após importação. | Conteúdo integral legível; original/hash imutáveis; markup ativo removido. Verificação de transformação já passada. |
| P1 / Mercado Financeiro | Manter corpo vazio identificado e procurar no acervo um exemplar/conteúdo real. | Sem conteúdo inventado ou Fintech usado como substituto silencioso. |
| P1 / aceitação pendente | Ensaios móveis, acessibilidade e outros perfis, C37/QA18, A/B e restante acervo. | Evidência própria por percurso/perfil; relato manual do piloto não os encerra. |
<!-- legacy-manual-sectors-20261010:end -->
<!-- qa-legacy-browser-20261010:begin -->
## Próximo lote após a demonstração do piloto
| Requisito | Demonstrado | Falta / próxima ação |
| --- | --- | --- |
| AUD-OPS-01 / C71 — P0 | Diagnóstico e preservação READ ONLY; backup anterior intacto, readiness atual inválida. | Recuperar cluster QA e renovar backup/restauro antes de novas escritas; não fixar candidata. |
| AUD-LEG-04 | Consulta pública e login apenas para dossier; MFA da conta QA21/nova sessão/F5; anónimo/QA23 sem contexto; QA23 negado 403. Concluído exclusivamente QA. | Ativação separada; não assumir C37/QA18 resolvido. |
| AUD-LEG-02 | Desktop/móvel/fotografia/fontes/paleta, pesquisa, teclado; C70 título QA corrigido. | Verificação sistemática de controlos/tecnologias assistivas e adaptação dos outros setores. |
| AUD-LEG-03/05 | Artigo→consumidores→dossier/fonte/retorno; referência/evento únicos; sem mistura. | Fundamentar correspondências dos seis corredores; mapas gerais não equivalem a geometrias relacionadas com o artigo. |
| AUD-LEG-01/06/07 | Piloto visual e fontes preservadas. | Reconciliar os demais 50 exemplares observados, sitemap misto/paginação, outros URLs/anexos; C68 imagem 404 continua aberto. |
| C37; A/B; dashboard QA21 | Ensaio QA21 do piloto não os substitui. | C37 no contexto problemático QA18; A/B completos com ferramentas/Balcão/acompanhamento; CRUD de oportunidades pelo dashboard. |
Continuar países/origem/estatísticas, 173 lacunas cartográficas (Mandie/Guro ambíguo), documentação/derivadas, Response/materialidade e workflow na mesma matriz. Nenhum responsável real ou compromisso SLA foi designado. Base ativa fora do âmbito; sem repetir instalações QA030/040/09-190. C71 bloqueia novas escritas QA até recuperar a integridade e o restauro, sem revogar as provas já obtidas.
<!-- qa-legacy-browser-20261010:end -->
<!-- qa-legacy-20261010:begin -->
## Histórico do lote planeado antes do ensaio Chromium
| Prioridade / requisito | Próxima ação concreta | Critério de fecho |
| --- | --- | --- |
| P1 / AUD-LEG-02/03/04/06/07 | Roteiro visual do piloto com Promotor QA21: leitura anónima, pesquisa, fonte, criação/continuação, nova sessão, retorno e móvel/teclado. | Ações observadas por perfil/consumidor; mesmos IDs/dados, recuperação/negações e resultado visível. C37 continua independente. |
| P1 / AUD-LEG-01/05; LIB01/02; DOC01 | Reconciliar os 50 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ão importados 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 imagem legada GOVERNADOR-in.gif (404) e outros links não recolhidos antes de expandir esses artigos. | Original recuperado com fundamento ou ausência identificada; não declarar ficheiro inexistente como válido. |
| P1 / OP04 / PROFILE28 | Continuar Response integral/materialidade e cerimónia funcional; quatro módulos QA42–48 e ausência de histórico QA49 têm prova HTTP, não fecho visual/formal. | Critérios especializados por categoria; revisões afetadas, decisões externas/aprovação/publicação distintas. Confirmação legado não aprova perfis/SLA. |
Países/origem, estatísticas,173 lacunas geo (Mandie/Guro ambí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 compromissos SLA. Plano global 030000 → 040000 → 190000 conservado; recursos do legado/cartografia/divulgação separados. Ver `legado-piloto-qa.md`.
<!-- qa-legacy-20261010:end -->
<!-- qa-response-20261010:begin -->
## Incremento atual — Response/§27 e propostas para decisão
Somente QA **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Mesmo inventário/matriz; nenhum fecho integral ou escrita ativa.
- **AUD-OP-04:** `/minhas-oportunidades/{id}/response` integra a leitura do editor existente. Preserva IDs, códigos, referências UUID e estados atuais. Dez casos #30–39 recuperados noutra sessão MFA; Validador permitido, organização alheia 403 e anónimo 401. A versão do adaptador declara expressamente que a resposta normativa completa continua parcial.
- **C65 / QA40:** §27 exige unicidade de número cadastral por oportunidade. Rascunho incompleto/duplicado permanece recuperável; submissão devolve 422 junto a `canonical.land_rights.parcels.1.cadastral_number`. Após correção, submissão aceite. Números ausentes não são inventados.
- **C66 / QA41:** `succession` constava no contrato original mas era recusada pela lista semântica de venda. Lista reconciliada; criação/edição, submissão e nova sessão demonstradas. Não alterar silenciosamente outras modalidades.
- **Ainda parcial:** algoritmo de completude, metadados/identidade de itens inline, equivalência explícita dos estados e recuperação dos autores de alteração/exclusão dos recursos. §27 não cria tabelas paralelas. Condições sem regra inequívoca continuam como decisões D-S1/S2.
Provas: `opportunity-response-http-qa.json`, `OpportunityCanonicalWorkflowTest`, `profile-installation-checks-qa.json` e schema original preservado. O recibo identifica separadamente a tentativa anterior em que passaram os dez critérios de leitura e o ensaio semântico posterior; falhas do harness foram conservadas, não apresentadas como falhas do portal.
**Propostas modulares e SLA continuam não aprovados.** O ficheiro `propostas-perfis-modulares-sla.md` explicita compromissos internos de atendimento, janela operacional do Promotor, pausas por tarefa, espera externa separada e ausência de decisão automática. Nenhum `due_at`, titular ou perfil modular ativado.
**Ativação:** o restauro reportado é QA. Para uma futura ativação, conferir a identidade, schema, dados e configuração efetivos da base ativa para a revisão candidata, capturar hashes/manifesto dessa revisão e provar o seu backup/restauro. As provas QA ou `ready=true` não substituem essa ligação. Sequência 030000 → 040000 → 190000; C37, QA21 e A/B visual pendentes, candidata não fixada.
**Próximo trabalho independente:** completar metadados/Response sem novos cadastros; PDFs gerados/exportações/derivadas por consumidor; países/origem com filtros e estatísticas por formulário; correspondências inequívocas, mantendo Mandie/Guro ambíguo. Cartografia permanece 408 resolvidas e 173 lacunas neste incremento.
<!-- qa-response-20261010:end -->
<!-- qa-installation-cartography-20261010:begin -->
## Estado atual — instalador, documentos e cartografia
QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Ativo inspecionado apenas por leitura em 2026-10-10T10:32:41+00:00: 030000, 040000 e 09/190000 ausentes; zero geometrias; manifesto preservado. **C37, Promotor QA21 e A/B permanecem visualmente pendentes. Candidata à ativação ainda não fixada.**
QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Ativo inspecionado apenas por leitura em 2026-10-10T12:26:56+00:00: 030000, 040000 e 09/190000 ausentes; zero geometrias; manifesto preservado. **C37, Promotor QA21 e A/B permanecem visualmente pendentes. Candidata à ativação ainda não fixada.**
| Requisitos | Código preparado / prova QA | Critério ainda aberto |
| --- | --- | --- |
@@ -14,7 +180,7 @@ QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_tes
| OP-03 / OP-04 | Código da interface e resposta distinguem aprovação interna, disponibilização restrita e catálogo público. Casos #30 restrito, #33 público e #32 aprovado sem publicação recuperados pela API; consulta da política pública efetiva. | Interface; Response integral/§27 e condições sem regra inequívoca continuam parciais. Testes dos seis contratos não encerram a demonstração visual. |
| PROFILE-22/28 | [Quatro perfis e SLA com regra, fundamento, efeito e decisões concretas](propostas-perfis-modulares-sla.md). | Propostas **inativas**; quatro categorias continuam com rascunhos recuperáveis, mas submissão/publicação bloqueadas. Nenhum prazo ou responsável real configurado. |
Prova de recuperação QA: dump consistente → nova base `profile_restore_20261010_104956_test:55441` → **295 tabelas com linhas/hashes iguais e 69 ficheiros fisicamente recuperados por hash**; todos os ficheiros referenciados recuperados por hash. Arquivo privado inclui o runtime e a chave QA estável. Readiness só atesta preparação técnica desta revisão. Repetir após fixar a candidata e preparar backup ativo apenas numa futura ativação autorizada.
Prova de recuperação QA: dump consistente → nova base `profile_restore_20261010_123125_test:55441` → **295 tabelas com linhas/hashes iguais e 71 ficheiros fisicamente recuperados por hash**; todos os ficheiros referenciados recuperados por hash. Arquivo privado inclui o runtime e a chave QA estável. Readiness só atesta preparação técnica desta revisão. Repetir após fixar a candidata e preparar backup ativo apenas numa futura ativação autorizada.
**Matriz única:** 3.386 requisitos/linhas e 6.838 ocorrências de origem; todas as fontes, critérios e versões preservados; sem novos fechos globais. Contagens por estado abaixo e na projeção dos pedidos abertos.
@@ -1,5 +1,126 @@
<!-- qa-territory-resource-cases-20261010:begin -->
## Casos C86/C87 — recursos e cartografia
**C86 (P1, OP02/GEO02):** editar localização MZ com província e mudar payload para PT mantendo IDs MZ. Esperado422 no país e preservação do original; observadoantes200 e gravação contraditória. Causa: catálogo de países e seleção territorial validados separadamente; editor e conferência não aplicavam país. Correção: regra cruzada no servidor; UI retira a hierarquia somente no preenchimento após mudança explícita; rascunho estrangeiro incompleto aceite; conferência não pede campos MZ para estrangeiro. Provas: regressão antes/depois, browserQA21 e SQLREADONLY; tentativa rejeitada não muda payload/versão/auditoria. Dois recursos guardados/recuperados noutra sessão; QA23 consulta/escrita403. Nenhuma conversão de registos históricos.
**C87 (P1, GEO03):** recuperar recurso CaboDelgado/Ancuabe e ler a lista sob o mapa. Esperado limite administrativo/edição/fonte; observado “limites oficiais por verificar” genérico. Causa: lista tinha texto fixo, apesar de popup usar purpose=administrative_boundary. Correção: lista usa o propósito existente e distingue edição administrativa de polígono a comparar; fontes oficiais aceites preservadas. Chromium390px mostrou dois limites e legenda correta, origemINE/2019-06-07. Tiles externos não testados; geometrias/dados/relações não modificados. Provas: territory-resource-map-shot-20261010.json e imagens anteriores preservadas.
C37 já encerrado por confirmação manual somente para autenticação QA18; não reabrir pelo estado histórico dos recibos. A/B e demais critérios independentes continuam abertos.
<!-- qa-territory-resource-cases-20261010:end -->
<!-- c37-manual-close-20261010:begin -->
## C37 encerrado — autenticação QA18 — 2026-10-10T20:26:29.902032+02:00
**Confirmação manual expressa do responsável registada. C37 está encerrado exclusivamente no âmbito da autenticação QA18.** Login → MFA → redirecionamento → sessão autenticada → atualização → logout → nova sessão/recuperação estão ligados às provas anteriores, sem repetir ensaios nem alterar conta, chave ou MFA. O logout manual de 20:17:15 foi confirmado pelo responsável e pelo rastreio: POST302→/, autenticação e estado persistido vazios.
Aceitação e hashes dos recibos: `c37-manual-close-20261010/acceptance.json`. Referências: `c37-qa18-chromium-20261010.json`, `logout-finalize-20261010/manual-qa18-logout.json`, `portal-logout-chromium-20261010.json` e recuperação do dossier em `investor-ab-chromium-20261010.json`. A prova QA21 do botão é complementar; a conta18 tem a sua própria confirmação. Horário local UTC+02; datas técnicas dos recibos conservadas em UTC.
O fecho **não encerra A/B, auditoria integral, móvel/acessibilidade integral, outros perfis ou ativação ativa**. Os cinco requisitos relacionados continuam parciais pelos seus outros critérios; contagens preservadas: 3393 requisitos/3393 linhas/6845 ocorrências. Menções históricas abaixo a C37/autenticação18 pendente são superadas somente por este fecho. C85 permanece corrigido em QA. Próximo trabalho independente: gravação/recuperação territorial e consistência país–hierarquia por consumidor, países/origem/estatísticas, documentos e workflow, preservando decisões próprias de perfis modulares/SLA.
<!-- c37-manual-close-20261010:end -->
<!-- qa-logout-c85-20261010:begin -->
## C85 — logout ausente no percurso do investidor
- **Reprodução:** autenticar QA18, abrir `/investimentos` ou um dossier; procurar logout no cabeçalho. Não havia botão; era preciso conhecer o menu do dashboard.
- **Esperado:** terminar sessão diretamente no percurso, em desktop/móvel e por teclado; dados guardados preservados e acesso privado negado após saída.
- **Causa:** PortalNavigation usa PortalGroupedMenu sem ação de sessão; o logout de UserMenuContent só era apresentado pelo AppLayout do dashboard.
- **Correção QA:** conta e botão Terminar sessão no cabeçalho comum; POST Fortify/CSRF existentes, limpeza do cache Inertia, sem alteração de guard, MFA ou privilégios.
- **Evidência:** responsável confirmou botão/logout QA18; trace18 POST/logout302→/ com auth e persistência vazias. Chromium21 desktop/móvel, API401, página privada→login, nova sessão MFA e snapshot do dossier iguais. Cabeçalho acessível por teclado, sem overflow ou violações automáticas nos seis ensaios.
- **Estado:** causa corrigida e logout manual demonstrado. C37 continua limitado pelos critérios F5 explícito e novo login/MFA18 após saída; provas21 não substituem conta18. Provas sanitizadas e 0 escritas ativas no relatório atual.
<!-- qa-logout-c85-20261010:end -->
<!-- qa-visual-cases-20261010:begin -->
## Casos adicionais C77–C84 e estado C37 — 10/10/2026
| Caso | Reprodução e observado | Esperado, causa e correção QA |
| --- | --- | --- |
| C37 | MFA manual aceite; destino pretendido era relatório administrativo, negado 403 com sessão autenticada. Nova tentativa 17:46:42 UTC chegou ao dashboard 200; responsável confirmou acesso. | Recusa administrativa correta. Página QA explica o acesso reservado e oferece retorno; pedido Inertia passa uma vez por 409 para renderizar a página completa, evitando modal na página MFA. QA21 provou 403 → painel → investimentos → F5. C37 habitual ainda pede F5/logout/novo login; ciclo anterior não demonstrado como falha persistente. |
| C77 / UI01, LEG02, DASH01 | axe: mains aninhados, conteúdo fora de landmarks e cabeçalho Sheet fora do Dialog. | Um main, navegação e apoio nomeados, SheetHeader dentro do SheetContent. Quinze verificações dirigidas posteriores sem violações automáticas. |
| C78 / GEO03 | Mapas com aria-label num div sem role. | Região nomeada. Cabo Delgado apresentou um polígono em quatro consumidores; seleção ainda não guardada e tiles externos não testados. |
| C79 / LEG02 | Turismo começava com h3 sob o título h1, omitindo h2. | Normalização apenas na apresentação para h2..h6 e saltos máximos de um nível, mantendo original, bytes, hash e texto. LegacyContentTest 5/84 e Chromium passaram. |
| C80 / SIM02, GUIDE01 | Preenchimento iniciado antes de terminar o GET podia ser sobrescrito pelo rascunho remoto; faltavam campos de A na recuperação. | Impedir edição e avanço até recuperar; cancelar efeito obsoleto; devolver resultado só após guardar snapshot completo. Chromium com GET deliberadamente atrasado e nova sessão A/B demonstraram recuperação. Histórico preservado. |
| C81 / SIM02 | Tabelas financeiras com scroll sem foco por teclado. | tabindex=0, região nomeada e foco visível. QA21 focou ambas e usou ArrowDown; axe passou. Leitor de ecrã real pendente. |
| C82 / GUIDE01 | Dossier a 390px alargava a página para 413px devido a labels/select/datetime da atribuição. | min-width:0, max-width:100% e quebra de linha, sem esconder ações. Dossier próprio QA21 corrigido sem overflow; repetir A/B móvel final. |
| C83 / UI01, LEG02 | HTML lang=en para interface em português. | Configuração pt-MZ somente no router QA e fallback anterior no template. Quinze verificações com pt-MZ; locale de validação preservado. |
| C84 / DOC01 | Mensagem global “ClamD real pendente” contrariava os scans reais. | Mostrar scan/motor por ficheiro/versão; retirar texto global. QA21 recursos51 confirmou apresentação; nenhuma derivada/PDF recebeu scan fictício. |
**Mapa manual 51, 17:52:33 UTC:** conta autenticada QA18 recebeu 403; QA21 havia recebido 200. Sessão mantida. É recusa correta por perfil/autoria e não um mapa funcional demonstrado pelo responsável. Credenciais e entradas MFA são distintas; não ampliar autorização para permitir o ensaio na conta errada.
Provas: `visual-followup-chromium-20261010.json`, `qa-report-access-chromium-20261010.json`, `investor-ab-chromium-20261010.json`, `visual-persistence-qa.json` e `layout-map-diagnosis-chromium-20261010.json`. Falhas do harness preservadas separadamente: ações antes de guardar, moeda não selecionada, esperas e oportunidade30 alheia a QA21. Não contar o 403 dessa oportunidade como cobertura do mapa. Nenhuma alteração ativa.
<!-- qa-visual-cases-20261010:end -->
# Casos de reprodução e lacunas observáveis
<!-- qa-c71-sector-cases-20261010:begin -->
## Estado dos casos C71–C76 após o lote — 2026-10-10T16:23:25.695297+00:00
**C71 / AUD-OPS-01:** causa/operação/reprodução históricas preservadas abaixo. CorrigidoQA: recuperação emcluster novo, comparação295 tabelas/34.902 linhas/31 sequências e schema/permissões compatíveis; cópia física75575 ficheiros com hashes iguais. Prévia22 itens renovada antes de aplicação; backup/restauro final295/103verificado. Processo que removeu26catálogos continua não identificado; nenhum remendo de catálogo ou rotação de chave. Requisito inteiro parcial pelas demais dependências/ativação. A manutenção foi retirada; GET/login200no18097. Ver provas no relatório atual.
**C72 / AUD-LEG-02/07:** título deInfraestruturas desaparecia pelo sanitizador excluirh1/filhos; h1→h2 preserva conteúdo e limpeza. Agora exemplar importado/scaneado/consultável e título observado no Chromium, captura `infra-original-heading`; original/hash intacto. CorrigidoQA, não ativo.
**C73 / AUD-UI-01/C37:** a manutenção técnica é independente das3rejeições de palavra-passe15:38–15:39UTC; estas ocorreram antes do MFA. QA18secret/chave/credencial reconfirmados15:57:57porREADONLY. 2FAS Auth já suportaTOTP; não requer nova aplicação/remoção da entrada antiga. C37continua pendente no navegador do responsável, sem novo ensaio MFA18 deste lote.
**C74 / AUD-LEG-07, consumidor documental:** reproduzir comimagem do manifesto semtema: login gestorQA14→PUT `/management/portal-services/resources/24` comAssuntovazio→HTTP422campo`topic`. Esperado: guardar metadados originais autorizados seminventarassociação. Causa: importador admite vazio, validaçãoAPI/frontend exigiamvalor. CorreçãoQA: tema nullable sobguarda realQA, vazio normalizado para stringcompatível; interfaceAssuntoopcional. RepetiçãoPUTs das5 imagens+recuperaçãoSQL/download passaram; originais/hashes/scans/ausência de relações preservados. Teste dedicado1/11asserções; visualdoeditor ainda pendente. DivulgaçãoPOSTseparada, sem contorno da autorização.
**C75 / AUD-LEG-02/03, regressão detectada antes da divulgação:** GET `/explorar/agronegocio` no testeLegacyContentTest retornava500 na nova parametrização, `Undefined property stdClass::$title`. Causa: projeçãoSQL selecionava documento mas omitia título do recurso usado nas leituras relacionadas. Corrigido incluindo`r.title`; teste4/81eHTTP/Chromium passaram. Não atribuir ao middleware. Nenhuma ocorrência ativa alegada.
**C76 / AUD-LEG-03/06, regressão detectada antes da divulgação:** POST `/investimentos` comartigo válido retornava422. Esperado: validar ID da biblioteca e guardar fonte numdossier próprio. Causa: `value(d.external_id)`sobrebuilder comprojeção pré-existente lia primeiro campoUUID; não encontrava a identidade configurada. Correção: leitura explícita`first()?->external_id`, preservando controles público/hash/scan/contexto. Testes4/81; HTTP4 dossiers/Chromium4 dossiers eREADONLY8 referências únicas. CorrigidoQA, nenhuma escrita ativa.
Falhas do harness conservadas separadamente: primeira leitura usou indevidamente rota pública de download para exemplar privado404 (a rota protegida correta é `/management/documents/{UUID}/download`); recompilação durante sessãoInertia produziu409/reload esperado; primeiras esperasnetworkidle/imagem ainda não decodificada falharam sob concorrência. Corrigida ordenação/espera no teste, sem alterar autenticação/aplicação para fabricar sucesso. Histórico de tentativasprivadas preservado; resultado final sófoi declarado após percursos completos.
<!-- qa-c71-sector-cases-20261010:end -->
<!-- qa18-credential-diagnosis-20261010:begin -->
**Atualização manual15:41UTC:** os pedidos15:38:38,15:39:04 e15:39:43UTC têm `email_exact_matches_qa18=true` e `password_matches_qa18=false`; POST/login302 → GET/login200, sem `login.id` nem utilizador autenticado. Primeiro ponto de falha agora identificado: validação da palavra-passe recebida, antes do MFA; não é rejeição do código, desencriptação ou perda após autenticação. A razão pela qual o valor enviado diverge não está demonstrada. A palavra-passe privada foi novamente conferida por READ ONLY às15:41:06UTC; hash e MFA válidos, conta inalterada. Rastreio sanitizado arquivado em `investor-credential-manual-rejection-20261010.json`. Preparado um auxiliar privado local com apenas a palavra-passe (sem JSON/aspas), ignorado pelo Git, para cópia direta; nenhum segredo publicado. Conector do navegador manual indisponível; completar a tentativa nesse navegador e manter C37 aberto.
## C73 — rejeição de credenciais QA18 reportada; diagnóstico renovado
**Requisito:** AUD-UI-01/C37; P0 de acesso, confirmação manual pendente. **Relato:** `/login` em `http://127.0.0.1:18097` apresentou “These credentials do not match our records.” **Resultado esperado:** credenciais QA18 corretas conduzem ao desafio MFA, sem autenticar antes de um segundo fator válido. **Diagnóstico READ ONLY 10/10/2026,15:34:50UTC:** destino efetivo `desk_restore_20261009_174726_test:55441`, uma conta18, email exato, palavra-passe privada corresponde ao hash, sem necessidade de rehash; MFA confirmado, desencriptação e correspondência com segredo privado válidas; cache de configuração não carregada. Nenhuma alteração à conta, MFA, chave ou base ativa.
**Interface observada:** Chromium em contexto isolado, pelo mesmo servidor18097; GET/login200 → POST/login302 → GET/two-factor-challenge200, com conta18 indicada e `login.id=18`; nenhuma autenticação completa nem código MFA submetido. Recibos privados `investor-credential-diagnosis-20261010_153450.json` e `investor-credential-browser-*.json`. A rejeição relatada não foi reproduzida com os valores do ficheiro; a causa da tentativa manual permanece indeterminada. Não atribuir a desencriptação, sessão, erro de cópia ou conta errada sem evidência da tentativa.
**Falha adicional identificada e corrigida no rastreio:** o ficheiro anterior atingiu5243564bytes e deixou de recolher pedidos às10:07:06UTC, por limite de5MiB. Histórico preservado; novos pedidos gravados em segmentos privados. O diagnóstico de POST/login regista apenas booleanos de correspondência à conta18, nunca email submetido, palavra-passe, hash, cookies ou códigos. Próxima rejeição poderá distinguir credenciais enviadas, resposta e estado MFA/sessão. C37, a recuperação autenticada e A/B continuam pendentes; esta prova termina no ecrã MFA. C71 permanece independente e aberto.
<!-- qa18-credential-diagnosis-20261010:end -->
<!-- legacy-sector-heading-20261010:begin -->
## C72 — título original removido na projeção de Infraestruturas
**Requisitos:** AUD-LEG-02/07; P1 na expansão. **Reprodução:** aplicar `LegacyContent::sanitize()` ao HTML original do post171992, através de `tools/legacy-sector-content-check.php before`. **Esperado:** título “2. Sectores Prioritários de Investimento” preservado sem introduzir segundo h1 de página. **Observado:** título ausente; os restantes16 exemplares do lote não apresentavam essa perda. **Causa:** lista de apresentação não admitia h1 e removia o elemento com todos os filhos. **Correção preparada:** normalizar h1 interno para h2 e manter limpeza de atributos/filhos. **Prova:** verificação after sobre17 originais, hashes preservados, título presente e markup ativo removido; nenhum acesso à base. **Pendente:** importar o exemplar privado depois de C71, divulgar separadamente e demonstrar a página real. Não atualizar outros critérios da matriz com esta prova de transformação, nem declarar o setor publicado. C37/QA18 continua independente.
<!-- legacy-sector-heading-20261010:end -->
<!-- qa-legacy-browser-20261010:begin -->
## C70 — identificação errada no título do navegador
**Requisito:** AUD-LEG-02; P2. **Reprodução:** abrir anónimamente `/explorar/agronegocio` na QA 18097 e consultar o título do separador. **Esperado:** identidade do portal e distinção QA. **Observado:** “Explorar agronegócio - Laravel”. **Causa:** `resources/js/app.tsx` usa o nome padrão da compilação; a QA herdava `VITE_APP_NAME` do ambiente local. **Correção:** `vite.config.ts` define “InvesteMoz QA” exclusivamente sob `INVESTMOZ_QA_BUILD=1`; não altera .env nem build ativo. **Evidência:** título verificado no Chromium, percurso renderizado após recompilação; manifesto ativo inalterado. Código preparado eQA demonstrada; não ativado.
As primeiras tentativas do ensaio de browser terminaram por falta de espera da navegação Inertia no próprio teste, antes de inspecionar o URL atualizado. O runner foi corrigido para aguardar o destino; recibos das tentativas preservados privadamente. Não classificar essas falhas de instrumentação como falhas do portal. C37 não foi repetido; C68 permanece aberto.
## C71 — falha do novo backup, cluster PostgreSQL QA
**Requisito:** AUD-OPS-01, dependência de AUD-PROFILE-31; P0, aberto. **Reprodução:** guarda identifica `desk_restore_20261009_174726_test:55441`; executar `tools/portal-profile-installation-backup-qa.php`. **Esperado:** dump atual, restauro noutra base QA e comparação integral. **Observado:** `pg_dump` exit 1 ao consultar `pg_roles`, pois não consegue abrir `global/2964`; nenhum novo restauro criado. **Causa identificada:** ficheiro de `pg_db_role_setting` ausente; inspeção READ ONLY identifica 26 ficheiros partilhados ausentes no cluster QA em Temp. O processo que os removeu não foi identificado. **Mitigação:** originais/backups anteriores preservados; 295 tabelas/34.902 linhas/31 sequências extraídas privadamente por leitura; readiness atual invalidada e erro do CLI controlado. **Correção pendente:** recuperar cluster QA preservando schema, IDs, relações, sequências e chave estável, conferir todos os dados/ficheiros e repetir backup/restauro antes de ensaios com escrita. Não declarar preservação NDJSON ou backup13:35 como restauro renovado. Nenhum `key:generate`, catálogo inventado, reinício do PostgreSQL ativo ou escrita ativa.
Referência técnica: [PostgreSQL17 — pg_db_role_setting](https://www.postgresql.org/docs/17/catalog-pg-db-role-setting.html), catálogo partilhado por todas as bases de um cluster. Diagnósticos completos e extrações permanecem privados.
<!-- qa-legacy-browser-20261010:end -->
<!-- qa-legacy-20261010:begin -->
## 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` de 500 carateres. Causa no novo adaptador, não no middleware. Correção limita projeção a 499 + reticência e preserva texto canónico completo. Tentativa falhada arquivada; depois QA42–48 divulgadas/recuperadas noutra sessão MFA. Visual pendente.
- **C68 / LEG07:** GET público legado `/images/ANO-2017/JUNHO/21/GOVERNADOR-in.gif`. Esperado: imagem original; observado 404. Identificado no inventário; não afeta PNG do agronegócio. Falha permanece emartigos consumidores dessaimagem; não inventar substituição ou autenticidade.
- **C69 / LIB02 / LEG01:** prévia do exemplar do artigo JSON no importador existente. Esperado: cadastro privado, scan e adaptação segura; observado MIME application/json não suportado. Lista doimportador reconciliada e upload JSON guardado em QA; original preservado, apresentação sanitizada, hash verificado, publicação autenticada separada. Dois recursos; repetição sem duplicação. Não alargar divulgação ativa.
Superutilizador27 recebeu403 em gestão documental, conforme autorização existente; gestor QA14 executou divulgação. Esse ensaio negativo comprova segregação e não é uma falha a contornar. C37 não foi retestado nem encerrado.
<!-- qa-legacy-20261010:end -->
<!-- qa-response-20261010:begin -->
- **C65 / QA40:** §27 exige unicidade de número cadastral por oportunidade. Rascunho incompleto/duplicado permanece recuperável; submissão devolve 422 junto a `canonical.land_rights.parcels.1.cadastral_number`. Após correção, submissão aceite. Números ausentes não são inventados.
- **C66 / QA41:** `succession` constava no contrato original mas era recusada pela lista semântica de venda. Lista reconciliada; criação/edição, submissão e nova sessão demonstradas. Não alterar silenciosamente outras modalidades.
C65 foi identificado por confronto código/§27; C66 por divergência da lista do servidor com o contrato. A prova atual é pós-correção. Não atribuir a estes casos falhas antigas não reproduzidas.
<!-- qa-response-20261010:end -->
<!-- qa-installation-cartography-20261010:begin -->
## Casos adicionais deste lote — sem reiniciar o inventário
@@ -535,7 +535,7 @@ C:\laragon\www\portal-investidor\docs\investmoz\v2\consolidacao-nove-diferencas.
C:\laragon\www\portal-investidor\docs\investmoz\v2\consolidacao-variantes.md,7958,3defe7f46aeaa37da2f06f5c358da83f48ff4656566d4044bfe7cfb0ad50d5b2,original,,Documentação do repositório,Nenhum cálculo ativado pela auditoria,Situação documental não determinada por inventário,
C:\laragon\www\portal-investidor\docs\investmoz\v2\ficheiros-primeira-entrega.csv,15338,b8c6eefb8fe879df7637bcda4aa4d458254edd82aceaa86991b5d885c046ee0a,original,,Documentação do repositório,Nenhum cálculo ativado pela auditoria,Situação documental não determinada por inventário,
C:\laragon\www\portal-investidor\docs\investmoz\v2\ficheiros-segunda-entrega.csv,3345,a7445854213ad91cf5a929237dc7c2ab2f2f682f5eb014cd10c4f9623ac30d20,original,,Documentação do repositório,Nenhum cálculo ativado pela auditoria,Situação documental não determinada por inventário,
C:\laragon\www\portal-investidor\docs\investmoz\v2\gestao-oportunidades-dashboard.md,13951,a12b57985e779180860983968922941bac9db3ab91aa4977d4ccf2b5b6d74997,original,,Documentação do repositório,Nenhum cálculo ativado pela auditoria,Situação documental não determinada por inventário,
C:\laragon\www\portal-investidor\docs\investmoz\v2\gestao-oportunidades-dashboard.md,58976,a544573a042064c74cc1aaa3003cdecf82f9d17b5ff198956b634504b74ba183,original," | [Visual QA 10/10/2026] Versão atual com provas visuais dirigidas. SHA anterior a12b57985e779180860983968922941bac9db3ab91aa4977d4ccf2b5b6d74997, bytes anteriores 13951; histórico preservado. | [Visual QA 10/10/2026] Versão atual com provas visuais dirigidas. SHA anterior b08f5b48263f4cca4aefeaaf6d59d6f1dfee185f2e0c57a8d4e00743588b0c6e, bytes anteriores 57819; histórico preservado. | [Visual QA 10/10/2026] Versão atual com provas visuais dirigidas. SHA anterior c54663ddf13b48235da8614d2b2f7c8b62bf801142d00d45642d65bcbbaba5fe, bytes anteriores 58005; histórico preservado.",Documentação do repositório,Nenhum cálculo ativado pela auditoria,Situação documental não determinada por inventário,
C:\laragon\www\portal-investidor\docs\investmoz\v2\indice-documento-mestre-atual.csv,151889,35ffb5d727f8ab1811ac946c2f89c2e95a74108b9a9cfbdd688a29fffee6080f,original,,Documentação do repositório,Nenhum cálculo ativado pela auditoria,Situação documental não determinada por inventário,
C:\laragon\www\portal-investidor\docs\investmoz\v2\indice-documento-mestre.csv,153757,4c13f1efd25a130c0d63e4a7f2efd9f9c2fbe9eb7ddee452534f06e8d7759bdd,original,,Documentação do repositório,Nenhum cálculo ativado pela auditoria,Situação documental não determinada por inventário,
C:\laragon\www\portal-investidor\docs\investmoz\v2\oportunidades-correspondencias.md,15378,3812fe47bf667a5323363cbfc7c0b329c1f717f4ea814a6e3c454e6624224a72,original,,Documentação do repositório,Nenhum cálculo ativado pela auditoria,Situação documental não determinada por inventário,
@@ -590,4 +590,9 @@ C:\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\prompt\novos\~$ompt a se
C:\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\prompt\novos\Gestao de perfis investeMOZ.docx,46648,283a32f4c4b02bdfd9318a9174268224eeefac4349c49e3171545707bd222855,original,Inventário complementar 10/10/2026; versões anteriores preservadas,Acessível localmente; importação/publicação não inferida,Nenhum cálculo ativado,Situação jurídica/documental não inferida do inventário,Exemplar identificado por SHA256
C:\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\prompt\novos\~WRL4044.tmp,46970,87fee09bb7e547d7aeebe7de984681fb52e588c74988da7ebe0ac72048e73a22,original,Inventário complementar 10/10/2026; versões anteriores preservadas,Acessível localmente; importação/publicação não inferida,Nenhum cálculo ativado,Situação jurídica/documental não inferida do inventário,Exemplar identificado por SHA256
C:\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\prompt\novos\Gestao de perfis investeMOZ v1.docx,50215,f9e0f6280566fed8b52d0b36bb814a82e11976fec9ea9f62d64611ff8c824b38,original,Inventário complementar 10/10/2026; versões anteriores preservadas,Acessível localmente; importação/publicação não inferida,Nenhum cálculo ativado,Situação jurídica/documental não inferida do inventário,Exemplar identificado por SHA256
C:\laragon\www\portal-investidor\docs\investmoz\v2\auditoria-portal\propostas-perfis-modulares-sla.md,17568,1fa569e1c02d0e204f98e58252babefc2e7f0391265215e5e49e7566026620c2,proposta interna QA,"Documento integral; regras/fundamento/efeito/decisões efonte§25.4. Não fonteoficial,nenhuma aprovaçãoinferida.",Consulta no repositório; publicação não inferida,Nenhuma regra/SLA/cálculo ativado,"Proposta para decisão, não ato jurídico",proposta20261010v1
C:\laragon\www\portal-investidor\docs\investmoz\v2\auditoria-portal\propostas-perfis-modulares-sla.md,24585,20028b0abcb1235109668b6c36ef7e931777c5444334a776f967a7e1ee750c85,reconciliação auditoriaQA,Legado inventariado/pilotoQA; cobertura histórica e decisões separadas explicitamente preservadas.,Relatório/inventário para consulta; não habilita importação/publicação ativa,Nenhum cálculo ativado,"Documento de programação/reconciliação, não fonte oficial",proposta20261010v1 | exemplar anterior SHA256=a4549b18cad22eddeafdad7cb64f299c4200b01e439fb6a334714c19c7f0a588 | atualizaçãoQA10/10/2026 | atualizaçãoQA10/10/2026
C:\laragon\www\portal-investidor\docs\investmoz\v2\auditoria-portal\legado-piloto-qa.md,38405,8ffe19cc416804baa67487d3c7f2218b3ed218ca72c855c97f1c8bef65a8189a,reconciliação auditoriaQA,"C71 recuperado e 22 originais QA importados/scaneados; 21 divulgados separadamente; 16 novos artigos e 4 dossiers demonstrados. Históricos/restrições preservados. | Nota anterior: Confirmação manual limitada e reconciliação dos quatro setores;22 candidatos privados, sem importação/publicação; C71/C37/A-B/móvel/acessibilidade/outros perfis preservados. | [Visual QA 10/10/2026] Versão atual com provas visuais dirigidas. SHA anterior c610372999be13f3df9d7683693adff1dbd8db4c9439cd2e951db2d294db4781, bytes anteriores 31191; histórico preservado. | [Visual QA 10/10/2026] Versão atual com provas visuais dirigidas. SHA anterior b2b25ef793d7b90b6b9a5295792a7d2415db3138851a7d50da9d090c84367938, bytes anteriores 37248; histórico preservado. | [Visual QA 10/10/2026] Versão atual com provas visuais dirigidas. SHA anterior 11ae88513acc709f80504bbf8f74200bafd47b3b910bdbc0eed7a921fcca1dc0, bytes anteriores 37434; histórico preservado.",Relatório/inventário para consulta; não habilita importação/publicação ativa,Nenhum cálculo ativado,"Documento de programação/reconciliação, não fonte oficial", | atualizaçãoQA10/10/2026 | atualizaçãoQA10/10/2026 | exemplar anterior SHA256=2b23ac5f0e0117f53362dfaa756b083aabb5ddc0625a4a2c4c7acd8a06902467 | exemplar anterior SHA256=4561077d6ce5e112e2ccb5d04742a304f03fd4bc188dc0133b4833497427b85f | interfaceQA10/10/2026 | exemplar anterior SHA256=1ca20bb4036f8c73ea5a98a53d43964fe555427595d67df8f7f64c7e1ee591d3 | interface QA10/10/2026 | exemplar anterior SHA256=30c9e98b5615fbc9992d1522155c8556b0371b06618ec041f21812a81ea12477 | exemplar anterior SHA256=25c343e53febc02cf04455fef61f6dd6e9a959ca6a25b288ada0b154182accbe | exemplar anterior SHA256=87a11691519df7bb6c29e74ae525f0a7074c54bca3e3eed38799b37c5fa0bf1c | loteQAsetores2026-10-10T16:23:25.695297+00:00; exemplar anteriorSHA256=bb3d68ddac3e97a34246e20ba4cfd30f34afc523d2439be0ec38e83b64a6ad31 | revisão de legibilidade; exemplar anterior SHA256=c5ec30a05739fabfadf958398903ae9efe6cb50af8bbc1534dedde484aa5b989
C:\laragon\www\portal-investidor\docs\investmoz\v2\auditoria-portal\legado-inventario.csv,35375,56cc3058d6824b8535cbe8ea905c8764233849656c62af0eaa1c76275cf191e9,reconciliação auditoriaQA,"C71 recuperado e 22 originais QA importados/scaneados; 21 divulgados separadamente; 16 novos artigos e 4 dossiers demonstrados. Históricos/restrições preservados. | Nota anterior: Confirmação manual limitada e reconciliação dos quatro setores;22 candidatos privados, sem importação/publicação; C71/C37/A-B/móvel/acessibilidade/outros perfis preservados.",Relatório/inventário para consulta; não habilita importação/publicação ativa,Nenhum cálculo ativado,"Documento de programação/reconciliação, não fonte oficial", | atualizaçãoQA10/10/2026 | atualizaçãoQA10/10/2026 | exemplar anterior SHA256=ff219009d137cd691fd08ad18135a7c039548141187334d9b9fdda806ea119b3 | loteQAsetores2026-10-10T16:23:25.695297+00:00; exemplar anteriorSHA256=74b33e590199880f9d04e77ce989aca63f30b92ceecc1cfbe172fa9f40784487
C:\laragon\www\portal-investidor\docs\investmoz\v2\auditoria-portal\legado-conteudos-reconciliados.csv,19973,02a352202d7e0bbe9e72ec7132b46489094facd4ef040f7074c8031f93ff7660,reconciliação auditoriaQA,"C71 recuperado e 22 originais QA importados/scaneados; 21 divulgados separadamente; 16 novos artigos e 4 dossiers demonstrados. Históricos/restrições preservados. | Nota anterior: Confirmação manual limitada e reconciliação dos quatro setores;22 candidatos privados, sem importação/publicação; C71/C37/A-B/móvel/acessibilidade/outros perfis preservados.",Relatório/inventário para consulta; não habilita importação/publicação ativa,Nenhum cálculo ativado,"Documento de programação/reconciliação, não fonte oficial", | atualizaçãoQA10/10/2026 | atualizaçãoQA10/10/2026 | exemplar anterior SHA256=b4ecf886ac1412f1699fbe092f070fd24298a91e6cc4adaf1eaad121fd563843 | loteQAsetores2026-10-10T16:23:25.695297+00:00; exemplar anteriorSHA256=a5ad522f12419c30b630988c57f84d8e29c1ff154fa4a0a5e8c041297dd7a396
C:\laragon\www\portal-investidor\docs\investmoz\v2\auditoria-portal\legado-referencias.csv,809020,084d2b0ea0c118c9c68d38407108e9c08c34ae073424b8b04bdf9f89f31b47d7,reconciliação auditoriaQA,Legado inventariado/pilotoQA; cobertura histórica e decisões separadas explicitamente preservadas.,Relatório/inventário para consulta; não habilita importação/publicação ativa,Nenhum cálculo ativado,"Documento de programação/reconciliação, não fonte oficial", | atualizaçãoQA10/10/2026 | atualizaçãoQA10/10/2026
C:\laragon\www\portal-investidor\docs\investmoz\v2\auditoria-portal\legado-sitemap-anunciado.csv,5736407,8b35432d4320da391a6bb7dbcaca15485cd2b9c03d4286ad4489a0e028271273,reconciliação auditoriaQA,Legado inventariado/pilotoQA; cobertura histórica e decisões separadas explicitamente preservadas.,Relatório/inventário para consulta; não habilita importação/publicação ativa,Nenhum cálculo ativado,"Documento de programação/reconciliação, não fonte oficial", | atualizaçãoQA10/10/2026 | atualizaçãoQA10/10/2026
1 Exemplar/caminho Bytes SHA256 Tipo Nota de leitura Disponibilidade Uso em cálculo Situação jurídica/documental Edição/versão identificada
535 C:\laragon\www\portal-investidor\docs\investmoz\v2\consolidacao-variantes.md 7958 3defe7f46aeaa37da2f06f5c358da83f48ff4656566d4044bfe7cfb0ad50d5b2 original Documentação do repositório Nenhum cálculo ativado pela auditoria Situação documental não determinada por inventário
536 C:\laragon\www\portal-investidor\docs\investmoz\v2\ficheiros-primeira-entrega.csv 15338 b8c6eefb8fe879df7637bcda4aa4d458254edd82aceaa86991b5d885c046ee0a original Documentação do repositório Nenhum cálculo ativado pela auditoria Situação documental não determinada por inventário
537 C:\laragon\www\portal-investidor\docs\investmoz\v2\ficheiros-segunda-entrega.csv 3345 a7445854213ad91cf5a929237dc7c2ab2f2f682f5eb014cd10c4f9623ac30d20 original Documentação do repositório Nenhum cálculo ativado pela auditoria Situação documental não determinada por inventário
538 C:\laragon\www\portal-investidor\docs\investmoz\v2\gestao-oportunidades-dashboard.md 13951 58976 a12b57985e779180860983968922941bac9db3ab91aa4977d4ccf2b5b6d74997 a544573a042064c74cc1aaa3003cdecf82f9d17b5ff198956b634504b74ba183 original | [Visual QA 10/10/2026] Versão atual com provas visuais dirigidas. SHA anterior a12b57985e779180860983968922941bac9db3ab91aa4977d4ccf2b5b6d74997, bytes anteriores 13951; histórico preservado. | [Visual QA 10/10/2026] Versão atual com provas visuais dirigidas. SHA anterior b08f5b48263f4cca4aefeaaf6d59d6f1dfee185f2e0c57a8d4e00743588b0c6e, bytes anteriores 57819; histórico preservado. | [Visual QA 10/10/2026] Versão atual com provas visuais dirigidas. SHA anterior c54663ddf13b48235da8614d2b2f7c8b62bf801142d00d45642d65bcbbaba5fe, bytes anteriores 58005; histórico preservado. Documentação do repositório Nenhum cálculo ativado pela auditoria Situação documental não determinada por inventário
539 C:\laragon\www\portal-investidor\docs\investmoz\v2\indice-documento-mestre-atual.csv 151889 35ffb5d727f8ab1811ac946c2f89c2e95a74108b9a9cfbdd688a29fffee6080f original Documentação do repositório Nenhum cálculo ativado pela auditoria Situação documental não determinada por inventário
540 C:\laragon\www\portal-investidor\docs\investmoz\v2\indice-documento-mestre.csv 153757 4c13f1efd25a130c0d63e4a7f2efd9f9c2fbe9eb7ddee452534f06e8d7759bdd original Documentação do repositório Nenhum cálculo ativado pela auditoria Situação documental não determinada por inventário
541 C:\laragon\www\portal-investidor\docs\investmoz\v2\oportunidades-correspondencias.md 15378 3812fe47bf667a5323363cbfc7c0b329c1f717f4ea814a6e3c454e6624224a72 original Documentação do repositório Nenhum cálculo ativado pela auditoria Situação documental não determinada por inventário
590 C:\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\prompt\novos\Gestao de perfis investeMOZ.docx 46648 283a32f4c4b02bdfd9318a9174268224eeefac4349c49e3171545707bd222855 original Inventário complementar 10/10/2026; versões anteriores preservadas Acessível localmente; importação/publicação não inferida Nenhum cálculo ativado Situação jurídica/documental não inferida do inventário Exemplar identificado por SHA256
591 C:\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\prompt\novos\~WRL4044.tmp 46970 87fee09bb7e547d7aeebe7de984681fb52e588c74988da7ebe0ac72048e73a22 original Inventário complementar 10/10/2026; versões anteriores preservadas Acessível localmente; importação/publicação não inferida Nenhum cálculo ativado Situação jurídica/documental não inferida do inventário Exemplar identificado por SHA256
592 C:\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\prompt\novos\Gestao de perfis investeMOZ v1.docx 50215 f9e0f6280566fed8b52d0b36bb814a82e11976fec9ea9f62d64611ff8c824b38 original Inventário complementar 10/10/2026; versões anteriores preservadas Acessível localmente; importação/publicação não inferida Nenhum cálculo ativado Situação jurídica/documental não inferida do inventário Exemplar identificado por SHA256
593 C:\laragon\www\portal-investidor\docs\investmoz\v2\auditoria-portal\propostas-perfis-modulares-sla.md 17568 24585 1fa569e1c02d0e204f98e58252babefc2e7f0391265215e5e49e7566026620c2 20028b0abcb1235109668b6c36ef7e931777c5444334a776f967a7e1ee750c85 proposta interna QA reconciliação auditoriaQA Documento integral; regras/fundamento/efeito/decisões efonte§25.4. Não fonteoficial,nenhuma aprovaçãoinferida. Legado inventariado/pilotoQA; cobertura histórica e decisões separadas explicitamente preservadas. Consulta no repositório; publicação não inferida Relatório/inventário para consulta; não habilita importação/publicação ativa Nenhuma regra/SLA/cálculo ativado Nenhum cálculo ativado Proposta para decisão, não ato jurídico Documento de programação/reconciliação, não fonte oficial proposta20261010v1 proposta20261010v1 | exemplar anterior SHA256=a4549b18cad22eddeafdad7cb64f299c4200b01e439fb6a334714c19c7f0a588 | atualizaçãoQA10/10/2026 | atualizaçãoQA10/10/2026
594 C:\laragon\www\portal-investidor\docs\investmoz\v2\auditoria-portal\legado-piloto-qa.md 38405 8ffe19cc416804baa67487d3c7f2218b3ed218ca72c855c97f1c8bef65a8189a reconciliação auditoriaQA C71 recuperado e 22 originais QA importados/scaneados; 21 divulgados separadamente; 16 novos artigos e 4 dossiers demonstrados. Históricos/restrições preservados. | Nota anterior: Confirmação manual limitada e reconciliação dos quatro setores;22 candidatos privados, sem importação/publicação; C71/C37/A-B/móvel/acessibilidade/outros perfis preservados. | [Visual QA 10/10/2026] Versão atual com provas visuais dirigidas. SHA anterior c610372999be13f3df9d7683693adff1dbd8db4c9439cd2e951db2d294db4781, bytes anteriores 31191; histórico preservado. | [Visual QA 10/10/2026] Versão atual com provas visuais dirigidas. SHA anterior b2b25ef793d7b90b6b9a5295792a7d2415db3138851a7d50da9d090c84367938, bytes anteriores 37248; histórico preservado. | [Visual QA 10/10/2026] Versão atual com provas visuais dirigidas. SHA anterior 11ae88513acc709f80504bbf8f74200bafd47b3b910bdbc0eed7a921fcca1dc0, bytes anteriores 37434; histórico preservado. Relatório/inventário para consulta; não habilita importação/publicação ativa Nenhum cálculo ativado Documento de programação/reconciliação, não fonte oficial | atualizaçãoQA10/10/2026 | atualizaçãoQA10/10/2026 | exemplar anterior SHA256=2b23ac5f0e0117f53362dfaa756b083aabb5ddc0625a4a2c4c7acd8a06902467 | exemplar anterior SHA256=4561077d6ce5e112e2ccb5d04742a304f03fd4bc188dc0133b4833497427b85f | interfaceQA10/10/2026 | exemplar anterior SHA256=1ca20bb4036f8c73ea5a98a53d43964fe555427595d67df8f7f64c7e1ee591d3 | interface QA10/10/2026 | exemplar anterior SHA256=30c9e98b5615fbc9992d1522155c8556b0371b06618ec041f21812a81ea12477 | exemplar anterior SHA256=25c343e53febc02cf04455fef61f6dd6e9a959ca6a25b288ada0b154182accbe | exemplar anterior SHA256=87a11691519df7bb6c29e74ae525f0a7074c54bca3e3eed38799b37c5fa0bf1c | loteQAsetores2026-10-10T16:23:25.695297+00:00; exemplar anteriorSHA256=bb3d68ddac3e97a34246e20ba4cfd30f34afc523d2439be0ec38e83b64a6ad31 | revisão de legibilidade; exemplar anterior SHA256=c5ec30a05739fabfadf958398903ae9efe6cb50af8bbc1534dedde484aa5b989
595 C:\laragon\www\portal-investidor\docs\investmoz\v2\auditoria-portal\legado-inventario.csv 35375 56cc3058d6824b8535cbe8ea905c8764233849656c62af0eaa1c76275cf191e9 reconciliação auditoriaQA C71 recuperado e 22 originais QA importados/scaneados; 21 divulgados separadamente; 16 novos artigos e 4 dossiers demonstrados. Históricos/restrições preservados. | Nota anterior: Confirmação manual limitada e reconciliação dos quatro setores;22 candidatos privados, sem importação/publicação; C71/C37/A-B/móvel/acessibilidade/outros perfis preservados. Relatório/inventário para consulta; não habilita importação/publicação ativa Nenhum cálculo ativado Documento de programação/reconciliação, não fonte oficial | atualizaçãoQA10/10/2026 | atualizaçãoQA10/10/2026 | exemplar anterior SHA256=ff219009d137cd691fd08ad18135a7c039548141187334d9b9fdda806ea119b3 | loteQAsetores2026-10-10T16:23:25.695297+00:00; exemplar anteriorSHA256=74b33e590199880f9d04e77ce989aca63f30b92ceecc1cfbe172fa9f40784487
596 C:\laragon\www\portal-investidor\docs\investmoz\v2\auditoria-portal\legado-conteudos-reconciliados.csv 19973 02a352202d7e0bbe9e72ec7132b46489094facd4ef040f7074c8031f93ff7660 reconciliação auditoriaQA C71 recuperado e 22 originais QA importados/scaneados; 21 divulgados separadamente; 16 novos artigos e 4 dossiers demonstrados. Históricos/restrições preservados. | Nota anterior: Confirmação manual limitada e reconciliação dos quatro setores;22 candidatos privados, sem importação/publicação; C71/C37/A-B/móvel/acessibilidade/outros perfis preservados. Relatório/inventário para consulta; não habilita importação/publicação ativa Nenhum cálculo ativado Documento de programação/reconciliação, não fonte oficial | atualizaçãoQA10/10/2026 | atualizaçãoQA10/10/2026 | exemplar anterior SHA256=b4ecf886ac1412f1699fbe092f070fd24298a91e6cc4adaf1eaad121fd563843 | loteQAsetores2026-10-10T16:23:25.695297+00:00; exemplar anteriorSHA256=a5ad522f12419c30b630988c57f84d8e29c1ff154fa4a0a5e8c041297dd7a396
597 C:\laragon\www\portal-investidor\docs\investmoz\v2\auditoria-portal\legado-referencias.csv 809020 084d2b0ea0c118c9c68d38407108e9c08c34ae073424b8b04bdf9f89f31b47d7 reconciliação auditoriaQA Legado inventariado/pilotoQA; cobertura histórica e decisões separadas explicitamente preservadas. Relatório/inventário para consulta; não habilita importação/publicação ativa Nenhum cálculo ativado Documento de programação/reconciliação, não fonte oficial | atualizaçãoQA10/10/2026 | atualizaçãoQA10/10/2026
598 C:\laragon\www\portal-investidor\docs\investmoz\v2\auditoria-portal\legado-sitemap-anunciado.csv 5736407 8b35432d4320da391a6bb7dbcaca15485cd2b9c03d4286ad4489a0e028271273 reconciliação auditoriaQA Legado inventariado/pilotoQA; cobertura histórica e decisões separadas explicitamente preservadas. Relatório/inventário para consulta; não habilita importação/publicação ativa Nenhum cálculo ativado Documento de programação/reconciliação, não fonte oficial | atualizaçãoQA10/10/2026 | atualizaçãoQA10/10/2026
@@ -0,0 +1,51 @@
ID legado,Título,Autor,Categoria,Publicado,Modificado,SHA256 conteúdo,URLs observadas,Recurso QA,Situação
171985,Agronegocio,tempo,Oportunidades,2026-04-06T09:19:07+00:00,2026-04-20T12:41:43+00:00,28383a11d95f13894381357d8f1227b33031d1d3ff74507da4ea8e5ce2470913,https://investemoz.tempo.co.mz/agronegocio,6,recurso único QA; não é oportunidade
62413,15/30 costa do sol com dua na zona de mupulene muito lindo,tempo,Categoria,2017-06-26T11:44:21+00:00,2017-06-26T11:44:21+00:00,c04ae821317ef556c01c3141ce75eeeeb5da7787e0c6849b1b3709b757edb9eb,https://investemoz.tempo.co.mz/1530-costa-do-sol-com-dua-na-zona-de-mupulene-muito-lindo,,inventariado; cadastro/versão por reconciliar
173220,Alumínio “Verde”,tempo,Oportunidades,2026-06-16T12:41:02+00:00,2026-06-16T12:41:06+00:00,c61f05a56bb5a5f13d9d5d54dbefd270cf9ac40320917d77aaad76cac56c7b97,https://investemoz.tempo.co.mz/aluminio-verde,,inventariado; cadastro/versão por reconciliar
73945,Banco Mundial classifica de proibitivos juros praticados em Moçambique,tempo,Nas Redes,2017-07-31T19:55:24+00:00,2017-07-31T19:55:24+00:00,36ba82b8885b00a4ea2fd9e4b1d8cf304a8e78e591972682f0aa53717f648d19,https://investemoz.tempo.co.mz/banco-mundial-classifica-de-proibitivos-juros-praticados-em-mocambique,,inventariado; cadastro/versão por reconciliar
173213,Bens de Consumo e Franquias,tempo,Oportunidades,2026-06-16T11:31:06+00:00,2026-06-16T11:32:10+00:00,abd5f36a0c2b89d2989334137aae5d59b3158da38d556013ccd55deaa5069e92,https://investemoz.tempo.co.mz/bens-de-consumo-franquias,,inventariado; cadastro/versão por reconciliar
172223,"Cabo Delgado - Pemba, A capital do turismo costeiro do norte de Moçambique",tempo,Turismo,2026-04-14T10:21:20+00:00,2026-04-14T10:41:05+00:00,c96b2430c44b8edc9cc1d0a7974c23290dd0207cff366fea8326bb28bcab8285,https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique/ | https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique,20,RecursoQA20; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
173482,Checklist para Contratação e Direitos do Trabalho Doméstico,tempo,Trabalho Doméstico,2026-09-15T12:36:11+00:00,2026-09-15T12:36:14+00:00,309b51ebc53bf840f9014da733bc09f8d162138be4e6e9d5a72670e26cc8e482,https://investemoz.tempo.co.mz/checklist-para-contratacao-e-direitos-do-trabalho-domestico,,inventariado; cadastro/versão por reconciliar
76474,"Coloque o seu número para o grupo de whatsapp, venda de acessórios de computador",tempo,Categoria,2017-08-08T17:29:51+00:00,2017-08-08T17:29:51+00:00,c8beeb94f936b3bfc69c720770bd84376e9f19b668d96185aaefbada05727010,https://investemoz.tempo.co.mz/coloque-o-seu-numero-para-o-grupo-de-whatsapp-venda-de-acessorios-de-computador,,inventariado; cadastro/versão por reconciliar
173484,Direitos Especiais da Trabalhadora Doméstica no Regulamento,tempo,Trabalho Doméstico,2026-09-15T12:38:08+00:00,2026-09-15T12:38:11+00:00,34c31bb02a3ad0886f12ac2853283de2ffb89a925be3cda73b7ce9385c6caec4,https://investemoz.tempo.co.mz/direitos-especiais-da-trabalhadora-domestica-no-regulamento,,inventariado; cadastro/versão por reconciliar
171981,Energia & Mineração,tempo,Oportunidades,2026-04-06T09:17:46+00:00,2026-04-30T13:35:32+00:00,e9a95f0c8176eea48a75bd2c5bce69f15308806988fa5203a688af7c40305e9c,https://investemoz.tempo.co.mz/energia-mineracao,7,RecursoQA7; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
173222,Energia e Recursos Naturais,tempo,Oportunidades,2026-06-16T12:47:16+00:00,2026-06-16T12:51:30+00:00,6f8236b0441d61fce5171994ba6b82ffc8b5f2de3e8242de7be29248ed046ad2,https://investemoz.tempo.co.mz/energia-recursos-naturais,,inventariado; cadastro/versão por reconciliar
62414,Frelimo e Zanu-PF: Evocados laços de irmandade,tempo,Categoria,2017-06-26T12:03:20+00:00,2017-06-26T12:03:20+00:00,013307965b8fcb349a0d936d844ed12437bda4c90389f8efe958d4ffdb9ce62b,https://investemoz.tempo.co.mz/frelimo-e-zanu-pf-evocados-lacos-de-irmandade,,inventariado; cadastro/versão por reconciliar
62418,Governo dará todo O apoio à PGR no seguimento da auditoria,tempo,Categoria,2017-06-26T12:30:37+00:00,2017-06-26T12:30:45+00:00,65299b6711499e417be778b51c7ab7880616b0ccbf362854370eb775fc54caf3,https://investemoz.tempo.co.mz/governo-dara-todo-o-apoio-a-pgr-no-seguimento-da-auditoria,,inventariado; cadastro/versão por reconciliar
75002,Governo pretende construir 100 mil habitações em dois anos,tempo,Nas Redes,2017-08-04T20:01:55+00:00,2017-08-04T20:01:55+00:00,b32ea95039c2af387687738ed601f0947384eb7e19ca4bd282d603c60ca26afa,https://investemoz.tempo.co.mz/governo-pretende-construir-100-mil-habitacoes-em-dois-anos,,inventariado; cadastro/versão por reconciliar
173478,Guia Prático para Empregadores sobre Contratação e Direitos no Trabalho Doméstico,tempo,Trabalho Doméstico,2026-09-15T12:30:34+00:00,2026-09-15T12:30:37+00:00,8038d8c221ba3cbbc4712304a6b17a6bba8ad946c8bac1a7836d472944aaf930,https://investemoz.tempo.co.mz/guia-pratico-para-empregadores-sobre-contratacao-e-direitos-no-trabalho-domestico,,inventariado; cadastro/versão por reconciliar
173218,Imobiliário,tempo,Oportunidades,2026-06-16T11:34:45+00:00,2026-06-16T11:34:49+00:00,be8b606f087a939bd88a86f4436a0ff18c5fb774a179e6e915d4abb10a95f930,https://investemoz.tempo.co.mz/imobiliario,,inventariado; cadastro/versão por reconciliar
171992,Infraestruturas,tempo,Oportunidades,2026-04-06T09:25:07+00:00,2026-04-30T12:44:37+00:00,9716da16aba58eec173343f0bbd582d62d23a5eee7d20522c080c2da5da8ba66,https://investemoz.tempo.co.mz/infraestruturas,22,RecursoQA22; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
172240,"Maputo Cidade – Plataforma Estratégica de Turismo, Negócios e Investimento Sustentável",tempo,Turismo,2026-04-14T10:58:53+00:00,2026-04-14T11:03:28+00:00,0866639d09c5c0b3b00e8527554065587c793f60df978b66b6a578b3c741bc0d,https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel/ | https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel,16,RecursoQA16; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
171994,Mercado Financeiro,tempo,Oportunidades,2026-04-06T09:27:00+00:00,2026-04-17T09:53:42+00:00,e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,https://investemoz.tempo.co.mz/mercado-financeiro,23,"RecursoQA23; original/hash/ClamD/replay demonstrados. Corpo vazio na origemHTML/Inertia200;privado,sem página publicada."
173480,Modelo de Contrato de Trabalho Doméstico para Pequenas Famílias,tempo,Trabalho Doméstico,2026-09-15T12:34:17+00:00,2026-09-15T12:34:19+00:00,5b95ad9010fcbdc1000052f47766ca54409d3e0649211729ced9ef142f5c6db0,https://investemoz.tempo.co.mz/modelo-de-contrato-de-trabalho-domestico-para-pequenas-familias,,inventariado; cadastro/versão por reconciliar
172219,Niassa - O último grande destino virgem do turismo africano,tempo,Turismo,2026-04-14T10:17:14+00:00,2026-04-14T10:43:56+00:00,47d82a59a86c3cbdef04ea065e711e7139d50137e440f03efbd710e3c4220ef7,https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano/ | https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano,21,RecursoQA21; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
173476,Procedimentos para Rescisão do Contrato de Trabalho Doméstico segundo o Regulamento de Moçambique,tempo,Trabalho Doméstico,2026-09-15T11:39:21+00:00,2026-09-15T11:39:24+00:00,bb634a9021da9d7b46ed105317aa54e4af42a16dab042e0ceb2bdc0d702d1a67,https://investemoz.tempo.co.mz/procedimentos-para-rescisao-do-contrato-de-trabalho-domestico-segundo-o-regulamento-de-mocambique,,inventariado; cadastro/versão por reconciliar
172263,"Província da Zambézia: Onde os Corredores de Moçambique Convergem com a Natureza, a Cultura e o Turismo Sustentável",tempo,Turismo,2026-04-14T14:21:52+00:00,2026-04-14T14:26:27+00:00,9e7ca4e9542135d5969f5c008ce8e29b037cc06a53875cfa7dc3e7452ec47744,https://investemoz.tempo.co.mz/provincia-da-zambezia-onde-os-corredores-de-mocambique-convergem-com-a-natureza-a-cultura-e-o-turismo-sustentavel,11,RecursoQA11; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
172233,Província de Gaza: Oportunidades mais sólidas e estratégicas de investimento no turismo no sul de Moçambique.,tempo,Turismo,2026-04-14T10:52:30+00:00,2026-04-17T08:28:56+00:00,2bf2df9bd3b7ab8656209a48f4928b5d8bbf2959e77c292fc4c9987e67ba2fea,https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique/ | https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique,18,RecursoQA18; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
172253,Província de Inhambane: Um Destino Estratégico para Investimento Turístico,tempo,Turismo,2026-04-14T14:15:12+00:00,2026-04-14T14:25:44+00:00,2d1cc729481ce65a5e50898c0eea4a1eb2418273b03fc749009e2ffaaa72fe71,https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico/ | https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico,15,RecursoQA15; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
172258,"Província de Manica: Onde o Turismo de Montanha, Natureza e Cultura Encontra o Próximo Ciclo de Investimento",tempo,Turismo,2026-04-14T14:18:28+00:00,2026-04-17T10:42:16+00:00,4c8bdd79212ba98fa347733e2f58aef8796b29cc8cb8b649d200bd37435df1c3,https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento/ | https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento,14,RecursoQA14; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
172237,"Província de Maputo: Natureza, Cultura e Conectividade como Motores de Crescimento Sustentável",tempo,Turismo,2026-04-14T10:55:21+00:00,2026-04-17T08:27:24+00:00,d112d0e535912f4487c2b8d41978e5e663f1db43f1a2cdf170dbbec535f13f27,https://investemoz.tempo.co.mz/provincia-de-maputo-turismo/ | https://investemoz.tempo.co.mz/provincia-de-maputo-turismo,17,RecursoQA17; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
172230,Província de Nampula - Perfil de investimento no setor do turismo,tempo,Turismo,2026-04-14T10:50:32+00:00,2026-04-14T11:10:23+00:00,d18ccc1e036b3480605462ce7176be75153f6aa988bc9b37d8d395b01e555a7a,https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo/ | https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo,19,RecursoQA19; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
172256,"Província de Sofala: Onde a Natureza, a História e o Futuro do Turismo Convergem",tempo,Turismo,2026-04-14T14:16:47+00:00,2026-04-14T14:26:17+00:00,12b46a34ba61c6ef0058a6c410ebc17329d897fe4b06a13b4ea3fe3c85a28342,https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem/ | https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem,12,RecursoQA12; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
172261,"Província de Tete: Onde o Rio Zambeze, a Cultura Viva e a Conservação Comunitária Criam o Futuro do Turismo",tempo,Turismo,2026-04-14T14:19:48+00:00,2026-04-14T14:26:25+00:00,e78fce2931260a7d6f82270897e906feb640f65d084c2d4ffb5ba19303858275,https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo/ | https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo,13,RecursoQA13; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
73246,Rofino Licuco preso cinco meses depois de condenado por agressão,tempo,Nas Redes,2017-07-28T20:23:11+00:00,2017-07-28T20:23:11+00:00,806c54948641f4f5933a2ea4a6935dfb7a67202ab227960451d62435880289f6,https://investemoz.tempo.co.mz/rofino-licuco-preso-cinco-meses-depois-de-condenado-por-agressao,,inventariado; cadastro/versão por reconciliar
77232,Sete indivíduos mortos a tiro na Matola,tempo,Nas Redes,2017-08-11T20:30:32+00:00,2017-08-11T20:30:32+00:00,e657a97d5d6db7fa0ea6b7d14029ff873ea5d36dfdadda5f8b9bc8f1ef5dd011,https://investemoz.tempo.co.mz/sete-individuos-mortos-a-tiro-na-matola,,inventariado; cadastro/versão por reconciliar
62416,Sete pessoas morrem e 150 são resgatados em acidente com eléctrico na Índia,tempo,Categoria,2017-06-26T12:14:48+00:00,2017-06-26T12:14:48+00:00,0fe20a9ca9dd99c58072f43b3dece1de347ef4063ee5f75bcffe8b2f4bf253b5,https://investemoz.tempo.co.mz/sete-pessoas-morrem-e-150-sao-resgatados-em-acidente-com-electrico-na-india,,inventariado; cadastro/versão por reconciliar
173209,Fintech,tempo,Oportunidades,2026-06-16T11:24:22+00:00,2026-06-23T10:03:06+00:00,8db9206bf5192b68e7c9bdfb87e9dc9c7063a4b7eca56630685b0e6c294f55ab,https://investemoz.tempo.co.mz/tech-fintech,,inventariado; cadastro/versão por reconciliar
171990,Turismo,tempo,Oportunidades,2026-04-06T09:22:51+00:00,2026-06-16T13:07:59+00:00,604d30a740cc6ed20d24e3447806e733aa4aefb9f339f81d3dc57b644dd19fb7,https://investemoz.tempo.co.mz/turismo,10,RecursoQA10; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
74364,,tempo,Categoria,2017-07-26T21:20:33+00:00,2017-07-26T21:20:33+00:00,2a7f0a5b09e0136fec9eae357ac8a88a159ead23b25ed7bb735c75997b10ec5e,https://investemoz.tempo.co.mz/74364-2,,inventariado; cadastro/versão por reconciliar
62929,Daúde Razaque já não é treinador da Liga Desportiva de Maputo,tempo,Nas Redes,2017-06-27T16:41:43+00:00,2017-06-27T16:41:43+00:00,c3b57ea685290bfb01c50403bc83db98cf1a42ad51c04d5b8d288c99b8c0a808,https://investemoz.tempo.co.mz/daude-razaque-ja-nao-e-treinador-da-liga-desportiva-de-maputo,,inventariado; cadastro/versão por reconciliar
62649,Daviz Simango não se incomoda com possível sucessão na Beira,tempo,Nas Redes,2017-06-26T19:38:09+00:00,2017-06-26T19:38:09+00:00,090e5f41ed11c8e680d2e30ea5c8310b7c6b165ddec318a8b0cf94927f78da74,https://investemoz.tempo.co.mz/daviz-simango-nao-se-incomoda-com-possivel-sucessao-na-beira,,inventariado; cadastro/versão por reconciliar
173470,Direitos e Deveres do Trabalhador Doméstico,tempo,Trabalho Doméstico,2026-09-15T11:28:25+00:00,2026-09-15T11:28:28+00:00,f69fd6fd8509913f2d79df0267c469dc7a9d0d30420ce8428749d53ba21446ab,https://investemoz.tempo.co.mz/direitos-e-deveres-do-trabalhador-domestico,,inventariado; cadastro/versão por reconciliar
172679,Energia,tempo,Featured,2026-04-30T13:03:57+00:00,2026-04-30T13:28:48+00:00,e2a7eb4be0a2cf4139737f6428c147ac35de99899478ce79195dc5e1b0a960f3,https://investemoz.tempo.co.mz/energia | https://investemoz.tempo.co.mz/energia/,8,RecursoQA8; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
62739,"“Exigimos responsabilização dos agentes do crime”, Daviz Simango",tempo,Nas Redes,2017-06-26T23:13:36+00:00,2017-06-26T23:13:36+00:00,e37ea18176246dab529d2ecec7734f0fb2b0a54cadc507fbd789033d5b8e5b93,https://investemoz.tempo.co.mz/exigimos-responsabilizacao-dos-agentes-do-crime-daviz-simango-2,,inventariado; cadastro/versão por reconciliar
62854,Governo e parceiros reforçam mecanismos de controlo de HIV/Sida,tempo,Nas Redes,2017-06-27T11:07:53+00:00,2017-06-27T11:07:53+00:00,0073816b1fa4bd50ec6417b07272da06565230d06a19079ca76cb006c882d2f1,https://investemoz.tempo.co.mz/governo-e-parceiros-reforcam-mecanismos-de-controlo-de-hivsida,,inventariado; cadastro/versão por reconciliar
63128,Graça Machel diz que desnutrição é um problema de desenvolvimento,tempo,Nas Redes,2017-06-27T22:01:59+00:00,2017-06-27T22:01:59+00:00,711f3707e3a0d72ec919216fc50823de9060349104ee98fe0a6b20d6684b6031,https://investemoz.tempo.co.mz/graca-machel-diz-que-desnutricao-e-um-problema-de-desenvolvimento,,inventariado; cadastro/versão por reconciliar
63181,In dependência,tempo,Nas Redes,2017-06-27T21:38:06+00:00,2017-06-27T21:38:06+00:00,b0d87c0d58d6d1343d102cd3141bda1215c5afd54f3780a1c6c262d786dbfe27,https://investemoz.tempo.co.mz/in-dependencia,,inventariado; cadastro/versão por reconciliar
63146,"Kroll não descobriu como foram gastos 2 biliões de dólares; Governo de Nyusi não apoiou a Auditoria; criação da Proindicus, EMATUM e MAM não foi para proteção da Zona Económica Exclusiva",tempo,Nas Redes,2017-06-27T19:30:01+00:00,2017-06-27T19:30:01+00:00,4ccbad5c92ddc3884f58181a4f1ed3c1d3d6a59f005bfd99b197a6fc1fc3c280,https://investemoz.tempo.co.mz/kroll-nao-descobriu-como-foram-gastos-2-bilioes-de-dolares-governo-de-nyusi-nao-apoiou-a-auditoria-criacao-da-proindicus-ematum-e-mam-nao-foi-para-protecao-da-zona-economica-exclusiva-2,,inventariado; cadastro/versão por reconciliar
172674,Mineração,tempo,Featured,2026-04-30T13:12:09+00:00,2026-04-30T13:30:13+00:00,8d3acc292ed806bc4cc7bcb6a3a6a5759f30b610766a5ef4b3d6a0b7c56d5861,https://investemoz.tempo.co.mz/mineracao,9,RecursoQA9; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
62688,O Instituto Nacional de Meteorologia prevê o seguinte estado do tempo para esta ...,tempo,Nas Redes,2017-06-26T18:21:18+00:00,2017-06-26T18:21:18+00:00,396fe2575641622015505eb11d4c7152b6e3e1b741966f14c0cd1673fd455fd3,https://investemoz.tempo.co.mz/o-instituto-nacional-de-meteorologia-preve-o-seguinte-estado-do-tempo-para-esta-29,,inventariado; cadastro/versão por reconciliar
173474,Sanções Disciplinares Aplicáveis a Trabalhadores Domésticos segundo o Regulamento de Moçambique,tempo,Trabalho Doméstico,2026-09-15T11:37:04+00:00,2026-09-15T11:37:06+00:00,e09a0fe71397e2b060ff8435111117aa487b476a85ec522fffcdf9e23a6cdac2,https://investemoz.tempo.co.mz/sancoes-disciplinares-aplicaveis-a-trabalhadores-domesticos-segundo-o-regulamento-de-mocambique,,inventariado; cadastro/versão por reconciliar
63248,Dois alunos da 9ª classe caíram nas mãos da Polícia da República de Moçambique (...,tempo,Nas Redes,2017-06-28T11:30:01+00:00,2017-06-28T11:30:01+00:00,2220ca2c07d10f10b2bd6fd0f103ee733b83570d436c751c449c85b42df46a13,https://investemoz.tempo.co.mz/dois-alunos-da-9a-classe-cairam-nas-maos-da-policia-da-republica-de-mocambique,,inventariado; cadastro/versão por reconciliar
173463,Regulamento do Trabalho Doméstico,tempo,Trabalho Doméstico,2026-09-15T10:56:07+00:00,2026-09-15T12:41:44+00:00,eb43b6c1f097631250f0b36d3d01b4d106db2fcae07a56bb154819143fff11e9,https://investemoz.tempo.co.mz/regulamento-do-trabalho-domestico,,inventariado; cadastro/versão por reconciliar
1 ID legado Título Autor Categoria Publicado Modificado SHA256 conteúdo URLs observadas Recurso QA Situação
2 171985 Agronegocio tempo Oportunidades 2026-04-06T09:19:07+00:00 2026-04-20T12:41:43+00:00 28383a11d95f13894381357d8f1227b33031d1d3ff74507da4ea8e5ce2470913 https://investemoz.tempo.co.mz/agronegocio 6 recurso único QA; não é oportunidade
3 62413 15/30 costa do sol com dua na zona de mupulene muito lindo tempo Categoria 2017-06-26T11:44:21+00:00 2017-06-26T11:44:21+00:00 c04ae821317ef556c01c3141ce75eeeeb5da7787e0c6849b1b3709b757edb9eb https://investemoz.tempo.co.mz/1530-costa-do-sol-com-dua-na-zona-de-mupulene-muito-lindo inventariado; cadastro/versão por reconciliar
4 173220 Alumínio “Verde” tempo Oportunidades 2026-06-16T12:41:02+00:00 2026-06-16T12:41:06+00:00 c61f05a56bb5a5f13d9d5d54dbefd270cf9ac40320917d77aaad76cac56c7b97 https://investemoz.tempo.co.mz/aluminio-verde inventariado; cadastro/versão por reconciliar
5 73945 Banco Mundial classifica de proibitivos juros praticados em Moçambique tempo Nas Redes 2017-07-31T19:55:24+00:00 2017-07-31T19:55:24+00:00 36ba82b8885b00a4ea2fd9e4b1d8cf304a8e78e591972682f0aa53717f648d19 https://investemoz.tempo.co.mz/banco-mundial-classifica-de-proibitivos-juros-praticados-em-mocambique inventariado; cadastro/versão por reconciliar
6 173213 Bens de Consumo e Franquias tempo Oportunidades 2026-06-16T11:31:06+00:00 2026-06-16T11:32:10+00:00 abd5f36a0c2b89d2989334137aae5d59b3158da38d556013ccd55deaa5069e92 https://investemoz.tempo.co.mz/bens-de-consumo-franquias inventariado; cadastro/versão por reconciliar
7 172223 Cabo Delgado - Pemba, A capital do turismo costeiro do norte de Moçambique tempo Turismo 2026-04-14T10:21:20+00:00 2026-04-14T10:41:05+00:00 c96b2430c44b8edc9cc1d0a7974c23290dd0207cff366fea8326bb28bcab8285 https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique/ | https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique 20 RecursoQA20; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
8 173482 Checklist para Contratação e Direitos do Trabalho Doméstico tempo Trabalho Doméstico 2026-09-15T12:36:11+00:00 2026-09-15T12:36:14+00:00 309b51ebc53bf840f9014da733bc09f8d162138be4e6e9d5a72670e26cc8e482 https://investemoz.tempo.co.mz/checklist-para-contratacao-e-direitos-do-trabalho-domestico inventariado; cadastro/versão por reconciliar
9 76474 Coloque o seu número para o grupo de whatsapp, venda de acessórios de computador tempo Categoria 2017-08-08T17:29:51+00:00 2017-08-08T17:29:51+00:00 c8beeb94f936b3bfc69c720770bd84376e9f19b668d96185aaefbada05727010 https://investemoz.tempo.co.mz/coloque-o-seu-numero-para-o-grupo-de-whatsapp-venda-de-acessorios-de-computador inventariado; cadastro/versão por reconciliar
10 173484 Direitos Especiais da Trabalhadora Doméstica no Regulamento tempo Trabalho Doméstico 2026-09-15T12:38:08+00:00 2026-09-15T12:38:11+00:00 34c31bb02a3ad0886f12ac2853283de2ffb89a925be3cda73b7ce9385c6caec4 https://investemoz.tempo.co.mz/direitos-especiais-da-trabalhadora-domestica-no-regulamento inventariado; cadastro/versão por reconciliar
11 171981 Energia & Mineração tempo Oportunidades 2026-04-06T09:17:46+00:00 2026-04-30T13:35:32+00:00 e9a95f0c8176eea48a75bd2c5bce69f15308806988fa5203a688af7c40305e9c https://investemoz.tempo.co.mz/energia-mineracao 7 RecursoQA7; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
12 173222 Energia e Recursos Naturais tempo Oportunidades 2026-06-16T12:47:16+00:00 2026-06-16T12:51:30+00:00 6f8236b0441d61fce5171994ba6b82ffc8b5f2de3e8242de7be29248ed046ad2 https://investemoz.tempo.co.mz/energia-recursos-naturais inventariado; cadastro/versão por reconciliar
13 62414 Frelimo e Zanu-PF: Evocados laços de irmandade tempo Categoria 2017-06-26T12:03:20+00:00 2017-06-26T12:03:20+00:00 013307965b8fcb349a0d936d844ed12437bda4c90389f8efe958d4ffdb9ce62b https://investemoz.tempo.co.mz/frelimo-e-zanu-pf-evocados-lacos-de-irmandade inventariado; cadastro/versão por reconciliar
14 62418 Governo dará todo O apoio à PGR no seguimento da auditoria tempo Categoria 2017-06-26T12:30:37+00:00 2017-06-26T12:30:45+00:00 65299b6711499e417be778b51c7ab7880616b0ccbf362854370eb775fc54caf3 https://investemoz.tempo.co.mz/governo-dara-todo-o-apoio-a-pgr-no-seguimento-da-auditoria inventariado; cadastro/versão por reconciliar
15 75002 Governo pretende construir 100 mil habitações em dois anos tempo Nas Redes 2017-08-04T20:01:55+00:00 2017-08-04T20:01:55+00:00 b32ea95039c2af387687738ed601f0947384eb7e19ca4bd282d603c60ca26afa https://investemoz.tempo.co.mz/governo-pretende-construir-100-mil-habitacoes-em-dois-anos inventariado; cadastro/versão por reconciliar
16 173478 Guia Prático para Empregadores sobre Contratação e Direitos no Trabalho Doméstico tempo Trabalho Doméstico 2026-09-15T12:30:34+00:00 2026-09-15T12:30:37+00:00 8038d8c221ba3cbbc4712304a6b17a6bba8ad946c8bac1a7836d472944aaf930 https://investemoz.tempo.co.mz/guia-pratico-para-empregadores-sobre-contratacao-e-direitos-no-trabalho-domestico inventariado; cadastro/versão por reconciliar
17 173218 Imobiliário tempo Oportunidades 2026-06-16T11:34:45+00:00 2026-06-16T11:34:49+00:00 be8b606f087a939bd88a86f4436a0ff18c5fb774a179e6e915d4abb10a95f930 https://investemoz.tempo.co.mz/imobiliario inventariado; cadastro/versão por reconciliar
18 171992 Infraestruturas tempo Oportunidades 2026-04-06T09:25:07+00:00 2026-04-30T12:44:37+00:00 9716da16aba58eec173343f0bbd582d62d23a5eee7d20522c080c2da5da8ba66 https://investemoz.tempo.co.mz/infraestruturas 22 RecursoQA22; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
19 172240 Maputo Cidade – Plataforma Estratégica de Turismo, Negócios e Investimento Sustentável tempo Turismo 2026-04-14T10:58:53+00:00 2026-04-14T11:03:28+00:00 0866639d09c5c0b3b00e8527554065587c793f60df978b66b6a578b3c741bc0d https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel/ | https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel 16 RecursoQA16; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
20 171994 Mercado Financeiro tempo Oportunidades 2026-04-06T09:27:00+00:00 2026-04-17T09:53:42+00:00 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 https://investemoz.tempo.co.mz/mercado-financeiro 23 RecursoQA23; original/hash/ClamD/replay demonstrados. Corpo vazio na origemHTML/Inertia200;privado,sem página publicada.
21 173480 Modelo de Contrato de Trabalho Doméstico para Pequenas Famílias tempo Trabalho Doméstico 2026-09-15T12:34:17+00:00 2026-09-15T12:34:19+00:00 5b95ad9010fcbdc1000052f47766ca54409d3e0649211729ced9ef142f5c6db0 https://investemoz.tempo.co.mz/modelo-de-contrato-de-trabalho-domestico-para-pequenas-familias inventariado; cadastro/versão por reconciliar
22 172219 Niassa - O último grande destino virgem do turismo africano tempo Turismo 2026-04-14T10:17:14+00:00 2026-04-14T10:43:56+00:00 47d82a59a86c3cbdef04ea065e711e7139d50137e440f03efbd710e3c4220ef7 https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano/ | https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano 21 RecursoQA21; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
23 173476 Procedimentos para Rescisão do Contrato de Trabalho Doméstico segundo o Regulamento de Moçambique tempo Trabalho Doméstico 2026-09-15T11:39:21+00:00 2026-09-15T11:39:24+00:00 bb634a9021da9d7b46ed105317aa54e4af42a16dab042e0ceb2bdc0d702d1a67 https://investemoz.tempo.co.mz/procedimentos-para-rescisao-do-contrato-de-trabalho-domestico-segundo-o-regulamento-de-mocambique inventariado; cadastro/versão por reconciliar
24 172263 Província da Zambézia: Onde os Corredores de Moçambique Convergem com a Natureza, a Cultura e o Turismo Sustentável tempo Turismo 2026-04-14T14:21:52+00:00 2026-04-14T14:26:27+00:00 9e7ca4e9542135d5969f5c008ce8e29b037cc06a53875cfa7dc3e7452ec47744 https://investemoz.tempo.co.mz/provincia-da-zambezia-onde-os-corredores-de-mocambique-convergem-com-a-natureza-a-cultura-e-o-turismo-sustentavel 11 RecursoQA11; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
25 172233 Província de Gaza: Oportunidades mais sólidas e estratégicas de investimento no turismo no sul de Moçambique. tempo Turismo 2026-04-14T10:52:30+00:00 2026-04-17T08:28:56+00:00 2bf2df9bd3b7ab8656209a48f4928b5d8bbf2959e77c292fc4c9987e67ba2fea https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique/ | https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique 18 RecursoQA18; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
26 172253 Província de Inhambane: Um Destino Estratégico para Investimento Turístico tempo Turismo 2026-04-14T14:15:12+00:00 2026-04-14T14:25:44+00:00 2d1cc729481ce65a5e50898c0eea4a1eb2418273b03fc749009e2ffaaa72fe71 https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico/ | https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico 15 RecursoQA15; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
27 172258 Província de Manica: Onde o Turismo de Montanha, Natureza e Cultura Encontra o Próximo Ciclo de Investimento tempo Turismo 2026-04-14T14:18:28+00:00 2026-04-17T10:42:16+00:00 4c8bdd79212ba98fa347733e2f58aef8796b29cc8cb8b649d200bd37435df1c3 https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento/ | https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento 14 RecursoQA14; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
28 172237 Província de Maputo: Natureza, Cultura e Conectividade como Motores de Crescimento Sustentável tempo Turismo 2026-04-14T10:55:21+00:00 2026-04-17T08:27:24+00:00 d112d0e535912f4487c2b8d41978e5e663f1db43f1a2cdf170dbbec535f13f27 https://investemoz.tempo.co.mz/provincia-de-maputo-turismo/ | https://investemoz.tempo.co.mz/provincia-de-maputo-turismo 17 RecursoQA17; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
29 172230 Província de Nampula - Perfil de investimento no setor do turismo tempo Turismo 2026-04-14T10:50:32+00:00 2026-04-14T11:10:23+00:00 d18ccc1e036b3480605462ce7176be75153f6aa988bc9b37d8d395b01e555a7a https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo/ | https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo 19 RecursoQA19; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
30 172256 Província de Sofala: Onde a Natureza, a História e o Futuro do Turismo Convergem tempo Turismo 2026-04-14T14:16:47+00:00 2026-04-14T14:26:17+00:00 12b46a34ba61c6ef0058a6c410ebc17329d897fe4b06a13b4ea3fe3c85a28342 https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem/ | https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem 12 RecursoQA12; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
31 172261 Província de Tete: Onde o Rio Zambeze, a Cultura Viva e a Conservação Comunitária Criam o Futuro do Turismo tempo Turismo 2026-04-14T14:19:48+00:00 2026-04-14T14:26:25+00:00 e78fce2931260a7d6f82270897e906feb640f65d084c2d4ffb5ba19303858275 https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo/ | https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo 13 RecursoQA13; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
32 73246 Rofino Licuco preso cinco meses depois de condenado por agressão tempo Nas Redes 2017-07-28T20:23:11+00:00 2017-07-28T20:23:11+00:00 806c54948641f4f5933a2ea4a6935dfb7a67202ab227960451d62435880289f6 https://investemoz.tempo.co.mz/rofino-licuco-preso-cinco-meses-depois-de-condenado-por-agressao inventariado; cadastro/versão por reconciliar
33 77232 Sete indivíduos mortos a tiro na Matola tempo Nas Redes 2017-08-11T20:30:32+00:00 2017-08-11T20:30:32+00:00 e657a97d5d6db7fa0ea6b7d14029ff873ea5d36dfdadda5f8b9bc8f1ef5dd011 https://investemoz.tempo.co.mz/sete-individuos-mortos-a-tiro-na-matola inventariado; cadastro/versão por reconciliar
34 62416 Sete pessoas morrem e 150 são resgatados em acidente com eléctrico na Índia tempo Categoria 2017-06-26T12:14:48+00:00 2017-06-26T12:14:48+00:00 0fe20a9ca9dd99c58072f43b3dece1de347ef4063ee5f75bcffe8b2f4bf253b5 https://investemoz.tempo.co.mz/sete-pessoas-morrem-e-150-sao-resgatados-em-acidente-com-electrico-na-india inventariado; cadastro/versão por reconciliar
35 173209 Fintech tempo Oportunidades 2026-06-16T11:24:22+00:00 2026-06-23T10:03:06+00:00 8db9206bf5192b68e7c9bdfb87e9dc9c7063a4b7eca56630685b0e6c294f55ab https://investemoz.tempo.co.mz/tech-fintech inventariado; cadastro/versão por reconciliar
36 171990 Turismo tempo Oportunidades 2026-04-06T09:22:51+00:00 2026-06-16T13:07:59+00:00 604d30a740cc6ed20d24e3447806e733aa4aefb9f339f81d3dc57b644dd19fb7 https://investemoz.tempo.co.mz/turismo 10 RecursoQA10; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
37 74364 tempo Categoria 2017-07-26T21:20:33+00:00 2017-07-26T21:20:33+00:00 2a7f0a5b09e0136fec9eae357ac8a88a159ead23b25ed7bb735c75997b10ec5e https://investemoz.tempo.co.mz/74364-2 inventariado; cadastro/versão por reconciliar
38 62929 Daúde Razaque já não é treinador da Liga Desportiva de Maputo tempo Nas Redes 2017-06-27T16:41:43+00:00 2017-06-27T16:41:43+00:00 c3b57ea685290bfb01c50403bc83db98cf1a42ad51c04d5b8d288c99b8c0a808 https://investemoz.tempo.co.mz/daude-razaque-ja-nao-e-treinador-da-liga-desportiva-de-maputo inventariado; cadastro/versão por reconciliar
39 62649 Daviz Simango não se incomoda com possível sucessão na Beira tempo Nas Redes 2017-06-26T19:38:09+00:00 2017-06-26T19:38:09+00:00 090e5f41ed11c8e680d2e30ea5c8310b7c6b165ddec318a8b0cf94927f78da74 https://investemoz.tempo.co.mz/daviz-simango-nao-se-incomoda-com-possivel-sucessao-na-beira inventariado; cadastro/versão por reconciliar
40 173470 Direitos e Deveres do Trabalhador Doméstico tempo Trabalho Doméstico 2026-09-15T11:28:25+00:00 2026-09-15T11:28:28+00:00 f69fd6fd8509913f2d79df0267c469dc7a9d0d30420ce8428749d53ba21446ab https://investemoz.tempo.co.mz/direitos-e-deveres-do-trabalhador-domestico inventariado; cadastro/versão por reconciliar
41 172679 Energia tempo Featured 2026-04-30T13:03:57+00:00 2026-04-30T13:28:48+00:00 e2a7eb4be0a2cf4139737f6428c147ac35de99899478ce79195dc5e1b0a960f3 https://investemoz.tempo.co.mz/energia | https://investemoz.tempo.co.mz/energia/ 8 RecursoQA8; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
42 62739 “Exigimos responsabilização dos agentes do crime”, Daviz Simango tempo Nas Redes 2017-06-26T23:13:36+00:00 2017-06-26T23:13:36+00:00 e37ea18176246dab529d2ecec7734f0fb2b0a54cadc507fbd789033d5b8e5b93 https://investemoz.tempo.co.mz/exigimos-responsabilizacao-dos-agentes-do-crime-daviz-simango-2 inventariado; cadastro/versão por reconciliar
43 62854 Governo e parceiros reforçam mecanismos de controlo de HIV/Sida tempo Nas Redes 2017-06-27T11:07:53+00:00 2017-06-27T11:07:53+00:00 0073816b1fa4bd50ec6417b07272da06565230d06a19079ca76cb006c882d2f1 https://investemoz.tempo.co.mz/governo-e-parceiros-reforcam-mecanismos-de-controlo-de-hivsida inventariado; cadastro/versão por reconciliar
44 63128 Graça Machel diz que desnutrição é um problema de desenvolvimento tempo Nas Redes 2017-06-27T22:01:59+00:00 2017-06-27T22:01:59+00:00 711f3707e3a0d72ec919216fc50823de9060349104ee98fe0a6b20d6684b6031 https://investemoz.tempo.co.mz/graca-machel-diz-que-desnutricao-e-um-problema-de-desenvolvimento inventariado; cadastro/versão por reconciliar
45 63181 In dependência tempo Nas Redes 2017-06-27T21:38:06+00:00 2017-06-27T21:38:06+00:00 b0d87c0d58d6d1343d102cd3141bda1215c5afd54f3780a1c6c262d786dbfe27 https://investemoz.tempo.co.mz/in-dependencia inventariado; cadastro/versão por reconciliar
46 63146 Kroll não descobriu como foram gastos 2 biliões de dólares; Governo de Nyusi não apoiou a Auditoria; criação da Proindicus, EMATUM e MAM não foi para proteção da Zona Económica Exclusiva tempo Nas Redes 2017-06-27T19:30:01+00:00 2017-06-27T19:30:01+00:00 4ccbad5c92ddc3884f58181a4f1ed3c1d3d6a59f005bfd99b197a6fc1fc3c280 https://investemoz.tempo.co.mz/kroll-nao-descobriu-como-foram-gastos-2-bilioes-de-dolares-governo-de-nyusi-nao-apoiou-a-auditoria-criacao-da-proindicus-ematum-e-mam-nao-foi-para-protecao-da-zona-economica-exclusiva-2 inventariado; cadastro/versão por reconciliar
47 172674 Mineração tempo Featured 2026-04-30T13:12:09+00:00 2026-04-30T13:30:13+00:00 8d3acc292ed806bc4cc7bcb6a3a6a5759f30b610766a5ef4b3d6a0b7c56d5861 https://investemoz.tempo.co.mz/mineracao 9 RecursoQA9; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
48 62688 O Instituto Nacional de Meteorologia prevê o seguinte estado do tempo para esta ... tempo Nas Redes 2017-06-26T18:21:18+00:00 2017-06-26T18:21:18+00:00 396fe2575641622015505eb11d4c7152b6e3e1b741966f14c0cd1673fd455fd3 https://investemoz.tempo.co.mz/o-instituto-nacional-de-meteorologia-preve-o-seguinte-estado-do-tempo-para-esta-29 inventariado; cadastro/versão por reconciliar
49 173474 Sanções Disciplinares Aplicáveis a Trabalhadores Domésticos segundo o Regulamento de Moçambique tempo Trabalho Doméstico 2026-09-15T11:37:04+00:00 2026-09-15T11:37:06+00:00 e09a0fe71397e2b060ff8435111117aa487b476a85ec522fffcdf9e23a6cdac2 https://investemoz.tempo.co.mz/sancoes-disciplinares-aplicaveis-a-trabalhadores-domesticos-segundo-o-regulamento-de-mocambique inventariado; cadastro/versão por reconciliar
50 63248 Dois alunos da 9ª classe caíram nas mãos da Polícia da República de Moçambique (... tempo Nas Redes 2017-06-28T11:30:01+00:00 2017-06-28T11:30:01+00:00 2220ca2c07d10f10b2bd6fd0f103ee733b83570d436c751c449c85b42df46a13 https://investemoz.tempo.co.mz/dois-alunos-da-9a-classe-cairam-nas-maos-da-policia-da-republica-de-mocambique inventariado; cadastro/versão por reconciliar
51 173463 Regulamento do Trabalho Doméstico tempo Trabalho Doméstico 2026-09-15T10:56:07+00:00 2026-09-15T12:41:44+00:00 eb43b6c1f097631250f0b36d3d01b4d106db2fcae07a56bb154819143fff11e9 https://investemoz.tempo.co.mz/regulamento-do-trabalho-domestico inventariado; cadastro/versão por reconciliar
@@ -0,0 +1,93 @@
URL,HTTP,Tipo,Título,ID legado,Autor,Categoria legado,Publicado,Modificado,Bytes,SHA256,Destino proposto,Situação
https://investemoz.tempo.co.mz/,200,text/html,Informacao Publica,,,,,,8368,42a2f14ca41b028045e9f2b2bb70fa324feb811698d9223f913b0b002a309f89,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/agronegocio,200,text/html,Agronegocio,171985,tempo,Oportunidades,2026-04-06T09:19:07+00:00,2026-04-20T12:41:43+00:00,88030,0bcd03e0e73cdf9b0f1a3a686689ae1a5ed80e990dcc34c77c9627c32f9d156f,/agronegocio,piloto preparado
https://investemoz.tempo.co.mz/portal-do-investidor,200,text/html,Informacao Publica,,,,,,8368,f62485a612c9c8cc5765758614fdbcaafdd77e1d7eb35139c67045f40e638689,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/robots.txt,200,text/plain,,,,,,,153,70743fc5a6acd78ea3083b92220bbd775e9c37642843fa16b7e353726e839228,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/sitemap.xml,200,text/xml,,,,,,,7606912,8470ee4038c9d4b1d722edda9044d71ad3afdfd1817c9b41680d0b910d77d011,biblioteca / correspondência por rever,inventariado; não importado
https://contentmanager.tempo.co.mz/wp-content/uploads/2026/04/Agronegocio.png,200,image/png,,,,,,,550888,5e3840705cf98226d1f189cf665aaa98cb05da96c645159a1332a1d429d642eb,biblioteca / correspondência por rever,piloto preparado
https://contentmanager.tempo.co.mz/wp-content/uploads/2026/04/Energia-Mineracao.png,200,image/png,,,,,,,534405,0a2c50d91dad3a6007544634540e0f61b11d3506682a5a19066bae03cdcba490,biblioteca existente recursoQA24;URLs/versão originais preservados,RecursoQA24; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://contentmanager.tempo.co.mz/wp-content/uploads/2026/04/Infraestruturas.png,200,image/png,,,,,,,363037,8e58544e49e719f9c865c91e07ef1030155a9e3d59260e6dc93e9c9e25674e39,biblioteca existente recursoQA27;URLs/versão originais preservados,RecursoQA27; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://contentmanager.tempo.co.mz/wp-content/uploads/2026/04/Mercado-Financeiro.png,200,image/png,,,,,,,708105,417b4a33468f360acef272ce3e4180c1d0a171980befa73ddb9a33de5a9d4c4e,biblioteca existente recursoQA28;URLs/versão originais preservados,RecursoQA28; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://contentmanager.tempo.co.mz/wp-content/uploads/2026/04/Turismo.png,200,image/png,,,,,,,575317,fe9a2e382e385b35f6e61ae320e199a979678677fd327fb7d4b07d8ce0111c11,biblioteca existente recursoQA26;URLs/versão originais preservados,RecursoQA26; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz,200,text/html,Informacao Publica,,,,,,8368,db5c258c7a10d2d9ec91d378c153e1da53de4eef1dfa168b2dc666ba9aea36fc,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/1530-costa-do-sol-com-dua-na-zona-de-mupulene-muito-lindo,200,text/html,15/30 costa do sol com dua na zona de mupulene muito lindo,62413,tempo,Categoria,2017-06-26T11:44:21+00:00,2017-06-26T11:44:21+00:00,46443,b9b6634f98240994fdd6a1d9f41012f9336e0a32e43a939710113802e0eca948,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/aluminio-verde,200,text/html,Alumínio “Verde”,173220,tempo,Oportunidades,2026-06-16T12:41:02+00:00,2026-06-16T12:41:06+00:00,88028,b6667ab0ed4cb3eca5ba7d9e09b829697a84ac1338d94025eada726b0b84addd,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/apple-touch-icon.png,200,image/png,,,,,,,1662,4001aa032ff113e1a268a9bbf1ab0fd9949439f9f54a85895956eb323aba977d,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/banco-mundial-classifica-de-proibitivos-juros-praticados-em-mocambique,200,text/html,Banco Mundial classifica de proibitivos juros praticados em Moçambique,73945,tempo,Nas Redes,2017-07-31T19:55:24+00:00,2017-07-31T19:55:24+00:00,63817,3b4535a181412bc95ef25725d32e997479328d6096f80046f95ca567233b1ce7,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/bens-de-consumo-franquias,200,text/html,Bens de Consumo e Franquias,173213,tempo,Oportunidades,2026-06-16T11:31:06+00:00,2026-06-16T11:32:10+00:00,88150,c7287adeaf11d2d8a40b0dc6a7b35e83ca0e2f312e2ac314caa01f166c08e3f8,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/build/assets/app-5tyHq2tj.js,200,application/javascript,,,,,,,76449,befcbe0252f450f664935a51f077030a30ae5955487f863990f204822bb7da1b,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/build/assets/app-CChIcWcY.js,200,application/javascript,,,,,,,115,81538671d6a533184b030e29c12d953c5dc584fd27e57ded6a576634e439f077,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/build/assets/app-CshAeK4J.css,200,text/css,,,,,,,219921,4c5addbbbb41752b8d8b17e1f3545ec461ec2631af24891940d3afa7347ccb91,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/build/assets/vendor-core-CDlfZzJQ.js,200,application/javascript,,,,,,,460473,fd4fdef257763f052e34a444b691d7322f49425e61e36d5fb83da8f0c340538b,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/build/assets/vendor-i18n-BY-fwNFQ.js,200,application/javascript,,,,,,,53387,7e87c342349f3e03d817f42e5f4da03a53f924839bc3f491d3cdf09d9c7f5886,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique/,200,text/html,"Cabo Delgado - Pemba, A capital do turismo costeiro do norte de Moçambique",172223,tempo,Turismo,2026-04-14T10:21:20+00:00,2026-04-14T10:41:05+00:00,123385,2ea7aa8588d4055b7033c66197d819e48716fcbf9520dd39ffe1713ef75a22c2,biblioteca existente recursoQA20;URLs/versão originais preservados,RecursoQA20; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/cdn-cgi/challenge-platform/scripts/jsd/main.js,200,application/javascript,,,,,,,22552,37f21e386bbbbaf80c7d98a667bbe049a55acfaebc7902fb88e56d03d5064e1d,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/checklist-para-contratacao-e-direitos-do-trabalho-domestico,200,text/html,Checklist para Contratação e Direitos do Trabalho Doméstico,173482,tempo,Trabalho Doméstico,2026-09-15T12:36:11+00:00,2026-09-15T12:36:14+00:00,86191,5e68de9c525c7779772fd5eb6f6e9e0a36ab93ca06ca4f2a686e2f0afa6482e2,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/coloque-o-seu-numero-para-o-grupo-de-whatsapp-venda-de-acessorios-de-computador,200,text/html,"Coloque o seu número para o grupo de whatsapp, venda de acessórios de computador",76474,tempo,Categoria,2017-08-08T17:29:51+00:00,2017-08-08T17:29:51+00:00,44204,d1c7794dd9ef8b223bad657753162b6a5a6f64ce52870b05440e5d3ec961fb52,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/direitos-especiais-da-trabalhadora-domestica-no-regulamento,200,text/html,Direitos Especiais da Trabalhadora Doméstica no Regulamento,173484,tempo,Trabalho Doméstico,2026-09-15T12:38:08+00:00,2026-09-15T12:38:11+00:00,86424,b7aeb6d6143a2925a220bb899af2cac8363c542d0f1f337b72191f4994793aa6,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/energia-mineracao,200,text/html,Energia & Mineração,171981,tempo,Oportunidades,2026-04-06T09:17:46+00:00,2026-04-30T13:35:32+00:00,60048,f0d3fb494743c7dbec389fd3650754402135ceb6f2293ae6cef00fd0af93cbad,biblioteca existente recursoQA7;URLs/versão originais preservados,RecursoQA7; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/energia-recursos-naturais,200,text/html,Energia e Recursos Naturais,173222,tempo,Oportunidades,2026-06-16T12:47:16+00:00,2026-06-16T12:51:30+00:00,88150,718e69adfa50a11de8aeda60209afc0cbff1fd0c2be4bcb18ff59c43abed356a,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/favicon.ico,200,image/x-icon,,,,,,,15406,87f9c3a47246509b030080e505c103b48e93f3451ff84f0af38daa9164ce5356,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/favicon.svg,200,image/svg+xml,,,,,,,3545,242f4f8f93f5fbc6c8aeec500c9bac02dcfe68daba166d484c5cdc986c88d8ee,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/frelimo-e-zanu-pf-evocados-lacos-de-irmandade,200,text/html,Frelimo e Zanu-PF: Evocados laços de irmandade,62414,tempo,Categoria,2017-06-26T12:03:20+00:00,2017-06-26T12:03:20+00:00,47408,6b87be9796d5f1f2707ccce29f6dfc76a4c9ce0511f2e91b51333f2f799676e6,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/governo-dara-todo-o-apoio-a-pgr-no-seguimento-da-auditoria,200,text/html,Governo dará todo O apoio à PGR no seguimento da auditoria,62418,tempo,Categoria,2017-06-26T12:30:37+00:00,2017-06-26T12:30:45+00:00,49598,b771655708e07cecb4da3fba87c9474c5e5eb5c47ed2e8d8b5fba2cbc99440d6,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/governo-pretende-construir-100-mil-habitacoes-em-dois-anos,200,text/html,Governo pretende construir 100 mil habitações em dois anos,75002,tempo,Nas Redes,2017-08-04T20:01:55+00:00,2017-08-04T20:01:55+00:00,63563,3dbfba8e5a5df2b6337f623b45927031861d0843c12fd13109191fe74a1be790,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/guia-pratico-para-empregadores-sobre-contratacao-e-direitos-no-trabalho-domestico,200,text/html,Guia Prático para Empregadores sobre Contratação e Direitos no Trabalho Doméstico,173478,tempo,Trabalho Doméstico,2026-09-15T12:30:34+00:00,2026-09-15T12:30:37+00:00,84088,de793ec332602fbf9bd6310a4d13be43b6a2064d5adc03913a5c27069f2b50fd,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/images/android-chrome-512x512.png,200,image/png,,,,,,,15124,5cf3fb02d85cd40d2e88fb75749070fc981a9346c6419b2f9910e2f7f6961121,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/images/tempo-background.png,200,image/png,,,,,,,10636,28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa,biblioteca existente recursoQA25;URLs/versão originais preservados,RecursoQA25; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/imobiliario,200,text/html,Imobiliário,173218,tempo,Oportunidades,2026-06-16T11:34:45+00:00,2026-06-16T11:34:49+00:00,87978,c044ba5246951dfd1fff8d47db000dc6eba2e29b98b900922ac1cce9ed3467e6,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/infraestruturas,200,text/html,Infraestruturas,171992,tempo,Oportunidades,2026-04-06T09:25:07+00:00,2026-04-30T12:44:37+00:00,88068,fd5df9e47e8e427ca0ce2e558d3fed29ff1ab0a2e5886779fb375329e84d77a2,biblioteca existente recursoQA22;URLs/versão originais preservados,RecursoQA22; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel/,200,text/html,"Maputo Cidade – Plataforma Estratégica de Turismo, Negócios e Investimento Sustentável",172240,tempo,Turismo,2026-04-14T10:58:53+00:00,2026-04-14T11:03:28+00:00,123599,2d5d06531402bab1dd276669b78cce500cb903f6e540f886b832f6856f0d35cc,biblioteca existente recursoQA16;URLs/versão originais preservados,RecursoQA16; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/mercado-financeiro,200,text/html,Mercado Financeiro,171994,tempo,Oportunidades,2026-04-06T09:27:00+00:00,2026-04-17T09:53:42+00:00,87723,3cf0ac4e7acc897d842272465ffc2dd12967cb5484884eccd9107dd8375c3060,biblioteca existente recursoQA23;URLs/versão originais preservados,"RecursoQA23; original/hash/ClamD/replay demonstrados. Corpo vazio na origemHTML/Inertia200;privado,sem página publicada."
https://investemoz.tempo.co.mz/modelo-de-contrato-de-trabalho-domestico-para-pequenas-familias,200,text/html,Modelo de Contrato de Trabalho Doméstico para Pequenas Famílias,173480,tempo,Trabalho Doméstico,2026-09-15T12:34:17+00:00,2026-09-15T12:34:19+00:00,86922,93f3910de0a5ff8f3752a7d2fe73e0201d05b717cd4b37380cd584ab9ec087eb,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano/,200,text/html,Niassa - O último grande destino virgem do turismo africano,172219,tempo,Turismo,2026-04-14T10:17:14+00:00,2026-04-14T10:43:56+00:00,126058,cad44af1c05e0d76579dbab7112b35d18938b80156fb58f4580e583fc905976f,biblioteca existente recursoQA21;URLs/versão originais preservados,RecursoQA21; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/procedimentos-para-rescisao-do-contrato-de-trabalho-domestico-segundo-o-regulamento-de-mocambique,200,text/html,Procedimentos para Rescisão do Contrato de Trabalho Doméstico segundo o Regulamento de Moçambique,173476,tempo,Trabalho Doméstico,2026-09-15T11:39:21+00:00,2026-09-15T11:39:24+00:00,85371,a76a8c9f6ef4b039a9349f52e8d2f0ce06020ce8101d2736e3c6e0b2e76c7d58,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/provincia-da-zambezia-onde-os-corredores-de-mocambique-convergem-com-a-natureza-a-cultura-e-o-turismo-sustentavel,200,text/html,"Província da Zambézia: Onde os Corredores de Moçambique Convergem com a Natureza, a Cultura e o Turismo Sustentável",172263,tempo,Turismo,2026-04-14T14:21:52+00:00,2026-04-14T14:26:27+00:00,124167,e871558095c9d08329651084994266f61afd64eb72ecf85befdd2dbe099c1915,biblioteca existente recursoQA11;URLs/versão originais preservados,RecursoQA11; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique/,200,text/html,Província de Gaza: Oportunidades mais sólidas e estratégicas de investimento no turismo no sul de Moçambique.,172233,tempo,Turismo,2026-04-14T10:52:30+00:00,2026-04-17T08:28:56+00:00,123875,baf3c60fa77742c9b88a67937f9269c95284f3d5d371319009c9af2bdb6be140,biblioteca existente recursoQA18;URLs/versão originais preservados,RecursoQA18; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico/,200,text/html,Província de Inhambane: Um Destino Estratégico para Investimento Turístico,172253,tempo,Turismo,2026-04-14T14:15:12+00:00,2026-04-14T14:25:44+00:00,123445,35829f3b9725650ad51ad7b84f149ddde2157202c17083b1faec4212f5305df6,biblioteca existente recursoQA15;URLs/versão originais preservados,RecursoQA15; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento/,200,text/html,"Província de Manica: Onde o Turismo de Montanha, Natureza e Cultura Encontra o Próximo Ciclo de Investimento",172258,tempo,Turismo,2026-04-14T14:18:28+00:00,2026-04-17T10:42:16+00:00,123841,f1ffc7cb73f68643e8ca5883c187bc7b8bb125d9f907ae190d1503d0877c9968,biblioteca existente recursoQA14;URLs/versão originais preservados,RecursoQA14; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/provincia-de-maputo-turismo/,200,text/html,"Província de Maputo: Natureza, Cultura e Conectividade como Motores de Crescimento Sustentável",172237,tempo,Turismo,2026-04-14T10:55:21+00:00,2026-04-17T08:27:24+00:00,123013,71887a836c75fc67b80a6a83be2a2af23b93acbd5aa353f42c75f598544ac46a,biblioteca existente recursoQA17;URLs/versão originais preservados,RecursoQA17; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo/,200,text/html,Província de Nampula - Perfil de investimento no setor do turismo,172230,tempo,Turismo,2026-04-14T10:50:32+00:00,2026-04-14T11:10:23+00:00,123337,148faca42ca44da6017fbd46e23de27fd497d36f9f8b1b695e0ea3f35474fcbb,biblioteca existente recursoQA19;URLs/versão originais preservados,RecursoQA19; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem/,200,text/html,"Província de Sofala: Onde a Natureza, a História e o Futuro do Turismo Convergem",172256,tempo,Turismo,2026-04-14T14:16:47+00:00,2026-04-14T14:26:17+00:00,123519,5100b90cb6c67fb36ce171ebf04c6801fe3ead214bcff26f942f6946fcd3add0,biblioteca existente recursoQA12;URLs/versão originais preservados,RecursoQA12; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo/,200,text/html,"Província de Tete: Onde o Rio Zambeze, a Cultura Viva e a Conservação Comunitária Criam o Futuro do Turismo",172261,tempo,Turismo,2026-04-14T14:19:48+00:00,2026-04-14T14:26:25+00:00,123849,8e28ccbeed95208d8766bfbc57b5b4b398d5787ea5aac922daa59e596ca58511,biblioteca existente recursoQA13;URLs/versão originais preservados,RecursoQA13; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/rofino-licuco-preso-cinco-meses-depois-de-condenado-por-agressao,200,text/html,Rofino Licuco preso cinco meses depois de condenado por agressão,73246,tempo,Nas Redes,2017-07-28T20:23:11+00:00,2017-07-28T20:23:11+00:00,63685,0b8053aa5c5a8ee29648394dd7e0a84dd0eb095776c5f3d88b3b59c47f1ae233,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/sete-individuos-mortos-a-tiro-na-matola,200,text/html,Sete indivíduos mortos a tiro na Matola,77232,tempo,Nas Redes,2017-08-11T20:30:32+00:00,2017-08-11T20:30:32+00:00,63135,f903b084f891f13467121990c3deccd2b995c036e9eaa9124ebe53dc2d04c081,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/sete-pessoas-morrem-e-150-sao-resgatados-em-acidente-com-electrico-na-india,200,text/html,Sete pessoas morrem e 150 são resgatados em acidente com eléctrico na Índia,62416,tempo,Categoria,2017-06-26T12:14:48+00:00,2017-06-26T12:14:48+00:00,45244,a193cf19595a6e3de307d0307c3c96bc819d5075678e91ac9ebc5681185f4a89,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/tech-fintech,200,text/html,Fintech,173209,tempo,Oportunidades,2026-06-16T11:24:22+00:00,2026-06-23T10:03:06+00:00,88010,3b3b525c5d7ef34b5b9a1023aea5e6bc70c619672b5d173e6d9fe767782ab2d3,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/turismo,200,text/html,Turismo,171990,tempo,Oportunidades,2026-04-06T09:22:51+00:00,2026-06-16T13:07:59+00:00,87984,b7b4b2f9d9c4fc3a993e97ecdccb48f70ec9f560e13b15a9aad7205d88d22126,biblioteca existente recursoQA10;URLs/versão originais preservados,RecursoQA10; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/74364-2,200,text/html,,74364,tempo,Categoria,2017-07-26T21:20:33+00:00,2017-07-26T21:20:33+00:00,43595,2da21e3e51ccfe3a095710c85c0f549f0d995f2a2cc29bdccb1bebdee10ba805,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique,200,text/html,"Cabo Delgado - Pemba, A capital do turismo costeiro do norte de Moçambique",172223,tempo,Turismo,2026-04-14T10:21:20+00:00,2026-04-14T10:41:05+00:00,123383,99a2d46fed61caf9c8313ec3a695e15608bd478177d3048c3bda7fa2281039e8,biblioteca existente recursoQA20;URLs/versão originais preservados,RecursoQA20; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/daude-razaque-ja-nao-e-treinador-da-liga-desportiva-de-maputo,200,text/html,Daúde Razaque já não é treinador da Liga Desportiva de Maputo,62929,tempo,Nas Redes,2017-06-27T16:41:43+00:00,2017-06-27T16:41:43+00:00,64169,80f4f83caa75dcef3df39d697bc9d7afd77468caea640f2bbdeda70960ef8f5d,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/daviz-simango-nao-se-incomoda-com-possivel-sucessao-na-beira,200,text/html,Daviz Simango não se incomoda com possível sucessão na Beira,62649,tempo,Nas Redes,2017-06-26T19:38:09+00:00,2017-06-26T19:38:09+00:00,64147,16981acbf74f0b85ae8abb4d4853aac8c05a68795b59913370672a4a57c9f1a1,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/direitos-e-deveres-do-trabalhador-domestico,200,text/html,Direitos e Deveres do Trabalhador Doméstico,173470,tempo,Trabalho Doméstico,2026-09-15T11:28:25+00:00,2026-09-15T11:28:28+00:00,86045,1821b7404c7761bd3ca1600fe1b0be0e63c98dc64800c30a3b228bca8dfe7ee3,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/energia,200,text/html,Energia,172679,tempo,Featured,2026-04-30T13:03:57+00:00,2026-04-30T13:28:48+00:00,59488,fe0f7fe20a980bee1f96277e25b2536eaa9941733d63fd39c8f018adcae0a6e8,biblioteca existente recursoQA8;URLs/versão originais preservados,RecursoQA8; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/energia/,200,text/html,Energia,172679,tempo,Featured,2026-04-30T13:03:57+00:00,2026-04-30T13:28:48+00:00,59490,69b5a62220e4f47037cc0c13c74a7c0c9d6b8845b260b12a317099ce4e560339,biblioteca existente recursoQA8;URLs/versão originais preservados,RecursoQA8; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/exigimos-responsabilizacao-dos-agentes-do-crime-daviz-simango-2,200,text/html,"“Exigimos responsabilização dos agentes do crime”, Daviz Simango",62739,tempo,Nas Redes,2017-06-26T23:13:36+00:00,2017-06-26T23:13:36+00:00,64225,8eb9a45e56e65febc657c4aba0ddcce77239a5d9c59902a25717a09f4572b5f5,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/governo-e-parceiros-reforcam-mecanismos-de-controlo-de-hivsida,200,text/html,Governo e parceiros reforçam mecanismos de controlo de HIV/Sida,62854,tempo,Nas Redes,2017-06-27T11:07:53+00:00,2017-06-27T11:07:53+00:00,64139,0bb91025f1790b9571ad761351ed2fbf52b4d7ee7f33c7e4bc7aee2e9c1c90b1,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/graca-machel-diz-que-desnutricao-e-um-problema-de-desenvolvimento,200,text/html,Graça Machel diz que desnutrição é um problema de desenvolvimento,63128,tempo,Nas Redes,2017-06-27T22:01:59+00:00,2017-06-27T22:01:59+00:00,64267,4c7c9a780d23a580b9f229b2a965d0f6145e5950c8d56a963047d720905a09e4,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/images/ANO-2017/JUNHO/21/GOVERNADOR-in.gif,404,,,,,,,,,,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/in-dependencia,200,text/html,In dependência,63181,tempo,Nas Redes,2017-06-27T21:38:06+00:00,2017-06-27T21:38:06+00:00,63079,ec3bc03878819dfb5af3c6268b4a812010d177924489ac49190351ac0b2f5bd4,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/kroll-nao-descobriu-como-foram-gastos-2-bilioes-de-dolares-governo-de-nyusi-nao-apoiou-a-auditoria-criacao-da-proindicus-ematum-e-mam-nao-foi-para-protecao-da-zona-economica-exclusiva-2,200,text/html,"Kroll não descobriu como foram gastos 2 biliões de dólares; Governo de Nyusi não apoiou a Auditoria; criação da Proindicus, EMATUM e MAM não foi para proteção da Zona Económica Exclusiva",63146,tempo,Nas Redes,2017-06-27T19:30:01+00:00,2017-06-27T19:30:01+00:00,66959,9cf92d04143df86c4cd86bf0a372696da3463b71239e4b4d737d355ff09e7e76,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel,200,text/html,"Maputo Cidade – Plataforma Estratégica de Turismo, Negócios e Investimento Sustentável",172240,tempo,Turismo,2026-04-14T10:58:53+00:00,2026-04-14T11:03:28+00:00,123597,4a4a88ac0c6fdeb26bdfe7183c9b618075ceb1460875ace0b351e96a2f1dd9e9,biblioteca existente recursoQA16;URLs/versão originais preservados,RecursoQA16; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/mineracao,200,text/html,Mineração,172674,tempo,Featured,2026-04-30T13:12:09+00:00,2026-04-30T13:30:13+00:00,104140,803652cd4358d0b65a364a91ef916f6c5cbfe6bb539867e3d94b6ca36fbf10c7,biblioteca existente recursoQA9;URLs/versão originais preservados,RecursoQA9; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano,200,text/html,Niassa - O último grande destino virgem do turismo africano,172219,tempo,Turismo,2026-04-14T10:17:14+00:00,2026-04-14T10:43:56+00:00,126056,c19557c9937180420d7ae52d28fbc3aea51e1229eefe2e05a33f576f98bcd2d0,biblioteca existente recursoQA21;URLs/versão originais preservados,RecursoQA21; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/o-instituto-nacional-de-meteorologia-preve-o-seguinte-estado-do-tempo-para-esta-29,200,text/html,O Instituto Nacional de Meteorologia prevê o seguinte estado do tempo para esta ...,62688,tempo,Nas Redes,2017-06-26T18:21:18+00:00,2017-06-26T18:21:18+00:00,64583,b1de67ebe5fefc505086bad0469295e41bf50f6f7aba7fe9e27f30220a14071f,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique,200,text/html,Província de Gaza: Oportunidades mais sólidas e estratégicas de investimento no turismo no sul de Moçambique.,172233,tempo,Turismo,2026-04-14T10:52:30+00:00,2026-04-17T08:28:56+00:00,123873,e2835df9992914631c34a5e3006e86277ddf135a0fc264f6ecf3811f778ac60d,biblioteca existente recursoQA18;URLs/versão originais preservados,RecursoQA18; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico,200,text/html,Província de Inhambane: Um Destino Estratégico para Investimento Turístico,172253,tempo,Turismo,2026-04-14T14:15:12+00:00,2026-04-14T14:25:44+00:00,123443,919876fd899936071aa7a56a76c357ae30370b29014db2b1e7837450a099f0bd,biblioteca existente recursoQA15;URLs/versão originais preservados,RecursoQA15; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento,200,text/html,"Província de Manica: Onde o Turismo de Montanha, Natureza e Cultura Encontra o Próximo Ciclo de Investimento",172258,tempo,Turismo,2026-04-14T14:18:28+00:00,2026-04-17T10:42:16+00:00,123839,37b9eebf01ccf49ff3832755aa91b2595bbb80729b7b7cc861558c556d3f4cb7,biblioteca existente recursoQA14;URLs/versão originais preservados,RecursoQA14; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/provincia-de-maputo-turismo,200,text/html,"Província de Maputo: Natureza, Cultura e Conectividade como Motores de Crescimento Sustentável",172237,tempo,Turismo,2026-04-14T10:55:21+00:00,2026-04-17T08:27:24+00:00,123011,e6bbe002132682c1bbcf5cf7666dcc78dc18915c92bf298a5540db6c07935a23,biblioteca existente recursoQA17;URLs/versão originais preservados,RecursoQA17; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo,200,text/html,Província de Nampula - Perfil de investimento no setor do turismo,172230,tempo,Turismo,2026-04-14T10:50:32+00:00,2026-04-14T11:10:23+00:00,123335,704d23e4c838df4249894b550f087eb36b90797d61d26de82b829c8d2b7d444f,biblioteca existente recursoQA19;URLs/versão originais preservados,RecursoQA19; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem,200,text/html,"Província de Sofala: Onde a Natureza, a História e o Futuro do Turismo Convergem",172256,tempo,Turismo,2026-04-14T14:16:47+00:00,2026-04-14T14:26:17+00:00,123517,cc0d8bead12f2e42af60cdf10fb2412756e67c2c03ddc8d91c621e31e921933c,biblioteca existente recursoQA12;URLs/versão originais preservados,RecursoQA12; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo,200,text/html,"Província de Tete: Onde o Rio Zambeze, a Cultura Viva e a Conservação Comunitária Criam o Futuro do Turismo",172261,tempo,Turismo,2026-04-14T14:19:48+00:00,2026-04-14T14:26:25+00:00,123847,70ca06168749124e2eaae340b1396ccbe183a1e5dfd348dfd93575d9b3519205,biblioteca existente recursoQA13;URLs/versão originais preservados,RecursoQA13; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
https://investemoz.tempo.co.mz/sancoes-disciplinares-aplicaveis-a-trabalhadores-domesticos-segundo-o-regulamento-de-mocambique,200,text/html,Sanções Disciplinares Aplicáveis a Trabalhadores Domésticos segundo o Regulamento de Moçambique,173474,tempo,Trabalho Doméstico,2026-09-15T11:37:04+00:00,2026-09-15T11:37:06+00:00,86561,6af9a928c826dd6a756310c0f948cb0db8c1b8bd98ca15577faba528ba6f3cf3,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/dois-alunos-da-9a-classe-cairam-nas-maos-da-policia-da-republica-de-mocambique,200,text/html,Dois alunos da 9ª classe caíram nas mãos da Polícia da República de Moçambique (...,63248,tempo,Nas Redes,2017-06-28T11:30:01+00:00,2017-06-28T11:30:01+00:00,64573,be57946f86277610a1597e73eaedeaf55941de76aaef3f8eda77e3a92672a6f0,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/regulamento-do-trabalho-domestico,200,text/html,Regulamento do Trabalho Doméstico,173463,tempo,Trabalho Doméstico,2026-09-15T10:56:07+00:00,2026-09-15T12:41:44+00:00,93646,6cafa4cc6a5a327de0ca4c5d98fce5e10bf7152c1dc987aa42350034fd747e1c,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/build/assets/welcome-DyF43GB5.js,200,application/javascript,,,,,,,19506,c94067f98b9d24cc457a20ed759f6beec1dbf9675354299f6141ed6056e95ab0,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/assets/logos/1_COR.png,200,image/png,,,,,,,25390,73c30a98a87960fc388d33aaf9ed3a15c3123338f8e82e84c3538adb6d816c9c,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/images/hero/hero-1.png,200,image/png,,,,,,,2315768,3c9246169025b9b1b4d1d10fb51c1f644123984bde56c6c2d9bf869d851024fc,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/images/entidades/APIEX.jpg,200,image/jpeg,,,,,,,240665,e04e4da3a1478b34a14e1d92dbebdea5bdf9bfd645c942d785b184d63dc58c48,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/images/entidades/BM.png,200,image/png,,,,,,,369795,3a8deddcf90f2376e8fe28447724739ce57da64b26e6887ddade566871deeea6,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/images/entidades/BM.jpg,200,image/jpeg,,,,,,,15681,bb93f6bb51104d4fe9d0776a79bbf3b2978f31c6802884e6f0fc3d50056268ff,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/images/entidades/bvm.jpg,200,image/jpeg,,,,,,,296992,8d3e532137896c9319bab4422e836bd1232fa81babf4626efa8c411927052731,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/images/entidades/BMM.png,200,image/png,,,,,,,199364,a92c6ed17cf8769d97a0e70c5db31cc6de1b583a71e161b8b35a7eae21468122,biblioteca / correspondência por rever,inventariado; não importado
https://investemoz.tempo.co.mz/images/hero/hero-3.jpg,200,image/jpeg,,,,,,,9875137,91377213b6286aca7ee4af90860a7dc7a75fa08a165ef7b3192081db43f92dff,biblioteca / correspondência por rever,inventariado; não importado
1 URL HTTP Tipo Título ID legado Autor Categoria legado Publicado Modificado Bytes SHA256 Destino proposto Situação
2 https://investemoz.tempo.co.mz/ 200 text/html Informacao Publica 8368 42a2f14ca41b028045e9f2b2bb70fa324feb811698d9223f913b0b002a309f89 biblioteca / correspondência por rever inventariado; não importado
3 https://investemoz.tempo.co.mz/agronegocio 200 text/html Agronegocio 171985 tempo Oportunidades 2026-04-06T09:19:07+00:00 2026-04-20T12:41:43+00:00 88030 0bcd03e0e73cdf9b0f1a3a686689ae1a5ed80e990dcc34c77c9627c32f9d156f /agronegocio piloto preparado
4 https://investemoz.tempo.co.mz/portal-do-investidor 200 text/html Informacao Publica 8368 f62485a612c9c8cc5765758614fdbcaafdd77e1d7eb35139c67045f40e638689 biblioteca / correspondência por rever inventariado; não importado
5 https://investemoz.tempo.co.mz/robots.txt 200 text/plain 153 70743fc5a6acd78ea3083b92220bbd775e9c37642843fa16b7e353726e839228 biblioteca / correspondência por rever inventariado; não importado
6 https://investemoz.tempo.co.mz/sitemap.xml 200 text/xml 7606912 8470ee4038c9d4b1d722edda9044d71ad3afdfd1817c9b41680d0b910d77d011 biblioteca / correspondência por rever inventariado; não importado
7 https://contentmanager.tempo.co.mz/wp-content/uploads/2026/04/Agronegocio.png 200 image/png 550888 5e3840705cf98226d1f189cf665aaa98cb05da96c645159a1332a1d429d642eb biblioteca / correspondência por rever piloto preparado
8 https://contentmanager.tempo.co.mz/wp-content/uploads/2026/04/Energia-Mineracao.png 200 image/png 534405 0a2c50d91dad3a6007544634540e0f61b11d3506682a5a19066bae03cdcba490 biblioteca existente recursoQA24;URLs/versão originais preservados RecursoQA24; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
9 https://contentmanager.tempo.co.mz/wp-content/uploads/2026/04/Infraestruturas.png 200 image/png 363037 8e58544e49e719f9c865c91e07ef1030155a9e3d59260e6dc93e9c9e25674e39 biblioteca existente recursoQA27;URLs/versão originais preservados RecursoQA27; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
10 https://contentmanager.tempo.co.mz/wp-content/uploads/2026/04/Mercado-Financeiro.png 200 image/png 708105 417b4a33468f360acef272ce3e4180c1d0a171980befa73ddb9a33de5a9d4c4e biblioteca existente recursoQA28;URLs/versão originais preservados RecursoQA28; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
11 https://contentmanager.tempo.co.mz/wp-content/uploads/2026/04/Turismo.png 200 image/png 575317 fe9a2e382e385b35f6e61ae320e199a979678677fd327fb7d4b07d8ce0111c11 biblioteca existente recursoQA26;URLs/versão originais preservados RecursoQA26; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
12 https://investemoz.tempo.co.mz 200 text/html Informacao Publica 8368 db5c258c7a10d2d9ec91d378c153e1da53de4eef1dfa168b2dc666ba9aea36fc biblioteca / correspondência por rever inventariado; não importado
13 https://investemoz.tempo.co.mz/1530-costa-do-sol-com-dua-na-zona-de-mupulene-muito-lindo 200 text/html 15/30 costa do sol com dua na zona de mupulene muito lindo 62413 tempo Categoria 2017-06-26T11:44:21+00:00 2017-06-26T11:44:21+00:00 46443 b9b6634f98240994fdd6a1d9f41012f9336e0a32e43a939710113802e0eca948 biblioteca / correspondência por rever inventariado; não importado
14 https://investemoz.tempo.co.mz/aluminio-verde 200 text/html Alumínio “Verde” 173220 tempo Oportunidades 2026-06-16T12:41:02+00:00 2026-06-16T12:41:06+00:00 88028 b6667ab0ed4cb3eca5ba7d9e09b829697a84ac1338d94025eada726b0b84addd biblioteca / correspondência por rever inventariado; não importado
15 https://investemoz.tempo.co.mz/apple-touch-icon.png 200 image/png 1662 4001aa032ff113e1a268a9bbf1ab0fd9949439f9f54a85895956eb323aba977d biblioteca / correspondência por rever inventariado; não importado
16 https://investemoz.tempo.co.mz/banco-mundial-classifica-de-proibitivos-juros-praticados-em-mocambique 200 text/html Banco Mundial classifica de proibitivos juros praticados em Moçambique 73945 tempo Nas Redes 2017-07-31T19:55:24+00:00 2017-07-31T19:55:24+00:00 63817 3b4535a181412bc95ef25725d32e997479328d6096f80046f95ca567233b1ce7 biblioteca / correspondência por rever inventariado; não importado
17 https://investemoz.tempo.co.mz/bens-de-consumo-franquias 200 text/html Bens de Consumo e Franquias 173213 tempo Oportunidades 2026-06-16T11:31:06+00:00 2026-06-16T11:32:10+00:00 88150 c7287adeaf11d2d8a40b0dc6a7b35e83ca0e2f312e2ac314caa01f166c08e3f8 biblioteca / correspondência por rever inventariado; não importado
18 https://investemoz.tempo.co.mz/build/assets/app-5tyHq2tj.js 200 application/javascript 76449 befcbe0252f450f664935a51f077030a30ae5955487f863990f204822bb7da1b biblioteca / correspondência por rever inventariado; não importado
19 https://investemoz.tempo.co.mz/build/assets/app-CChIcWcY.js 200 application/javascript 115 81538671d6a533184b030e29c12d953c5dc584fd27e57ded6a576634e439f077 biblioteca / correspondência por rever inventariado; não importado
20 https://investemoz.tempo.co.mz/build/assets/app-CshAeK4J.css 200 text/css 219921 4c5addbbbb41752b8d8b17e1f3545ec461ec2631af24891940d3afa7347ccb91 biblioteca / correspondência por rever inventariado; não importado
21 https://investemoz.tempo.co.mz/build/assets/vendor-core-CDlfZzJQ.js 200 application/javascript 460473 fd4fdef257763f052e34a444b691d7322f49425e61e36d5fb83da8f0c340538b biblioteca / correspondência por rever inventariado; não importado
22 https://investemoz.tempo.co.mz/build/assets/vendor-i18n-BY-fwNFQ.js 200 application/javascript 53387 7e87c342349f3e03d817f42e5f4da03a53f924839bc3f491d3cdf09d9c7f5886 biblioteca / correspondência por rever inventariado; não importado
23 https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique/ 200 text/html Cabo Delgado - Pemba, A capital do turismo costeiro do norte de Moçambique 172223 tempo Turismo 2026-04-14T10:21:20+00:00 2026-04-14T10:41:05+00:00 123385 2ea7aa8588d4055b7033c66197d819e48716fcbf9520dd39ffe1713ef75a22c2 biblioteca existente recursoQA20;URLs/versão originais preservados RecursoQA20; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
24 https://investemoz.tempo.co.mz/cdn-cgi/challenge-platform/scripts/jsd/main.js 200 application/javascript 22552 37f21e386bbbbaf80c7d98a667bbe049a55acfaebc7902fb88e56d03d5064e1d biblioteca / correspondência por rever inventariado; não importado
25 https://investemoz.tempo.co.mz/checklist-para-contratacao-e-direitos-do-trabalho-domestico 200 text/html Checklist para Contratação e Direitos do Trabalho Doméstico 173482 tempo Trabalho Doméstico 2026-09-15T12:36:11+00:00 2026-09-15T12:36:14+00:00 86191 5e68de9c525c7779772fd5eb6f6e9e0a36ab93ca06ca4f2a686e2f0afa6482e2 biblioteca / correspondência por rever inventariado; não importado
26 https://investemoz.tempo.co.mz/coloque-o-seu-numero-para-o-grupo-de-whatsapp-venda-de-acessorios-de-computador 200 text/html Coloque o seu número para o grupo de whatsapp, venda de acessórios de computador 76474 tempo Categoria 2017-08-08T17:29:51+00:00 2017-08-08T17:29:51+00:00 44204 d1c7794dd9ef8b223bad657753162b6a5a6f64ce52870b05440e5d3ec961fb52 biblioteca / correspondência por rever inventariado; não importado
27 https://investemoz.tempo.co.mz/direitos-especiais-da-trabalhadora-domestica-no-regulamento 200 text/html Direitos Especiais da Trabalhadora Doméstica no Regulamento 173484 tempo Trabalho Doméstico 2026-09-15T12:38:08+00:00 2026-09-15T12:38:11+00:00 86424 b7aeb6d6143a2925a220bb899af2cac8363c542d0f1f337b72191f4994793aa6 biblioteca / correspondência por rever inventariado; não importado
28 https://investemoz.tempo.co.mz/energia-mineracao 200 text/html Energia & Mineração 171981 tempo Oportunidades 2026-04-06T09:17:46+00:00 2026-04-30T13:35:32+00:00 60048 f0d3fb494743c7dbec389fd3650754402135ceb6f2293ae6cef00fd0af93cbad biblioteca existente recursoQA7;URLs/versão originais preservados RecursoQA7; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
29 https://investemoz.tempo.co.mz/energia-recursos-naturais 200 text/html Energia e Recursos Naturais 173222 tempo Oportunidades 2026-06-16T12:47:16+00:00 2026-06-16T12:51:30+00:00 88150 718e69adfa50a11de8aeda60209afc0cbff1fd0c2be4bcb18ff59c43abed356a biblioteca / correspondência por rever inventariado; não importado
30 https://investemoz.tempo.co.mz/favicon.ico 200 image/x-icon 15406 87f9c3a47246509b030080e505c103b48e93f3451ff84f0af38daa9164ce5356 biblioteca / correspondência por rever inventariado; não importado
31 https://investemoz.tempo.co.mz/favicon.svg 200 image/svg+xml 3545 242f4f8f93f5fbc6c8aeec500c9bac02dcfe68daba166d484c5cdc986c88d8ee biblioteca / correspondência por rever inventariado; não importado
32 https://investemoz.tempo.co.mz/frelimo-e-zanu-pf-evocados-lacos-de-irmandade 200 text/html Frelimo e Zanu-PF: Evocados laços de irmandade 62414 tempo Categoria 2017-06-26T12:03:20+00:00 2017-06-26T12:03:20+00:00 47408 6b87be9796d5f1f2707ccce29f6dfc76a4c9ce0511f2e91b51333f2f799676e6 biblioteca / correspondência por rever inventariado; não importado
33 https://investemoz.tempo.co.mz/governo-dara-todo-o-apoio-a-pgr-no-seguimento-da-auditoria 200 text/html Governo dará todo O apoio à PGR no seguimento da auditoria 62418 tempo Categoria 2017-06-26T12:30:37+00:00 2017-06-26T12:30:45+00:00 49598 b771655708e07cecb4da3fba87c9474c5e5eb5c47ed2e8d8b5fba2cbc99440d6 biblioteca / correspondência por rever inventariado; não importado
34 https://investemoz.tempo.co.mz/governo-pretende-construir-100-mil-habitacoes-em-dois-anos 200 text/html Governo pretende construir 100 mil habitações em dois anos 75002 tempo Nas Redes 2017-08-04T20:01:55+00:00 2017-08-04T20:01:55+00:00 63563 3dbfba8e5a5df2b6337f623b45927031861d0843c12fd13109191fe74a1be790 biblioteca / correspondência por rever inventariado; não importado
35 https://investemoz.tempo.co.mz/guia-pratico-para-empregadores-sobre-contratacao-e-direitos-no-trabalho-domestico 200 text/html Guia Prático para Empregadores sobre Contratação e Direitos no Trabalho Doméstico 173478 tempo Trabalho Doméstico 2026-09-15T12:30:34+00:00 2026-09-15T12:30:37+00:00 84088 de793ec332602fbf9bd6310a4d13be43b6a2064d5adc03913a5c27069f2b50fd biblioteca / correspondência por rever inventariado; não importado
36 https://investemoz.tempo.co.mz/images/android-chrome-512x512.png 200 image/png 15124 5cf3fb02d85cd40d2e88fb75749070fc981a9346c6419b2f9910e2f7f6961121 biblioteca / correspondência por rever inventariado; não importado
37 https://investemoz.tempo.co.mz/images/tempo-background.png 200 image/png 10636 28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa biblioteca existente recursoQA25;URLs/versão originais preservados RecursoQA25; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
38 https://investemoz.tempo.co.mz/imobiliario 200 text/html Imobiliário 173218 tempo Oportunidades 2026-06-16T11:34:45+00:00 2026-06-16T11:34:49+00:00 87978 c044ba5246951dfd1fff8d47db000dc6eba2e29b98b900922ac1cce9ed3467e6 biblioteca / correspondência por rever inventariado; não importado
39 https://investemoz.tempo.co.mz/infraestruturas 200 text/html Infraestruturas 171992 tempo Oportunidades 2026-04-06T09:25:07+00:00 2026-04-30T12:44:37+00:00 88068 fd5df9e47e8e427ca0ce2e558d3fed29ff1ab0a2e5886779fb375329e84d77a2 biblioteca existente recursoQA22;URLs/versão originais preservados RecursoQA22; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
40 https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel/ 200 text/html Maputo Cidade – Plataforma Estratégica de Turismo, Negócios e Investimento Sustentável 172240 tempo Turismo 2026-04-14T10:58:53+00:00 2026-04-14T11:03:28+00:00 123599 2d5d06531402bab1dd276669b78cce500cb903f6e540f886b832f6856f0d35cc biblioteca existente recursoQA16;URLs/versão originais preservados RecursoQA16; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
41 https://investemoz.tempo.co.mz/mercado-financeiro 200 text/html Mercado Financeiro 171994 tempo Oportunidades 2026-04-06T09:27:00+00:00 2026-04-17T09:53:42+00:00 87723 3cf0ac4e7acc897d842272465ffc2dd12967cb5484884eccd9107dd8375c3060 biblioteca existente recursoQA23;URLs/versão originais preservados RecursoQA23; original/hash/ClamD/replay demonstrados. Corpo vazio na origemHTML/Inertia200;privado,sem página publicada.
42 https://investemoz.tempo.co.mz/modelo-de-contrato-de-trabalho-domestico-para-pequenas-familias 200 text/html Modelo de Contrato de Trabalho Doméstico para Pequenas Famílias 173480 tempo Trabalho Doméstico 2026-09-15T12:34:17+00:00 2026-09-15T12:34:19+00:00 86922 93f3910de0a5ff8f3752a7d2fe73e0201d05b717cd4b37380cd584ab9ec087eb biblioteca / correspondência por rever inventariado; não importado
43 https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano/ 200 text/html Niassa - O último grande destino virgem do turismo africano 172219 tempo Turismo 2026-04-14T10:17:14+00:00 2026-04-14T10:43:56+00:00 126058 cad44af1c05e0d76579dbab7112b35d18938b80156fb58f4580e583fc905976f biblioteca existente recursoQA21;URLs/versão originais preservados RecursoQA21; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
44 https://investemoz.tempo.co.mz/procedimentos-para-rescisao-do-contrato-de-trabalho-domestico-segundo-o-regulamento-de-mocambique 200 text/html Procedimentos para Rescisão do Contrato de Trabalho Doméstico segundo o Regulamento de Moçambique 173476 tempo Trabalho Doméstico 2026-09-15T11:39:21+00:00 2026-09-15T11:39:24+00:00 85371 a76a8c9f6ef4b039a9349f52e8d2f0ce06020ce8101d2736e3c6e0b2e76c7d58 biblioteca / correspondência por rever inventariado; não importado
45 https://investemoz.tempo.co.mz/provincia-da-zambezia-onde-os-corredores-de-mocambique-convergem-com-a-natureza-a-cultura-e-o-turismo-sustentavel 200 text/html Província da Zambézia: Onde os Corredores de Moçambique Convergem com a Natureza, a Cultura e o Turismo Sustentável 172263 tempo Turismo 2026-04-14T14:21:52+00:00 2026-04-14T14:26:27+00:00 124167 e871558095c9d08329651084994266f61afd64eb72ecf85befdd2dbe099c1915 biblioteca existente recursoQA11;URLs/versão originais preservados RecursoQA11; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
46 https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique/ 200 text/html Província de Gaza: Oportunidades mais sólidas e estratégicas de investimento no turismo no sul de Moçambique. 172233 tempo Turismo 2026-04-14T10:52:30+00:00 2026-04-17T08:28:56+00:00 123875 baf3c60fa77742c9b88a67937f9269c95284f3d5d371319009c9af2bdb6be140 biblioteca existente recursoQA18;URLs/versão originais preservados RecursoQA18; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
47 https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico/ 200 text/html Província de Inhambane: Um Destino Estratégico para Investimento Turístico 172253 tempo Turismo 2026-04-14T14:15:12+00:00 2026-04-14T14:25:44+00:00 123445 35829f3b9725650ad51ad7b84f149ddde2157202c17083b1faec4212f5305df6 biblioteca existente recursoQA15;URLs/versão originais preservados RecursoQA15; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
48 https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento/ 200 text/html Província de Manica: Onde o Turismo de Montanha, Natureza e Cultura Encontra o Próximo Ciclo de Investimento 172258 tempo Turismo 2026-04-14T14:18:28+00:00 2026-04-17T10:42:16+00:00 123841 f1ffc7cb73f68643e8ca5883c187bc7b8bb125d9f907ae190d1503d0877c9968 biblioteca existente recursoQA14;URLs/versão originais preservados RecursoQA14; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
49 https://investemoz.tempo.co.mz/provincia-de-maputo-turismo/ 200 text/html Província de Maputo: Natureza, Cultura e Conectividade como Motores de Crescimento Sustentável 172237 tempo Turismo 2026-04-14T10:55:21+00:00 2026-04-17T08:27:24+00:00 123013 71887a836c75fc67b80a6a83be2a2af23b93acbd5aa353f42c75f598544ac46a biblioteca existente recursoQA17;URLs/versão originais preservados RecursoQA17; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
50 https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo/ 200 text/html Província de Nampula - Perfil de investimento no setor do turismo 172230 tempo Turismo 2026-04-14T10:50:32+00:00 2026-04-14T11:10:23+00:00 123337 148faca42ca44da6017fbd46e23de27fd497d36f9f8b1b695e0ea3f35474fcbb biblioteca existente recursoQA19;URLs/versão originais preservados RecursoQA19; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
51 https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem/ 200 text/html Província de Sofala: Onde a Natureza, a História e o Futuro do Turismo Convergem 172256 tempo Turismo 2026-04-14T14:16:47+00:00 2026-04-14T14:26:17+00:00 123519 5100b90cb6c67fb36ce171ebf04c6801fe3ead214bcff26f942f6946fcd3add0 biblioteca existente recursoQA12;URLs/versão originais preservados RecursoQA12; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
52 https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo/ 200 text/html Província de Tete: Onde o Rio Zambeze, a Cultura Viva e a Conservação Comunitária Criam o Futuro do Turismo 172261 tempo Turismo 2026-04-14T14:19:48+00:00 2026-04-14T14:26:25+00:00 123849 8e28ccbeed95208d8766bfbc57b5b4b398d5787ea5aac922daa59e596ca58511 biblioteca existente recursoQA13;URLs/versão originais preservados RecursoQA13; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
53 https://investemoz.tempo.co.mz/rofino-licuco-preso-cinco-meses-depois-de-condenado-por-agressao 200 text/html Rofino Licuco preso cinco meses depois de condenado por agressão 73246 tempo Nas Redes 2017-07-28T20:23:11+00:00 2017-07-28T20:23:11+00:00 63685 0b8053aa5c5a8ee29648394dd7e0a84dd0eb095776c5f3d88b3b59c47f1ae233 biblioteca / correspondência por rever inventariado; não importado
54 https://investemoz.tempo.co.mz/sete-individuos-mortos-a-tiro-na-matola 200 text/html Sete indivíduos mortos a tiro na Matola 77232 tempo Nas Redes 2017-08-11T20:30:32+00:00 2017-08-11T20:30:32+00:00 63135 f903b084f891f13467121990c3deccd2b995c036e9eaa9124ebe53dc2d04c081 biblioteca / correspondência por rever inventariado; não importado
55 https://investemoz.tempo.co.mz/sete-pessoas-morrem-e-150-sao-resgatados-em-acidente-com-electrico-na-india 200 text/html Sete pessoas morrem e 150 são resgatados em acidente com eléctrico na Índia 62416 tempo Categoria 2017-06-26T12:14:48+00:00 2017-06-26T12:14:48+00:00 45244 a193cf19595a6e3de307d0307c3c96bc819d5075678e91ac9ebc5681185f4a89 biblioteca / correspondência por rever inventariado; não importado
56 https://investemoz.tempo.co.mz/tech-fintech 200 text/html Fintech 173209 tempo Oportunidades 2026-06-16T11:24:22+00:00 2026-06-23T10:03:06+00:00 88010 3b3b525c5d7ef34b5b9a1023aea5e6bc70c619672b5d173e6d9fe767782ab2d3 biblioteca / correspondência por rever inventariado; não importado
57 https://investemoz.tempo.co.mz/turismo 200 text/html Turismo 171990 tempo Oportunidades 2026-04-06T09:22:51+00:00 2026-06-16T13:07:59+00:00 87984 b7b4b2f9d9c4fc3a993e97ecdccb48f70ec9f560e13b15a9aad7205d88d22126 biblioteca existente recursoQA10;URLs/versão originais preservados RecursoQA10; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
58 https://investemoz.tempo.co.mz/74364-2 200 text/html 74364 tempo Categoria 2017-07-26T21:20:33+00:00 2017-07-26T21:20:33+00:00 43595 2da21e3e51ccfe3a095710c85c0f549f0d995f2a2cc29bdccb1bebdee10ba805 biblioteca / correspondência por rever inventariado; não importado
59 https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique 200 text/html Cabo Delgado - Pemba, A capital do turismo costeiro do norte de Moçambique 172223 tempo Turismo 2026-04-14T10:21:20+00:00 2026-04-14T10:41:05+00:00 123383 99a2d46fed61caf9c8313ec3a695e15608bd478177d3048c3bda7fa2281039e8 biblioteca existente recursoQA20;URLs/versão originais preservados RecursoQA20; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
60 https://investemoz.tempo.co.mz/daude-razaque-ja-nao-e-treinador-da-liga-desportiva-de-maputo 200 text/html Daúde Razaque já não é treinador da Liga Desportiva de Maputo 62929 tempo Nas Redes 2017-06-27T16:41:43+00:00 2017-06-27T16:41:43+00:00 64169 80f4f83caa75dcef3df39d697bc9d7afd77468caea640f2bbdeda70960ef8f5d biblioteca / correspondência por rever inventariado; não importado
61 https://investemoz.tempo.co.mz/daviz-simango-nao-se-incomoda-com-possivel-sucessao-na-beira 200 text/html Daviz Simango não se incomoda com possível sucessão na Beira 62649 tempo Nas Redes 2017-06-26T19:38:09+00:00 2017-06-26T19:38:09+00:00 64147 16981acbf74f0b85ae8abb4d4853aac8c05a68795b59913370672a4a57c9f1a1 biblioteca / correspondência por rever inventariado; não importado
62 https://investemoz.tempo.co.mz/direitos-e-deveres-do-trabalhador-domestico 200 text/html Direitos e Deveres do Trabalhador Doméstico 173470 tempo Trabalho Doméstico 2026-09-15T11:28:25+00:00 2026-09-15T11:28:28+00:00 86045 1821b7404c7761bd3ca1600fe1b0be0e63c98dc64800c30a3b228bca8dfe7ee3 biblioteca / correspondência por rever inventariado; não importado
63 https://investemoz.tempo.co.mz/energia 200 text/html Energia 172679 tempo Featured 2026-04-30T13:03:57+00:00 2026-04-30T13:28:48+00:00 59488 fe0f7fe20a980bee1f96277e25b2536eaa9941733d63fd39c8f018adcae0a6e8 biblioteca existente recursoQA8;URLs/versão originais preservados RecursoQA8; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
64 https://investemoz.tempo.co.mz/energia/ 200 text/html Energia 172679 tempo Featured 2026-04-30T13:03:57+00:00 2026-04-30T13:28:48+00:00 59490 69b5a62220e4f47037cc0c13c74a7c0c9d6b8845b260b12a317099ce4e560339 biblioteca existente recursoQA8;URLs/versão originais preservados RecursoQA8; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
65 https://investemoz.tempo.co.mz/exigimos-responsabilizacao-dos-agentes-do-crime-daviz-simango-2 200 text/html “Exigimos responsabilização dos agentes do crime”, Daviz Simango 62739 tempo Nas Redes 2017-06-26T23:13:36+00:00 2017-06-26T23:13:36+00:00 64225 8eb9a45e56e65febc657c4aba0ddcce77239a5d9c59902a25717a09f4572b5f5 biblioteca / correspondência por rever inventariado; não importado
66 https://investemoz.tempo.co.mz/governo-e-parceiros-reforcam-mecanismos-de-controlo-de-hivsida 200 text/html Governo e parceiros reforçam mecanismos de controlo de HIV/Sida 62854 tempo Nas Redes 2017-06-27T11:07:53+00:00 2017-06-27T11:07:53+00:00 64139 0bb91025f1790b9571ad761351ed2fbf52b4d7ee7f33c7e4bc7aee2e9c1c90b1 biblioteca / correspondência por rever inventariado; não importado
67 https://investemoz.tempo.co.mz/graca-machel-diz-que-desnutricao-e-um-problema-de-desenvolvimento 200 text/html Graça Machel diz que desnutrição é um problema de desenvolvimento 63128 tempo Nas Redes 2017-06-27T22:01:59+00:00 2017-06-27T22:01:59+00:00 64267 4c7c9a780d23a580b9f229b2a965d0f6145e5950c8d56a963047d720905a09e4 biblioteca / correspondência por rever inventariado; não importado
68 https://investemoz.tempo.co.mz/images/ANO-2017/JUNHO/21/GOVERNADOR-in.gif 404 biblioteca / correspondência por rever inventariado; não importado
69 https://investemoz.tempo.co.mz/in-dependencia 200 text/html In dependência 63181 tempo Nas Redes 2017-06-27T21:38:06+00:00 2017-06-27T21:38:06+00:00 63079 ec3bc03878819dfb5af3c6268b4a812010d177924489ac49190351ac0b2f5bd4 biblioteca / correspondência por rever inventariado; não importado
70 https://investemoz.tempo.co.mz/kroll-nao-descobriu-como-foram-gastos-2-bilioes-de-dolares-governo-de-nyusi-nao-apoiou-a-auditoria-criacao-da-proindicus-ematum-e-mam-nao-foi-para-protecao-da-zona-economica-exclusiva-2 200 text/html Kroll não descobriu como foram gastos 2 biliões de dólares; Governo de Nyusi não apoiou a Auditoria; criação da Proindicus, EMATUM e MAM não foi para proteção da Zona Económica Exclusiva 63146 tempo Nas Redes 2017-06-27T19:30:01+00:00 2017-06-27T19:30:01+00:00 66959 9cf92d04143df86c4cd86bf0a372696da3463b71239e4b4d737d355ff09e7e76 biblioteca / correspondência por rever inventariado; não importado
71 https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel 200 text/html Maputo Cidade – Plataforma Estratégica de Turismo, Negócios e Investimento Sustentável 172240 tempo Turismo 2026-04-14T10:58:53+00:00 2026-04-14T11:03:28+00:00 123597 4a4a88ac0c6fdeb26bdfe7183c9b618075ceb1460875ace0b351e96a2f1dd9e9 biblioteca existente recursoQA16;URLs/versão originais preservados RecursoQA16; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
72 https://investemoz.tempo.co.mz/mineracao 200 text/html Mineração 172674 tempo Featured 2026-04-30T13:12:09+00:00 2026-04-30T13:30:13+00:00 104140 803652cd4358d0b65a364a91ef916f6c5cbfe6bb539867e3d94b6ca36fbf10c7 biblioteca existente recursoQA9;URLs/versão originais preservados RecursoQA9; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
73 https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano 200 text/html Niassa - O último grande destino virgem do turismo africano 172219 tempo Turismo 2026-04-14T10:17:14+00:00 2026-04-14T10:43:56+00:00 126056 c19557c9937180420d7ae52d28fbc3aea51e1229eefe2e05a33f576f98bcd2d0 biblioteca existente recursoQA21;URLs/versão originais preservados RecursoQA21; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
74 https://investemoz.tempo.co.mz/o-instituto-nacional-de-meteorologia-preve-o-seguinte-estado-do-tempo-para-esta-29 200 text/html O Instituto Nacional de Meteorologia prevê o seguinte estado do tempo para esta ... 62688 tempo Nas Redes 2017-06-26T18:21:18+00:00 2017-06-26T18:21:18+00:00 64583 b1de67ebe5fefc505086bad0469295e41bf50f6f7aba7fe9e27f30220a14071f biblioteca / correspondência por rever inventariado; não importado
75 https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique 200 text/html Província de Gaza: Oportunidades mais sólidas e estratégicas de investimento no turismo no sul de Moçambique. 172233 tempo Turismo 2026-04-14T10:52:30+00:00 2026-04-17T08:28:56+00:00 123873 e2835df9992914631c34a5e3006e86277ddf135a0fc264f6ecf3811f778ac60d biblioteca existente recursoQA18;URLs/versão originais preservados RecursoQA18; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
76 https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico 200 text/html Província de Inhambane: Um Destino Estratégico para Investimento Turístico 172253 tempo Turismo 2026-04-14T14:15:12+00:00 2026-04-14T14:25:44+00:00 123443 919876fd899936071aa7a56a76c357ae30370b29014db2b1e7837450a099f0bd biblioteca existente recursoQA15;URLs/versão originais preservados RecursoQA15; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
77 https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento 200 text/html Província de Manica: Onde o Turismo de Montanha, Natureza e Cultura Encontra o Próximo Ciclo de Investimento 172258 tempo Turismo 2026-04-14T14:18:28+00:00 2026-04-17T10:42:16+00:00 123839 37b9eebf01ccf49ff3832755aa91b2595bbb80729b7b7cc861558c556d3f4cb7 biblioteca existente recursoQA14;URLs/versão originais preservados RecursoQA14; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
78 https://investemoz.tempo.co.mz/provincia-de-maputo-turismo 200 text/html Província de Maputo: Natureza, Cultura e Conectividade como Motores de Crescimento Sustentável 172237 tempo Turismo 2026-04-14T10:55:21+00:00 2026-04-17T08:27:24+00:00 123011 e6bbe002132682c1bbcf5cf7666dcc78dc18915c92bf298a5540db6c07935a23 biblioteca existente recursoQA17;URLs/versão originais preservados RecursoQA17; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
79 https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo 200 text/html Província de Nampula - Perfil de investimento no setor do turismo 172230 tempo Turismo 2026-04-14T10:50:32+00:00 2026-04-14T11:10:23+00:00 123335 704d23e4c838df4249894b550f087eb36b90797d61d26de82b829c8d2b7d444f biblioteca existente recursoQA19;URLs/versão originais preservados RecursoQA19; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
80 https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem 200 text/html Província de Sofala: Onde a Natureza, a História e o Futuro do Turismo Convergem 172256 tempo Turismo 2026-04-14T14:16:47+00:00 2026-04-14T14:26:17+00:00 123517 cc0d8bead12f2e42af60cdf10fb2412756e67c2c03ddc8d91c621e31e921933c biblioteca existente recursoQA12;URLs/versão originais preservados RecursoQA12; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
81 https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo 200 text/html Província de Tete: Onde o Rio Zambeze, a Cultura Viva e a Conservação Comunitária Criam o Futuro do Turismo 172261 tempo Turismo 2026-04-14T14:19:48+00:00 2026-04-14T14:26:25+00:00 123847 70ca06168749124e2eaae340b1396ccbe183a1e5dfd348dfd93575d9b3519205 biblioteca existente recursoQA13;URLs/versão originais preservados RecursoQA13; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.
82 https://investemoz.tempo.co.mz/sancoes-disciplinares-aplicaveis-a-trabalhadores-domesticos-segundo-o-regulamento-de-mocambique 200 text/html Sanções Disciplinares Aplicáveis a Trabalhadores Domésticos segundo o Regulamento de Moçambique 173474 tempo Trabalho Doméstico 2026-09-15T11:37:04+00:00 2026-09-15T11:37:06+00:00 86561 6af9a928c826dd6a756310c0f948cb0db8c1b8bd98ca15577faba528ba6f3cf3 biblioteca / correspondência por rever inventariado; não importado
83 https://investemoz.tempo.co.mz/dois-alunos-da-9a-classe-cairam-nas-maos-da-policia-da-republica-de-mocambique 200 text/html Dois alunos da 9ª classe caíram nas mãos da Polícia da República de Moçambique (... 63248 tempo Nas Redes 2017-06-28T11:30:01+00:00 2017-06-28T11:30:01+00:00 64573 be57946f86277610a1597e73eaedeaf55941de76aaef3f8eda77e3a92672a6f0 biblioteca / correspondência por rever inventariado; não importado
84 https://investemoz.tempo.co.mz/regulamento-do-trabalho-domestico 200 text/html Regulamento do Trabalho Doméstico 173463 tempo Trabalho Doméstico 2026-09-15T10:56:07+00:00 2026-09-15T12:41:44+00:00 93646 6cafa4cc6a5a327de0ca4c5d98fce5e10bf7152c1dc987aa42350034fd747e1c biblioteca / correspondência por rever inventariado; não importado
85 https://investemoz.tempo.co.mz/build/assets/welcome-DyF43GB5.js 200 application/javascript 19506 c94067f98b9d24cc457a20ed759f6beec1dbf9675354299f6141ed6056e95ab0 biblioteca / correspondência por rever inventariado; não importado
86 https://investemoz.tempo.co.mz/assets/logos/1_COR.png 200 image/png 25390 73c30a98a87960fc388d33aaf9ed3a15c3123338f8e82e84c3538adb6d816c9c biblioteca / correspondência por rever inventariado; não importado
87 https://investemoz.tempo.co.mz/images/hero/hero-1.png 200 image/png 2315768 3c9246169025b9b1b4d1d10fb51c1f644123984bde56c6c2d9bf869d851024fc biblioteca / correspondência por rever inventariado; não importado
88 https://investemoz.tempo.co.mz/images/entidades/APIEX.jpg 200 image/jpeg 240665 e04e4da3a1478b34a14e1d92dbebdea5bdf9bfd645c942d785b184d63dc58c48 biblioteca / correspondência por rever inventariado; não importado
89 https://investemoz.tempo.co.mz/images/entidades/BM.png 200 image/png 369795 3a8deddcf90f2376e8fe28447724739ce57da64b26e6887ddade566871deeea6 biblioteca / correspondência por rever inventariado; não importado
90 https://investemoz.tempo.co.mz/images/entidades/BM.jpg 200 image/jpeg 15681 bb93f6bb51104d4fe9d0776a79bbf3b2978f31c6802884e6f0fc3d50056268ff biblioteca / correspondência por rever inventariado; não importado
91 https://investemoz.tempo.co.mz/images/entidades/bvm.jpg 200 image/jpeg 296992 8d3e532137896c9319bab4422e836bd1232fa81babf4626efa8c411927052731 biblioteca / correspondência por rever inventariado; não importado
92 https://investemoz.tempo.co.mz/images/entidades/BMM.png 200 image/png 199364 a92c6ed17cf8769d97a0e70c5db31cc6de1b583a71e161b8b35a7eae21468122 biblioteca / correspondência por rever inventariado; não importado
93 https://investemoz.tempo.co.mz/images/hero/hero-3.jpg 200 image/jpeg 9875137 91377213b6286aca7ee4af90860a7dc7a75fa08a165ef7b3192081db43f92dff biblioteca / correspondência por rever inventariado; não importado
@@ -0,0 +1,212 @@
<!-- qa-visual-results-20261010:begin -->
## Ensaios visuais e pendências atuais — 2026-10-10T17:57:54.604301+00:00
Trabalho exclusivo na QA da porta 18097, base `desk_restore_20261009_174726_test:55441`. Nenhuma alteração na base ativa. A mesma matriz mantém **3393 requisitos / 3393 linhas / 6845 ocorrências de origem**: 3235 parciais, 145 não implementados, 5 dependentes de decisão, 1 implementado mas não ativado e 7 concluídos e demonstrados. Os estados globais não mudaram; foram atualizados subcritérios com provas próprias. A ausência de requisitos globalmente classificados “com falha” não significa ausência de casos abertos.
| Requisito | Demonstrado em QA | Critérios ainda pendentes |
| --- | --- | --- |
| C37; UI01; PROFILE24–25 | MFA manual QA18 aceite às 17:40:21 e 17:46:42 UTC; autenticação persistida e dashboard 200. Responsável confirmou acesso. O 403 inicial era do relatório administrativo reservado. Mensagem e retorno ao painel corrigidos; Chromium QA21 provou recusa 403, retorno e F5 sem perder sessão. | F5, logout e novo login no navegador habitual QA18. Não declarar o loop anterior resolvido por inferência: o rastreio não demonstra um ciclo persistente. Uma atualização Inertia 409 isolada não é perda de autenticação. |
| A/B; GUIDE01; SIM02; DESK01–02; CRM01 | Chromium desktop QA18: criação → gravação → nova sessão → recuperação → aplicabilidade → ferramenta → resultado no dossier → preparação no Balcão → acompanhamento interno → nova recuperação. Cinco fases e 19 etapas preservadas. A dispensa S10; B aguarda informação externa em S10 enquanto S02 avança. Resultado: equilíbrio de 20 unidades e défice A=0 / B=70.000 MZN. QA23 recebe 403/404; gestor não vê resultados financeiros privados. | Balcão demonstrado em preparação DRAFT, sem submissão ou decisão externa. Restantes critérios especializados, fiscalidade, documentos e consumidores não encerrados. Repetição final A/B móvel ainda necessária. |
| DASH01; PROFILE05/32 | QA21, a partir do dashboard: “Minhas oportunidades” e “Nova oportunidade” → rascunho incompleto #50 → guardar → nova sessão → recuperar → editar. Promotor solicita arquivo; Publicador QA25 arquiva com fundamento; estado recuperado. | Restauração por perfil ausente no workflow. Definir e implementar ator/regra coerentes sem conceder arquivo direto ao Promotor por inferência. Workflow e contratos especializados ainda exigem demonstrações próprias. |
| COUNTRY01–04/06/07 | Residência PT guardada no perfil e recuperada após F5. Negócio e fundiário DRAFT: país PT / origem FR guardados e recuperados. Balcão A/B: PT / ZA recuperados noutra sessão. Quatro formulários públicos apresentam 249 países cada. Gestão: filtros ZA/unknown, quatro manifestações sem origem e zero pedidos submetidos ZA, com rascunhos excluídos. | Nos formulários públicos, a presença de opções não prova submissão ou recuperação. Concluir cada gravação e estatística positiva de origem conhecida no UI. Nos rascunhos de negócio/fundiário, F5 não substitui nova sessão. |
| GEO01–03 | Oportunidade própria QA21 #51 e criação de projeto, cluster e corredor: Cabo Delgado registado → API com uma geometria acessível → um polígono SVG visível em cada consumidor. | Seleções desse ensaio não guardadas. Recuperar relações por consumidor e testar descendentes, restantes mapas e perfis. Tiles externos bloqueados no ensaio, sem confirmação da base externa. 408 associações anteriores e 173 pendentes preservadas; Mandie/Guro ambíguo. |
| LEG02/03/06/07; C77–C83 | Corrigidos landmarks, ARIA, hierarquia de títulos, reflow, foco das tabelas e idioma HTML pt-MZ em QA. Quinze execuções dirigidas de axe, em 320/390/1440px, sem violações automáticas ou overflow. Três páginas com skiplink primeiro e tabelas financeiras operadas por teclado. | Cobertura integral, leitor de ecrã, dispositivos reais, mais perfis e editor C74. O dossier QA21 verifica o componente móvel corrigido, sem substituir a repetição final A/B. |
| DOC01; C84 | Retirada mensagem global falsa de ClamD pendente; scan e motor apresentados por ficheiro/versão. Os 22 scans reais anteriores permanecem demonstrados. | PDFs, exportações, partilhas, derivadas e operações restantes exigem provas próprias. Um original limpo não comprova a derivada. |
| OPS01 | Backup QA 2026-10-10T17:48:11+00:00: 295 tabelas e 103 ficheiros restaurados. Ativo READ ONLY, backup 2026-10-10T17:48:52+00:00: 288 tabelas restauradas numa base QA distinta e cinco ficheiros conferidos no arquivo. Instalador/publicação: 9 testes / 93 asserções, além de TypeScript. | Aplicação, compatibilidade e reversão da futura revisão candidata no clone fresco do ativo. Revisão candidata global e ativação continuam por fixar. Contagens de testes não substituem critérios funcionais. |
**C80 corrigido:** a resposta tardia do GET do rascunho podia substituir preenchimento já iniciado. Edição e avanço ficam bloqueados até a recuperação terminar; o retorno exige o snapshot completo guardado. Chromium com resposta deliberadamente atrasada comprovou bloqueio, recuperação e libertação. A/B recuperaram os inputs corretos noutra sessão, preservando versões anteriores.
**Recusa manual do mapa às 17:52:33 UTC:** o pedido à oportunidade 51 foi feito com QA18, não QA21; recebeu 403 com sessão preservada. É uma prova de autorização, não de apresentação do mapa. Usar QA21 (`profiles-v1-promoter@example.invalid`) para esse consumidor; o Chromium anterior QA21 recebeu 200. Nenhuma permissão alargada para contornar a recusa.
**Provas privadas sanitizadas:** `investor-ab-chromium-20261010.json`, `promoter-dashboard-chromium-20261010.json`, `consumers-mobile-accessibility-chromium-20261010.json`, `visual-followup-chromium-20261010.json`, `qa-report-access-chromium-20261010.json`, `c37-qa18-post-mfa-destination-20261010.json`, `visual-persistence-qa.json`, `profile-installation-backup-qa.json` e `virtual-desk/backup-proof.json`. A tentativa antiga de QA21 na oportunidade 30 recebeu 403 e não contou como mapa; foi substituída pelo consumidor próprio 51. Tentativas e falhas de harness preservadas; sem códigos, segredos ou cookies nas evidências entregáveis.
Mercado Financeiro permanece privado, sem corpo inventado; rotas públicas 404. Energia e Mineração mantêm artigos distintos e Turismo não criou associações geográficas automáticas. Fontes oficiais aceites não foram reabertas. Próximo lote: C37 manual, restauração por perfil, submissão/estatística por formulário, gravação/recuperação territorial, documentos, Response/§27 e workflow. Perfis modulares/SLA continuam decisões separadas; não iniciar novas jornadas educativas.
As versões históricas abaixo ficam preservadas. Afirmações antigas sobre falta de navegador, A/B não testados ou ClamD global pendente são superadas apenas pelos subcritérios acima. Código preparado, QA demonstrada e disponibilidade ativa são estados distintos.
<!-- qa-visual-results-20261010:end -->
# Legado Tempo — piloto de agronegócio em QA
<!-- qa-c71-sectors-result-20261010:begin -->
## Resultado atual — 2026-10-10T16:23:25.695297+00:00
**QA18097 recuperada; nenhuma alteração na base ativa.** C71 técnico corrigido: faltavam26 ficheiros de catálogos partilhados, incluindo `global/2964`, necessários ao `pg_dump`. O agente/processo que os removeu continua desconhecido. O cluster danificado foi preservado e substituído por cluster novo em diretório estável, apenas no alvoQA55441. As 295 tabelas/34.902 linhas e 31 sequências iniciais conferiram; schema/owners/ACL/triggers equivalentes após normalização explícita de 2 constraints varchar/text e slots de colunas removidas. A cópia física retida confere em 75575 ficheiros por nomes, tamanhos ehashes; isso não afirma restauro lógico de todos os bancos históricosQA.
**Backup/restauro posterior ao lote:** 2026-10-10T16: 22:44+00:00,295 tabelas e 103 ficheiros privados conferidos em `profile_restore_20261010_162229_test:55441`. APP_KEY, MFA e contas preservados. Não é prova de backup da base ativa nem fixa candidata global. QA/login voltou aHTTP200; manutenção arquivada. 2FAS Auth continua compatível com TOTP; a rejeição manualQA18 registada foi de palavra-passe antes do MFA, sem justificar troca de aplicação.
| Requisito / caso | Evidência deste lote | Lacuna concreta |
| --- | --- | --- |
| AUD-OPS-01 / C71 | Cluster saudável, dados/sequências/permissões preservados, backup/restauro renovado,0escritasativas. Estado global passa de com falha para parcial. | C37/QA18,A/B e outros consumidores; candidata e backup ativo/autorização própria. |
| AUD-LEG-01/05/07 |22 ficheiros (17 artigos+5 imagens) importados privadamente, hashes recuperados,22 scans ClamD reais; replay sem duplicações/eventos/scans novos. |32 das 50 versões observadas continuam não importadas; sitemap misto/paginação/links externos/C68 e cobertura integral. |
| AUD-LEG-02/03/06 / C72 |16 artigos novos com texto públicos, título deInfraestruturas preservado. Chromium desktop: imagens/pesquisa/artigos→MFA21→4 dossiers→retorno→logout→nova sessão→F5/replay. HTTP e persistência separados,8 dossiers no total. | Móvel/acessibilidade/mais perfis e percursosA/B completos não encerrados. |
| AUD-LEG-04 |Leitura pública; guardar requer login. Anónimo/QA23 sem contexto privado; QA23recebe403 ao dossier alheio. Estado de consulta/proteçãoQA mantido concluído no âmbito demonstrado. | Disponibilização ativa e demais aceitações são independentes. |
| Mercado Financeiro |OrigemHTML e respostaInertiaHTTP200 contêm corpo vazio; exemplar22 no lote fica privado na biblioteca (recurso23). `/mercado-financeiro` e respetiva exploração404,sem página vazia apresentada como entrega. | Obter conteúdo real/versão com texto; não usar Fintech nem inventar corpo. |
| C74 |Imagens semAssunto eram recusadas porPUT422. QA aceita tema vazio sem inferir setor; interface tornaAssunto opcional. Cinco metadados vazios guardados/recuperados; teste dedicado1/11. | Editor administrativo sem ensaio visual próprio; alteração protegida pela guardaQA. |
**Contagens canónicas:**3393 requisitos/3393 linhas, 6845 ocorrências de origem; 3235 parciais, 145 não implementados, 5 dependentes de decisão, 1 implementado mas não ativado, 7 concluídos e demonstrados, 0 com falha. Casos adicionais e aceitações abertas continuam visíveis dentro dos requisitos parciais;0 com falha global não significa portal sem erros. BacklogCSV é projeção reconciliada dos mesmos3393 IDs, incluindo39 anteriormente omitidos, não outra matriz.
**RotasQA:** `/explorar/energia-mineracao` → `/energia-mineracao`, `/energia`, `/mineracao`; `/explorar/turismo` → `/turismo` e 11 artigos territoriais; `/explorar/infraestruturas` → `/infraestruturas`. Agronegócio preservado. Energia/Mineração são fontes distintas; Turismo mantém texto editorial e 0associações territoriais automáticas. Todos os dados de fonte/autoria/datas/hash/URLs/versões originais são conservados. Os artigos não criaram oportunidades (49antes/depois), geometrias ou perfis. Divulgação foi uma ação autenticada do gestorQA14 pelo mecanismo existente, separada da importaçãoCLIQA (atribuição `--actor`, não autenticação ativa); nenhum desafioMFA foi observado nessa conta. QA21/QA23 usaramMFA normalmente.
**Próximo trabalho executável:** restantes32 versões/acervo eC68 por ficheiro; Response/§27/validações, PDFs/derivadas epaíses/origem/estatísticas por consumidor;173 correspondências territoriais ainda abertas comMandie/Guroambíguo. Não acrescentar jornadas educativas para contornar essas pendências. Perfis modulares eSLA conservam decisões próprias. C37/QA18,A/B,móvel/acessibilidade permanecem abertos; este lote não demonstraCRUD/workflow de oportunidades nem os encerra.
**Ativação futura:** revisão/hashes/flags e aceitação global→leitura/backup/restauro do ativo correspondente→código/build compatíveis com flags desligadas→030000→040000→09/190000 somente se ausentes→importação privada de conteúdo/cartografia com prévia e scan→divulgação separada→disponibilização incremental. QA já tem as 3 instalações;não as repetir. Previsão desta extensão: 22 originais (reutilizar hashes existentes),21 potencialmente divulgáveis e 1 exemplar vazio privado;0 contas/dossiers/opportunities/geometrias transferidos. O CLIrecusa aplicação ativa: falta preparar/demonstrar uma ação autenticada emlote que preserve exatamente as identidades/versionamento existentes; upload unitário não foi provado equivalente. Cartografia:408 candidatas/173 pendentes no retratoQA anterior, não importadas por esta frente. `ready=true` continua preparação, não instalação/disponibilização.
**Reversão:** retirar divulgação em ações auditadas e ocultar consumidores/flag, preservando originais, versões, consentimentos/referências eeventos. Restauro técnico usa o novo dump íntegro e chave estável numa QA distinta,comparação antes de corte;não voltar ao cluster danificado. Nunca drop de tabelas utilizadas nem copiar fixturesQA para o ativo. Provas: `legacy-sectors-{apply,replay,http,browser,persistence}-qa.json`, `legacy-financial-origin-20261010/proof.json`, `portal-qa-recovery/20261010_154445/*-proof.json` e `profile-installation-backup-qa.json` (privadas, sem credenciais/cookies/códigos nas capturas).
**Versões históricas preservadas abaixo:** frases sobreC71aberto,22 ficheiros apenas preparados ou rotas apenas deagronegócio descrevem os incrementos anteriores e são substituídas, para o estado atual, pelas provas desta secção.
<!-- qa-c71-sectors-result-20261010:end -->
<!-- legacy-manual-sectors-20261010:begin -->
## Confirmação manual recebida do responsável — 10/10/2026
Confirmado nesta conversa: **leitura pública → autenticação → criar/escolher dossier → guardar referência → nova sessão → recuperar dossier e abrir artigo**. Conta, perfil, dispositivo e hora de execução não foram indicados; não os presumir. “Criar/escolher” conserva o relato do responsável, sem declarar que ambas as alternativas foram ensaiadas separadamente.
| Critério confirmado manualmente | Correspondência na matriz existente |
| --- | --- |
| Leitura pública e autenticação ao entrar no percurso privado | AUD-LEG-03 e AUD-LEG-04 |
| Criar/escolher dossier e guardar referência | AUD-LEG-03, AUD-LEG-06; gravação/consumidor em AUD-LEG-07 |
| Nova sessão, recuperação do dossier e abertura do artigo | AUD-LEG-03, AUD-LEG-06 e AUD-LEG-07 |
Só estes subcritérios receberam a confirmação manual. Nenhum estado global foi alterado, nenhuma API/persistência recebeu prova técnica adicional por inferência e nenhuma linha foi acrescentada à matriz. O relato manual fica separado das provas Chromium/API/READ ONLY anteriores.
**Continuam pendentes:** ensaios manuais móveis, acessibilidade, outros perfis e cobertura integral do legado. As observações automáticas anteriores de viewport/teclado/QA23 ficam preservadas como provas limitadas; não constituem essas aceitações manuais. C37 da conta18 e os cenários A/B completos não foram encerrados. A demonstração não renova backup/restauro nem resolve C71.
## Reconciliação dos quatro restantes setores — preparação, sem importação
As 92 entradas do inventário existente foram conferidas antes da seleção. Os quatro artigos de entrada foram novamente recolhidos por leitura anónima: HTTP200 e corpo/data de modificação iguais ao exemplar inicial. Os artigos descendentes e imagens usam os snapshots já inventariados, com hash conferido. Sitemap misto, paginação e restante acervo continuam abertos.
| Fonte legada | Identidade e cadastro existente | Conteúdo e lacuna concreta |
| --- | --- | --- |
| [Energia & Mineração](https://investemoz.tempo.co.mz/energia-mineracao) | 171981 / setor QA2 (`energia`) | Índice com ligações explícitas a Energia172679 e Mineração172674; preservar as três identidades e os setores existentes2/8/7. |
| [Turismo](https://investemoz.tempo.co.mz/turismo) | 171990 / setor QA3 (`turismo`) | Artigo setorial e 11 artigos territoriais explicitamente ligados; categoria Turismo preservada, sem inferir geometrias. |
| [Infraestruturas](https://investemoz.tempo.co.mz/infraestruturas) | 171992 / setor QA4 (`infraestruturas`) | Artigo integral; C72 removia um título h1. Normalização h1→h2 preparada e conferida sobre o original. |
| [Mercado Financeiro](https://investemoz.tempo.co.mz/mercado-financeiro) | 171994 / setor QA6 (`mercado-financeiro`) | Corpo vazio no exemplar atual. Arquivar metadados/original; não inventar texto nem substituir por Fintech. |
**Lote privado preparado: 17 versões de artigos + 5 imagens = 22 documentos.** Inclui quatro entradas setoriais, dois artigos de Energia/Mineração e 11 artigos territoriais de Turismo. A imagem de fundo genérica é um único exemplar por bytes, referenciado por vários artigos; não representa a fotografia de cada território. URLs com/sem barra são conservados como aliases do mesmo ID/versão/hash. Autoria, publicação, alteração, classificação original e HTML ficam preservados.
Os setores foram confrontados com a preservação READ ONLY da QA de10/10/2026 às14:54:19UTC, não com a base ativa. Correspondência2/3/4/6 para as entradas setoriais;8/7 para Energia/Mineração. Nenhum setor, identidade, artigo ou oportunidade foi criado na base. Alterações do corpo com a mesma identidade/versão deverão gerar conflito, não substituição silenciosa.
**Prévia real do importador existente:** 22 candidatos, zero duplicados por bytes e zero conflitos de identidade encontrados; transação READ ONLY no alvo `desk_restore_20261009_174726_test:55441`. CLI guardado, sem `--apply`, sem contas ou privilégios escolhidos. Não é aplicação/publicação autenticada. Nenhum scan novo foi executado ou declarado limpo; os scans do piloto não se estendem a este lote. C71 impede a aplicação enquanto faltar integridade/backup/restauro QA.
Reutilização concreta: biblioteca `documents`/`portal_resources`, importador `documents:import-batch`, verificador de binários, `portal_resource_links`, `LegacyContent`, controlador e componente `portal/legacy`, referências cifradas/eventos do dossier. Rotas de leitura/entrada, identidade do artigo e rótulos do componente ainda estão fixos em agronegócio: parametrizar esses pontos antes de disponibilizar outras páginas. As URLs legadas não são rotas QA disponíveis por estarem nesta tabela. Não criar um cadastro de conteúdo paralelo.
**C72 — código preparado:** o sanitizador removia o texto do h1 interno de Infraestruturas. A correção transforma-o em h2 e aplica a limpeza normal dos seus filhos/atributos. Os 17 originais conservaram hash; transformação e remoção de markup ativo verificadas sem bootstrap de aplicação ou acesso à base. Resultado não equivale a apresentação ou percurso dos novos setores demonstrado em QA.
Próximo incremento, após recuperar C71: prévia renovada no alvo e aplicação como rascunhos privados → ClamD por ficheiro → metadados/relações por citação e divulgação separada → parametrizar adaptador/componente e URLs preservadas → ensaios públicos e de dossier por setor. Mercado Financeiro mantém corpo vazio visível/identificado até existir conteúdo real; a consulta/cadastro do exemplar não exige uma relação a módulo/regra. Nenhuma importação ativa ou cálculo fiscal é habilitado.
Provas privadas: `legacy-sector-reconciliation-20261010/reconciliation.json`, `import-preview.json`, `content-check-before.json`, `content-check-after.json`; manifesto de staging `legacy-setores-20261010/manifest.json`. Inventários únicos atualizados: `legado-inventario.csv` e `legado-conteudos-reconciliados.csv`. Permanecem92 URLs/50 versões de artigos;22 entradas reconciliadas e17 versões preparadas neste incremento, sem omitir as restantes.
<!-- legacy-manual-sectors-20261010:end -->
<!-- qa-legacy-browser-20261010:begin -->
## Verificação do piloto na interface — 10/10/2026, exclusivamente QA
**O piloto existente foi demonstrado em Chromium instalado**, com contextos separados, sem usar o perfil pessoal do utilizador. O controlo nativo continua indisponível (`native pipe`, os error 2, duas sondagens); esse problema não impediu renderização, cliques e capturas por Playwright. C37/QA18 permanece pendente; este ensaio usou Promotor QA21 e outra conta sintética QA23.
Página pública→pesquisa com/sem resultado→artigo→carteira/mapas/biblioteca/oportunidade publicada→CTA→login/MFA da conta QA21→dossier guardado→fonte/retorno→logout→novo contexto/MFA→recuperação e F5→continuação/repetição passaram. Leitura anónima e QA23 não recebem o título/ID do contexto privado; dossier negado 403 à QA23. A criação na interface consta da tentativa inicial preservada; as repetições recuperam o mesmo dossier sem criar outro. Cinco fases / 19 etapas observadas. Persistência em transação READ ONLY: uma referência, um evento, um documento por hash; originais e scan preservados; projeto independente com zero referências.
Desktop 1440 px / móvel 390 px: fotografia 900×506, Noto Sans / Noto Serif carregadas, cores reais adaptadas, texto integral de 4.702 carateres, foco de 3 px por teclado e nenhum deslocamento horizontal. Sete capturas privadas. **C70 corrigido:** título de separador “Laravel” substituído por “InvesteMoz QA” apenas no build QA; bundle ativo preservado. Isto não certifica acessibilidade integral nem demonstra todas as geometrias/consumidores do portal.
**AUD-LEG-04 concluído e demonstrado em QA**, por consulta pública/proteção privada. AUD-LEG-01/02/03/05/06/07 continuam parciais pelas lacunas explicitadas na matriz: cobertura histórica, controlo assistivo sistemático, relações territoriais concretas, restante mapa de URLs e anexos. Nenhum I0–I3/A-B/workflow/C37 é encerrado por esta prova.
Recibos: `legacy-content-browser-qa.json`, `legacy-content-browser-persistence-qa.json`; testes `tools/legacy-content-browser-qa.cjs` e `tools/legacy-content-browser-persistence-qa.php`. Capturas e credenciais permanecem privadas; não foram registados Cookie, X-XSRF-TOKEN ou corpo com o código MFA. O ZIP anteriormente entregue é um snapshot anterior à verificação, não prova de execução remota.
**C71 / P0 — novo backup QA bloqueado:** `pg_dump` falhou às 14:48–14:49UTC: `global/2964` ausente, catálogo `pg_db_role_setting`; diagnóstico identifica 26 ficheiros de catálogos partilhados ausentes no cluster 55441, situado em Temp. Não atribuir a remoção à limpeza automática sem evidência. O backup/restauro válido de 13:35:39UTC (295 tabelas/81 ficheiros) permanece histórico. Preservação posterior READ ONLY: 295 tabelas/34.902 linhas/31 sequências; não equivale a backup/restauro verificado. Readiness QA foi invalidada e o CLI passa a devolver exit 1 controlado. Próxima ação: recuperação em ambiente isolado, preservação/conferência integral e nova prova de restauro antes de retomar escritas. Nenhum catálogo recriado às cegas nem acesso/escrita na base ativa. Ver `qa-readable-preservation.json`, `profile-installation-backup-failure-qa.json` e C71 em `casos-reproducao.md`.
<!-- qa-legacy-browser-20261010:end -->
10/10/2026. Fonte pública: https://investemoz.tempo.co.mz/. Esta frente integra a auditoria existente. A autorização refere-se exclusivamente à integração do legado em QA; decisões de perfis modulares/SLA, C37 e ensaios visuais anteriores permanecem separados. Nenhuma instalação, publicação ou escrita na base ativa.
## Endereços e demonstração disponível
- Entrada pública: **http://127.0.0.1:18097/explorar/agronegocio**.
- Artigo, mantendo o caminho do legado: **http://127.0.0.1:18097/agronegocio**.
- Exemplar único na biblioteca: **http://127.0.0.1:18097/documentos/6**; imagem original: `/documentos/5/view`.
- Preparar/continuar: **http://127.0.0.1:18097/investimentos/a-partir-de/agronegocio**. Só este passo exige autenticação e os dados pessoais do dossier continuam privados.
- O menu existente recebe “Explorar agronegócio” exclusivamente em QA. Os restantes acessos e URLs permanecem disponíveis.
Servidor guardado: `desk_restore_20261009_174726_test`, PostgreSQL loopback **55441**; HTTP **18097**, cookie próprio do ambiente. A identidade real é verificada antes de qualquer escrita. O adaptador só funciona com flag QA e essa base isolada; uma flag não o disponibiliza na base ativa.
## Inventário e reconciliação
| Unidade | Quantidade / situação | Artefacto |
| --- | --- | --- |
| URLs efetivamente recolhidos | 92; 91 HTTP200 e um HTTP404 | `legado-inventario.csv`; originais e hashes no inventário privado |
| Versões de artigos observadas | 50 agrupadas por ID legado, alteração anunciada e hash do corpo; URLs equivalentes conservados | `legado-conteudos-reconciliados.csv` |
| Referências entre páginas/recursos | 2.826 ocorrências identificadas, incluindo destinos externos e paginação não recolhidos | `legado-referencias.csv` |
| Sitemap anunciado | 30.170 entradas; URLs de `infopublica.tempo.co.mz`, apesar da recolha no domínio InvestMoz | `legado-sitemap-anunciado.csv`; não afirmar que essas páginas foram recolhidas |
| Importação do piloto | Dois documentos e dois recursos existentes: artigo6 e imagem5; repetição sem novos documentos/recursos | `legacy-import-preview-qa.txt`, `legacy-import-qa.json` privados |
| Associações do artigo | Tema agronegócio; imagem referenciada no `thumbnail` original; apoio editorial à S01 | `portal_resource_links` existentes, com citação/âmbito; nenhuma associação territorial automática |
O levantamento é progressivo. Não fecha a cobertura histórica do acervo: entradas do sitemap, paginação, destinos externos e conteúdos fora do piloto permanecem inventariados e por reconciliar/importar. Fontes oficiais já aceites mantêm essa decisão. O artigo Tempo é editorial: não recebe classificação oficial pelo facto de citar corredores ou políticas.
O domínio entrega uma SPA Laravel/Inertia: metadados “Informacao Publica”, página `welcome`, artigos `noticias/show` e uma área de investimento escolhida pela aplicação. Os resultados indexados e os metadados atuais divergem; ambos ficam identificados. Não presumir que a aplicação de leitura é WordPress porque as imagens provêm de `/wp-content/uploads/`.
**Lacuna do legado C68:** `https://investemoz.tempo.co.mz/images/ANO-2017/JUNHO/21/GOVERNADOR-in.gif` devolve404. Não afeta a imagem do piloto; não foi substituída nem considerada válida. Conteúdos com essa referência precisam de revisão individual na expansão.
**Conflitos de identidade/versão:** o importador existente compara hashes e identidade. Repetir o mesmo lote não acrescenta bytes nem recursos. Alteração do corpo com a mesma identidade exige revisão; não substitui silenciosamente o exemplar. A classificação original “Oportunidades” fica preservada no exemplar, mas o artigo é um guia na biblioteca e não cria uma linha de oportunidade.
## Fonte única e preservação
Post legado **171985**, autor **tempo**, publicado **2026-04-06T09:19:07+00:00**, modificado **2026-04-20T12:41:43+00:00**. Original de conteúdo preservado como documento JSON na biblioteca privada, sem props de autenticação, cookies ou CSRF. O corpo HTML original mantém-se nesse exemplar; o adaptador só apresenta uma projeção sanitizada. Snapshots HTTP brutos ficam exclusivamente privados e nunca são servidos.
- SHA256 do corpo original: `28383a11d95f13894381357d8f1227b33031d1d3ff74507da4ea8e5ce2470913`.
- SHA256 do exemplar da biblioteca: `0929184a6d4645b9046512675394ad4f60575273f6dab0955f71cc7474a6a4e2`.
- Imagem original `Agronegocio.png`: `5e3840705cf98226d1f189cf665aaa98cb05da96c645159a1332a1d429d642eb`, 550.888 bytes, 900×506. Autoria individual e data de criação não indicadas; não inventadas.
O importador prepara rascunhos privados e executa ClamD. A divulgação QA foi registada em metadados pelo gestor documental sintético14 e a publicação foi uma ação autenticada posterior. Superutilizador27 não recebeu permissão documental por acumulação de papéis. Disponibilidade pública, situação documental `not_applicable` e utilização em cálculo `false` permanecem independentes. A autorização deste piloto não habilita divulgação ativa nem cálculo.
O dossier conserva apenas referência ao recurso/documento, hash, versão de metadados, origem, ator e data, no payload cifrado existente. O artigo inteiro não é copiado para cada projeto. Há evento `legacy_content_referenced`; repetição da mesma fonte/versão não duplica referência, evento ou versão. A criação e associação são atómicas. Roteiros antigos não são convertidos nem renumerados.
## Referências visuais usadas
| Evidência real | Adaptação QA |
| --- | --- |
| CSS legado `app-CshAeK4J.css`: Noto Sans e Noto Serif; head inclui Instrument Sans | Instrument Sans do portal conservada; Noto Sans/Serif empacotadas localmente apenas no build QA do piloto, sem chamadas a fontes externas durante consulta |
| `welcome-DyF43GB5.js`: vermelho `#ED1C24`, hover `#b22222`, verde `#10352b`, fundo `#f6f3ea` | Fundo claro e verde profundo; vermelho escuro nos controlos para contraste legível. Não copiar cores claras com texto de contraste insuficiente |
| Hero fotográfico; cartões `rounded-2xl`, grelha móvel de uma coluna e expansão em ecrãs maiores | Imagem original, cartões sem animação obrigatória, grelha responsiva, largura de leitura limitada |
| Imagem ligada ao post e hospedagem Tempo | Original na biblioteca, alt descritivo, dimensões declaradas e origem explícita; não afirmar que a fotografia identifica um terreno disponível |
Foco visível, labels de pesquisa, mensagens de pesquisa sem resultado, hierarquia de títulos, links identificados e layout móvel preparados em React. Estas condições não equivalem a inspeção visual, medição no navegador ou aceitação de acessibilidade completa.
## Percurso e relações
Página setorial → artigo educativo → oportunidades publicadas **no mesmo setor** e acesso à carteira/mapas existentes → autenticação para iniciar ou escolher dossier → referência guardada → nova sessão recupera → regressar ao artigo com contexto autorizado → continuar o mesmo dossier.
A QA tem uma oportunidade publicada nesse setor, independente do artigo. Os links públicos da carteira, mapas, biblioteca e ficha dessa oportunidade respondem200. O artigo menciona **Pemba–Lichinga, Nacala, Vale do Zambeze, Beira, Limpopo e Maputo**. Os corredores cadastrados incluem designações como “Nacala e Norte”, “Beira e Centro”, “Maputo–Limpopo” e “Zambeze–Macuse”; nome semelhante não demonstra igualdade de âmbito. Nenhuma delimitação administrativa, geometria ou parcela foi associada por inferência. Correspondências concretas continuam pendentes e não alteram as408 geometrias QA já reportadas.
O contexto privado só regressa a um utilizador com acesso de escrita ao dossier; outras contas e anónimos continuam a consultar o artigo sem receber ID/título privado. Cinco fases e19 etapas preservadas. Não foram criadas novas jornadas educativas, identidades, tabelas de artigos ou oportunidades a partir do conteúdo.
## Provas e critérios ainda abertos
`legacy-content-http-qa.json`: leitura anónima; pesquisa com resultado/sem resultado; imagem e exemplar com hash; consumidor da biblioteca/carteira/mapas/oportunidade; autenticação normal do Promotor21; criação e associação; logout/nova sessão MFA; recuperação; continuação de dossier já existente; referência única; projeto independente sem mistura; outra conta403; contexto privado ausente em consulta anónima/alheia. Respostas Inertia observadas como dados, sem afirmar render/cliques.
`LegacyContentTest`, `InvestorPathTest` e `ImportDocumentBatchTest`: oito testes/264 asserções no ensaio inicial, complementados pelos controlos HTTP específicos. Testes sintéticos usam fixture de scan, não comprovam ClamD; o lote real comprova scan/hash dos dois documentos. TypeScript, buildQA e análise estática separados. Não usar a contagem global para fechar os requisitos.
**Histórico do ensaio inicial, anterior ao Chromium:** `cua.getState()` devolveu zero browsers/apps e criar o navegador `iab` devolveu “Browser is not available: iab”. Não foi repetido C37. Navegador do utilizador pode executar o roteiro abaixo; registar os resultados na mesma matriz antes de qualquer fecho funcional.
1. Sem login, abrir **http://127.0.0.1:18097/explorar/agronegocio**. Esperado: menu QA, fotografia, pesquisa e artigo; nenhum pedido de login para leitura. Pesquisar “corredores” e depois uma expressão inexistente.
2. Abrir “Ler o artigo”. Esperado: corpo completo, autor/datas/origem, contexto territorial explicitamente textual, uma ficha publicada do setor e links funcionais para carteira/mapas/biblioteca. Conferir móvel e teclado.
3. Clicar “Iniciar ou continuar uma ideia”. Entrar como **Promotor QA21** com MFA normal; credenciais apenas locais. Esperado: seleção dos seus dossiers e formulário com consentimento explícito, fonte preservada. C37 permanece um ensaio separado.
4. Criar um dossier sintético. Esperado: referência ao artigo, cinco fases/19 etapas, próxima ação existente; sem transformar artigo em oportunidade ou escolher terra/financiamento automaticamente.
5. Fazer logout; nova sessão/MFA; recuperar o mesmo dossier. Abrir a fonte e regressar ao dossier pelo contexto. Esperado: mesmo ID/fonte/dados, sem mistura. Numa conta alheia, o dossier é negado, mas o artigo permanece público.
6. Voltar ao artigo e escolher um dossier existente. Esperado: referência única após guardar/repetir, evento auditado e nova sessão recupera. Registar separadamente resultado apresentado, API e persistência.
## Expansão, ativação e reversão
1. Demonstrar o piloto na interface e corrigir falhas observadas; depois reconciliar as outras quatro páginas setoriais, artigos de âmbito territorial e anexos por identidade/versão/hash. Conteúdos editoriais permanecem distintos de oportunidades concretas.
2. Resolver relações territoriais com citações e IDs inequívocos; preservar divergências de âmbito. Reutilizar `portal_resource_links`, cadastros territoriais e mapas; não desenhar fronteiras pelo nome do corredor.
3. Completar cobertura histórica: sitemap misto, paginação, links externos, imagem404, datas/autorias não indicadas e conflitos de edição. Não publicar os50 artigos ou30.170 entradas automaticamente.
4. Preparar revisão candidata, mapa de URLs e importação autenticada no gestor existente. O CLI `documents:import-batch --apply` continua QA; não é uma ação de ativação. Upload JSON preparado apenas no ambiente QA, com hash/duplicados/quarentena já existentes. A disponibilização ativa exige aprovação própria e revisão da guarda QA, não apenas mudar uma flag.
5. Preservar sequência global **030000 →040000 →190000**, só instalar o que ledger/schema da base alvo demonstrar ausente. Leitura pública depende da biblioteca/documentos, não dos perfis modulares; associação ao percurso exige Balcão+190000. Conteúdo, cartografia e disponibilização são ações diferentes.
6. Contagem prevista do primeiro lote: **1 artigo +1 imagem**, reutilização por hash se já existirem, **3 associações fundamentadas**, **0 oportunidades criadas pelo artigo**, **0 geometrias importadas**, **0 dossiers/contas QA transferidos**. Contagens finais dependem da prévia autenticada no alvo.
7. Antes de ativar, renovar backup/restauro da **base ativa e revisão candidata** com ficheiros privados e chave estável. O ensaio QA não o substitui. Publicar cada recurso/camada/oportunidade separadamente; sem ativar cálculos.
8. Reversão: ocultar o piloto/desativar flag e reverter bundle compatível; retirar divulgação dos dois recursos numa ação auditada, sem eliminar originais/versões/associações históricas ou fontes já guardadas nos dossiers. Restauro de dados apenas do backup verificado, reconciliando escritas posteriores.
Perfis modulares e SLA continuam sujeitos a decisões próprias. C37, Promotor21 visual, A/B, workflow normativo integral, materialidade e restantes lacunas não são encerrados por este piloto.
## Verificação final deste lote
Leitura ativa renovada em10/10/2026, com transação READ ONLY: zero documentos do legado, piloto indisponível e bundle ativo inalterado. Ausentes exatamente as extensões `2026_10_09_030000_extend_opportunity_resources`, `2026_10_10_040000_create_portal_profile_authorization` e `2026_10_09_190000_prepare_investor_path`. Os190000 fiscal(03/10) eBalcão-base(07/10) instalados não substituem09/190000. Recibo `legacy-active-readonly.json`.
Persistência: um documento por hash; publicação e revisões documentais auditadas; ClamAV1.5.4 registado nos dois originais. Dossier criado e dossier continuado têm uma referência e um evento cada; projeto independente conserva zero referências/eventos. `legacy-persistence-qa.json`.
Restauro QA renovado em10/10/2026: **295 tabelas e81 ficheiros, zero ausentes**, base de restauro distinta `profile_restore_20261010_133520_test:55441`. Ensaio QA, sem substituir backup da futura base ativa/candidata. Recibo `profile-installation-backup-qa.json`. Build QA, TypeScript e análise estática passaram; oito testes específicos da biblioteca/percurso passaram, além dos ensaios HTTP por critério. Candidata não fixada nem ativação autorizada.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
<!-- qa-installation-cartography-20261010:begin -->
## Estado atual — instalador, documentos e cartografia
QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Ativo inspecionado apenas por leitura em 2026-10-10T10:32:41+00:00: 030000, 040000 e 09/190000 ausentes; zero geometrias; manifesto preservado. **C37, Promotor QA21 e A/B permanecem visualmente pendentes. Candidata à ativação ainda não fixada.**
QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Ativo inspecionado apenas por leitura em 2026-10-10T12:26:56+00:00: 030000, 040000 e 09/190000 ausentes; zero geometrias; manifesto preservado. **C37, Promotor QA21 e A/B permanecem visualmente pendentes. Candidata à ativação ainda não fixada.**
| Requisitos | Código preparado / prova QA | Critério ainda aberto |
| --- | --- | --- |
@@ -14,7 +14,7 @@ QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_tes
| OP-03 / OP-04 | Código da interface e resposta distinguem aprovação interna, disponibilização restrita e catálogo público. Casos #30 restrito, #33 público e #32 aprovado sem publicação recuperados pela API; consulta da política pública efetiva. | Interface; Response integral/§27 e condições sem regra inequívoca continuam parciais. Testes dos seis contratos não encerram a demonstração visual. |
| PROFILE-22/28 | [Quatro perfis e SLA com regra, fundamento, efeito e decisões concretas](propostas-perfis-modulares-sla.md). | Propostas **inativas**; quatro categorias continuam com rascunhos recuperáveis, mas submissão/publicação bloqueadas. Nenhum prazo ou responsável real configurado. |
Prova de recuperação QA: dump consistente → nova base `profile_restore_20261010_104956_test:55441` → **295 tabelas com linhas/hashes iguais e 69 ficheiros fisicamente recuperados por hash**; todos os ficheiros referenciados recuperados por hash. Arquivo privado inclui o runtime e a chave QA estável. Readiness só atesta preparação técnica desta revisão. Repetir após fixar a candidata e preparar backup ativo apenas numa futura ativação autorizada.
Prova de recuperação QA: dump consistente → nova base `profile_restore_20261010_123125_test:55441` → **295 tabelas com linhas/hashes iguais e 71 ficheiros fisicamente recuperados por hash**; todos os ficheiros referenciados recuperados por hash. Arquivo privado inclui o runtime e a chave QA estável. Readiness só atesta preparação técnica desta revisão. Repetir após fixar a candidata e preparar backup ativo apenas numa futura ativação autorizada.
**Matriz única:** 3.386 requisitos/linhas e 6.838 ocorrências de origem; todas as fontes, critérios e versões preservados; sem novos fechos globais. Contagens por estado abaixo e na projeção dos pedidos abertos.
@@ -1,5 +1,108 @@
<!-- qa-territory-resource-activation-20261010:begin -->
## C86/C87 — efeito e reversão, sem instalação nova
Recursos030000, perfis040000 e percurso09/190000 continuam instalados emQA; não repetir. Este lote altera validação cruzada país/hierarquia, preenchimento/conferência e legenda do componente existente; **0 migrations,0 tabelas e0 novas contas**. Dois recursos de contexto criados apenas emQA não serão transferidos ao ativo.
Fingerprint atual `dab3606aaaf4adaa3e279326d606160bda7c5d2eb9beebc234303f4c810c7ac3` diverge do backupQA18:19 (`f1e99482dd5080b77782507cc351b79981daf0294269b65757c356cf432edb78`); essa prova permanece datada e não habilita a candidata atual. Não repetir testes de C37 fechado. Fixar futura revisão após critérios independentes, conferir/regenerar provas de compatibilidade/backup/restauro correspondentes e aplicar somente instalações ausentes na sequência030000→040000→09/190000 por ação autenticada. Correções, importação cartográfica e publicação continuam operações distintas.
Compatibilidade: schema/IDs/JSON existentes preservados; registos contraditórios antigos permanecem recuperáveis com mensagem e correção explícita, sem migração silenciosa. Reversão do lote: código/build anteriores, conservando drafts/versões/auditoria e sem desfazer schema; dados gravados pelo lote são compatíveis com as tabelas anteriores. Reversão ainda deve ser ensaiada na futura candidata/clone do ativo. Zero escritas ativas; C37 fechado não equivale a ativação ou fechoA/B.
<!-- qa-territory-resource-activation-20261010:end -->
<!-- c37-manual-close-20261010:begin -->
## C37 encerrado — autenticação QA18 — 2026-10-10T20:26:29.902032+02:00
**Confirmação manual expressa do responsável registada. C37 está encerrado exclusivamente no âmbito da autenticação QA18.** Login → MFA → redirecionamento → sessão autenticada → atualização → logout → nova sessão/recuperação estão ligados às provas anteriores, sem repetir ensaios nem alterar conta, chave ou MFA. O logout manual de 20:17:15 foi confirmado pelo responsável e pelo rastreio: POST302→/, autenticação e estado persistido vazios.
Aceitação e hashes dos recibos: `c37-manual-close-20261010/acceptance.json`. Referências: `c37-qa18-chromium-20261010.json`, `logout-finalize-20261010/manual-qa18-logout.json`, `portal-logout-chromium-20261010.json` e recuperação do dossier em `investor-ab-chromium-20261010.json`. A prova QA21 do botão é complementar; a conta18 tem a sua própria confirmação. Horário local UTC+02; datas técnicas dos recibos conservadas em UTC.
O fecho **não encerra A/B, auditoria integral, móvel/acessibilidade integral, outros perfis ou ativação ativa**. Os cinco requisitos relacionados continuam parciais pelos seus outros critérios; contagens preservadas: 3393 requisitos/3393 linhas/6845 ocorrências. Menções históricas abaixo a C37/autenticação18 pendente são superadas somente por este fecho. C85 permanece corrigido em QA. Próximo trabalho independente: gravação/recuperação territorial e consistência país–hierarquia por consumidor, países/origem/estatísticas, documentos e workflow, preservando decisões próprias de perfis modulares/SLA.
<!-- c37-manual-close-20261010:end -->
<!-- qa-logout-activation-20261010:begin -->
## Incremento C85 — revisão QA, sem ativação
Menus de sessão adicionados ao fingerprint; preparação anterior ligada ao hash antigo preservada. Checagens e backup/restauro QA renovados em 2026-10-10T18:19:43+00:00 para o código atual: 295 tabelas/103 ficheiros em `profile_restore_20261010_181928_test:55441`; nenhum novo recurso, migração, titular ou grant necessário para o botão. Bundle ativo permaneceu com SHA256 `75352185c2a85c34ab726613db864a598e301a5b342ebbc80ac8e678eed17a94`.
Correção futura: disponibilizar componente/build compatíveis, após revisão candidata e ensaios correspondentes; preservar a ordem 030000→040000→09/190000 e executar somente instalações ausentes. Não repetir instalações QA por causa do botão. Reversão do incremento UI: regressar ao bundle/componentes anteriores, preservando dados e MFA; não desfazer schema nem recuperar uma chave distinta. Backup ativo READ ONLY de 17:48:52 UTC não é aceitação da nova candidata; verificar compatibilidade e reversão no clone e renovar o retrato antes de qualquer corte autorizado. C37 manual parcialmente confirmado; sem autorização de ativação ativa.
<!-- qa-logout-activation-20261010:end -->
<!-- qa-visual-activation-20261010:begin -->
## Ativação preparada, sem execução no ativo — 2026-10-10T17:57:54.604301+00:00
Leitura ativa `investemoz:5432`: flags de dashboard/perfis/percurso desligadas; migrações 030000, 040000 e **2026_10_09_190000_prepare_investor_path** ausentes. A extensão 09/190000 continua necessária e não foi substituída pelo Balcão 07/190000. As três já estão instaladas na QA; não repetir.
1. Concluir os critérios pendentes e fixar revisão, hashes, build e flags candidatos. O snapshot técnico atual não representa aceitação global.
2. Backup ativo renovado em 2026-10-10T17:48:52+00:00: 288 tabelas / cinco ficheiros, restauro SQL em `desk_restore_20261010_174843_test:55441`, dados e bytes do arquivo conferidos. Preparar aplicação e reversão das migrações e compatibilidade da candidata neste clone fresco. Repetir o snapshot se dados/revisão mudarem antes do corte. A prova QA separada é 295 tabelas / 103 ficheiros em `profile_restore_20261010_174750_test:55441`.
3. Disponibilizar código/build compatíveis com flags desligadas, preservando URLs, IDs, classificações, consentimentos, cinco fases e 19 etapas. Zero contas, dossiers ou oportunidades sintéticas transferidos.
4. Prévia e aplicação autenticadas **030000 → 040000 → 09/190000**, apenas se ausentes e com dependências coerentes. 030000 prepara recursos versionados. 040000 instala quatro tabelas, uma coluna em users e três em opportunities; não cria titulares ou grants. 09/190000 prepara duas colunas e uma definição; não cria dossiers ou tarefas. Conferir contagens na prévia real. Bootstrap separado, exatamente dois titulares reais ainda não designados.
5. Correções de interface, API e recuperação como lote próprio. Demonstrar C37 e dashboard por perfil; restauração de oportunidades continua por implementar e aceitar.
6. Conteúdos: prévia/hash/identidade/versão → armazenamento privado → ClamD → recuperação/replay. Lote de 22 originais (17 artigos / cinco imagens), reutilizando hashes existentes; 21 recursos divulgáveis e um exemplar vazio de Mercado Financeiro privado. Falta preparar e demonstrar o mecanismo autenticado de lote; CLI --actor QA não é autenticação ativa. Divulgação separada não ativa regras/cálculos.
7. Cartografia em lote independente: prévia, correspondências inequívocas, aplicação idempotente, auditoria e reversão. Retrato QA: 408 candidatas / 173 pendentes (26 distritos / 147 postos); Mandie/Guro ambíguo e ausência de polígonos de localidades no exemplar 2019. Publicação das camadas separada. Gravar/recuperar relações e demonstrar cada mapa consumidor.
8. Habilitar consumidores e percurso incrementalmente com permissões e provas próprias. `ready=true` significa preparação, não instalação, divulgação ou conclusão funcional.
**Reversão:** desligar flags e retirar divulgação por ações auditadas, preservando originais, versões, IDs, documentos, consentimentos, relações e eventos. Não executar DROP em tabelas utilizadas ou copiar fixtures QA. Restauro completo exige ensaio em QA distinta e reconciliação de escritas posteriores ao snapshot antes do corte. Não alterar chave nem retirar MFA para recuperar acesso. O backup ativo verificado não comprova ainda aplicação/reversão da futura candidata.
<!-- qa-visual-activation-20261010:end -->
# Plano consolidado de ativação — lote 3
<!-- qa-c71-sectors-result-20261010:begin -->
## Preparação atual após recuperação e lote setorial — 2026-10-10T16:23:25.695297+00:00
C71 corrigido emQA: comparação integral e backup/restauro renovado 2026-10-10T16: 22:44+00:00 (295 tabelas/103 ficheiros), em `profile_restore_20261010_162229_test:55441`. Esta prova não é backup do ativo nem fixa candidata. C37/QA18,A/B,móvel/acessibilidade/outros consumidores e decisões próprias permanecem pendentes.
1. Concluir as aceitações da revisão; fixar código/manifesto/configuração e hashes. A guarda atual do legado continua restrita aQA; código preparado não é habilitação ativa.
2. Conferir o alvo ativo por leitura e provar backup/restauro próprio dessa revisão, incluindo ficheiros e chave estável. O último retrato ativo anterior continua histórico.
3. Disponibilizar código/build compatíveis com flags desligadas; preservarURLs,IDs,roteiros/consentimentos. Não copiar contas ou dossiers sintéticos.
4. Prévia e aplicação autenticadas030000→040000→**2026_10_09_190000_prepare_investor_path**, apenas se ausentes e coerentes. QA já tem as 3: não repetir. O Balcão07/190 não substitui a extensão09/190; dependências/efeitos e bootstrap separado estão na tabela histórica abaixo.
5. Conteúdo: prévia/hash/identidade/versão, armazenamento privado eClamD. **22candidatos do lote (17 artigos+5 imagens)**;17 artigos com texto no total incluindo piloto anterior,1 exemplar vazio privado. Reutilizarbytes/identidades já existentes; qualquer conflito fica pendente. A CLI com--actor é apenas ensaioQA, não instalação autenticada ativa. Ainda falta preparar/demonstrar um ponto autenticado de importação emlote com as mesmas identidades; upload unitário não foi demonstrado equivalente.
6. Cartografia emlote distinto, por pais e correspondências inequívocas; retratoQA408 candidatas/173 pendentes, Mandie/Guroambíguo.0geometrias importadas nesta frente; medir novamente no alvo. Publicação das camadas separada.
7. Divulgar os conteúdos autorizados em ação própria: previsão21recursos deste lote; Mercado Financeiro permanece privado/vazio e nunca apresentado como página concluída. QA demonstrou divulgação autenticada do gestor14, sem presumir MFA não observado. Aprovação/consulta/cálculo/decisão externa são estados independentes.
8. Disponibilizar consumidores/percurso incrementalmente após provas;0fixturesQA transferidos. `ready=true` não instala, não publica e não fecha requisitos.
**Reversão:** retirar divulgação e ocultar consumidores/flags com auditoria, mantendo originais, versões, fontes dos dossiers eeventos. Preferir reconciliação/restauro numaQA distinta comdump íntegro/chave preservada antes de corte; não reativar o cluster danificado nem eliminar tabelas utilizadas. Conservados o original danificado e a cópia física de 75575 ficheiros (nomes/tamanhos/hashes iguais), sem afirmar restauro lógico de todos os históricos no novo runtime. Plano não executado na base ativa.
As secções abaixo preservam a evolução anterior; referências aC71aberto ou lote apenas preparado foram superadas pelas provas atuais, sem apagar dependências independentes.
<!-- qa-c71-sectors-result-20261010:end -->
<!-- legacy-manual-sectors-20261010:begin -->
## Complemento após confirmação manual — preparação QA
O relato manual do piloto confirma somente a sequência registada em AUD-LEG-03/04/06/07. C37/QA18, A/B, outros perfis, móvel/acessibilidade e cobertura integral permanecem pendentes. C71 continua a impedir novas aplicações QA e a fixação de candidata.
O lote seguinte tem previsão de **17 artigos +5 imagens**, biblioteca existente e zero oportunidades/territórios/contas criados. A prévia READ ONLY encontrou22 candidatos novos; reconfirmar contagens/identidades no alvo após recuperação. Arquivo do exemplar vazio de Mercado Financeiro não significa artigo educativo completo. Relações do Turismo são textuais/editoriais; não importam geometrias.
Sequência global030000 →040000 →09/190000 preservada, sem repetir instalações. Após recuperar QA: aplicação privada/scan, parametrização dos consumidores e divulgação separada; depois ensaios específicos. Ativação futura requer revisão candidata, prova da base ativa por leitura e backup/restauro próprio, além da autorização separada. Reversão conserva originais, versões, referências e eventos; retirar divulgação/desabilitar disponibilização não elimina fontes dos dossiers. Nenhuma aplicação ativa nesta frente.
<!-- legacy-manual-sectors-20261010:end -->
<!-- qa-legacy-browser-20261010:begin -->
## Atualização após a prova visual do piloto
O piloto legado foi demonstrado no Chromium QA com Promotor QA21; C37/QA18, CRUD de oportunidades no dashboard e A/B completos continuam pendentes. Não fixar uma revisão candidata global apenas por esta prova. A identificação “InvesteMoz QA” é compilada somente no build QA; não transferir esse rótulo automaticamente para produção.
Preparação preservada: corrigir critérios globais→fixar revisão/hashes/configuração→conferir base ativa por leitura e renovar backup/restauro correspondente→código/bundle compatíveis com flags desligadas→030000→040000→09/190000 apenas se ausentes→importações privadas por prévia/hash/ClamD→divulgação por recurso/camada→disponibilização do percurso. Fonte editorial pública usa biblioteca; guardar contexto requer Balcão/09-190. Cartografia continua lote distinto.
Contagens do legado inalteradas: um artigo + uma imagem, três associações fundamentadas, zero oportunidades/geometrias criadas pelo artigo e zero contas/dossiers QA transferidos. Reversão: ocultar piloto/flag e usar bundle compatível; retirar divulgação em ação auditada preservando originais, versões, relações e histórico. Ensaio/restauro QA não substitui backup da base ativa/revisão candidata; nenhuma ativação autorizada.
C71 é agora pré-condição técnica: recuperar o cluster QA e renovar backup/restauro, retomar as escritas e repetir os percursos afetados; depois fixar candidata e preparar a prova específica do alvo ativo. O restauroQA anterior13:35 permanece histórico; readiness invalidada. A falha nova não autoriza alterar ou reiniciar a base ativa.
<!-- qa-legacy-browser-20261010:end -->
<!-- qa-legacy-20261010:begin -->
## Histórico — complemento legado anterior ao ensaio Chromium
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ão candidata/config/bundle→backup/restauro da base ativa correspondente→upload/importação autenticados como rascunhos privados→hash/duplicados/ClamD/metadados→associações fundamentadas→divulgação explícita por recurso→disponibilização pública/percurso. O CLI importador só aplica QA; a guarda atual do piloto impede ativo, mesmo com flag true. Habilitação ativa requer revisão/autorização própria.
Contagem prevista: **1 artigo +1 imagem**, **3 associações**, **0 oportunidades ou geometrias criadas pelo artigo**, **0 contas/dossiers QA transferidos**; reutilização se hash/ID já existirem, conflitos aguardam revisão. Cartografia é outro lote, sem publicação automática. Reversão: ocultar a flag/usar bundle compatível e retirar divulgação auditada; preservar originais, versões, fontes dos dossiers e histórico. Backup QA não substitui prova da base ativa + candidata. Decisões de perfis/SLA, C37, Promotor21 visual e A/B continuam abertas.
<!-- qa-legacy-20261010:end -->
<!-- qa-response-20261010:begin -->
**Preparação da futura ativação.** o restauro reportado é QA. Para uma futura ativação, conferir a identidade, schema, dados e configuração efetivos da base ativa para a revisão candidata, capturar hashes/manifesto dessa revisão e provar o seu backup/restauro. As provas QA ou `ready=true` não substituem essa ligação. Sequência 030000 → 040000 → 190000; C37, QA21 e A/B visual pendentes, candidata não fixada.
<!-- qa-response-20261010:end -->
<!-- qa-installation-cartography-20261010:begin -->
## Sequência reconciliada — preparada, não executada no ativo
@@ -1,6 +1,39 @@
# Perfis modulares e SLA — propostas para decisão
10/10/2026. Complemento da matriz existente, sobretudo AUD-OP-04, PROFILE-22/28/31. **Nenhuma proposta abaixo está aprovada, instalada como perfil ativo ou aplicada aos prazos.** Não designa pessoas reais. A criação e recuperação dos quatro rascunhos já existe; submissão e publicação continuam bloqueadas até aprovação do perfil e implementação demonstrada do adaptador correspondente.
10/10/2026. Complemento da matriz existente, sobretudo AUD-OP-04, PROFILE-22/28/31. O texto original abaixo fica preservado como proposta histórica; o ensaio técnico QA seguinte identifica as recomendações adotadas experimentalmente, sem decisão formal de ativação. Não designa pessoas reais nem constitui aprovação ativa ou aceitação visual.
## Recomendações ensaiadas tecnicamente em QA — decisões próprias pendentes
Fonte: resposta do responsável após leitura integral desta proposta, com recomendações D-M1–M4, D-S1/S2 e D-R1. 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. O contrato original de 110 definições e os registos anteriores permanecem intactos. As extensões usam o adaptador e o JSON canónico já existentes, sem novas tabelas de identidade ou oportunidades.
| Decisão | Reconciliação vigente em QA | Pendência concreta |
| --- | --- | --- |
| D-M1 | Aceites os quatro contratos como primeira versão QA. Aprovação funcional por competência é separada da administração de segurança; revogada nesta proposta a obrigação de ambos os aprovadores funcionais serem superutilizadores. As atribuições técnicas/jurídicas/editoriais existentes continuam aplicáveis. | Cerimónia autenticada de aprovação/ativação de versões e correspondência final das competências funcionais/governança. A dupla de segurança não é alterada ou automaticamente designada para funções de produto. |
| D-M2 | Intenção/chamada pode ser disponibilizada com autorização de divulgação e rótulo explícito de preparação. Direito declarado atribuído exige o ato competente. | Demonstração visual e documentos reais específicos numa futura utilização autorizada; o exemplar sintético não é um ato oficial. |
| D-M3 | Condições propostas exigem cenário, fonte e data. Compromisso assumido exige documento; indicação de um valor não comprova financiamento assegurado ou contrato. | Demonstração visual de cada modalidade e dos respetivos rótulos. |
| D-M4 | Ideia sem subtipo aprovado continua guardável e desenvolvível; submissão exige classificação aprovada no servidor. | Classificação funcional dos subtipos reais. O subtipo `qa-test-subtype` é apenas uma fixture sintética de aceitação, não aprovação de um objeto real. |
| D-S1 | Ausência de histórico e projeções identificadas são aceites por extensão QA v2 explícita; não exigir nem inventar demonstrações inexistentes. | Demonstrar o percurso completo na interface e manter a escolha explícita da versão. |
| D-S2 | Obrigatoriedade por tipo e reabertura apenas das revisões afetadas por alterações substantivas. Correção ortográfica não deve reiniciar todas as revisões. | Implementar e demonstrar classificação/reutilização auditada por revisão. O comportamento anterior que invalida todas as revisões ao guardar ainda não é declarado corrigido por este lote. |
| D-R1 | Preparar e testar recuperação em QA com identidades sintéticas e segregação. | Habilitação ativa depende da designação formal das pessoas responsáveis; sem bypass, terceiro titular ou escolha de pessoas reais. |
### Procura e oferta — contratos QA explícitos
- **Financiamento / `seeking_funding`:** projeto representado, finalidade e plano financeiro; reutiliza `total_investment`, `investment_breakdown`, `secured_financing`, `funding_sought` e moeda. Rubricas reconciliadas, assegurado ≤ total e pedido positivo ≤ necessidade remanescente. Financiamento assegurado positivo exige evidência.
- **Financiamento / `funding_offer`:** entidade financiadora representada, capacidade oferecida positiva documentada, instrumento, critérios de elegibilidade e calendário/condições. Não aplica `pedido ≤ necessidade do projeto` e não declara um beneficiário aprovado. Não misturar o cenário de procura nesta ficha; limpar dados apenas por edição explícita, preservando histórico.
- **Fornecimento / `purchase_request`:** comprador/requisitante é o Promotor representado; objeto, quantidade/unidade, destino, calendário, critérios de aceitação e preço total ou método de cotação.
- **Fornecimento / `supply_offer`:** fornecedor é o Promotor representado; mantém especificação, quantidade, destino/calendarização, condições e critérios de aceitação; exige evidência da capacidade declarada. Não representa um pedido de compra nem um contrato celebrado.
- Em ambas as modalidades comerciais, valores preenchidos conservam moeda e condições propostas/assumidas identificadas. Quantidade × preço unitário é reconciliada quando ambos e o total estão declarados; impostos ficam separados e não são calculados pela categoria.
- Concessão distingue `intention_call` de `awarded_right`; “outros” usa `classified_other` com subtipo configurado e aprovado no servidor. O Promotor não pode definir gates ou autoaprovar um subtipo no pedido.
Versões novas: `concession-qa-20261010-v1`, `financing-qa-20261010-v1`, `supply-qa-20261010-v1`, `other-qa-20261010-v1`; histórico financeiro: `business-history-qa-20261010-v2`. A escolha é explícita. Rascunhos com os identificadores anteriores não são convertidos silenciosamente. A extensão é limitada por flag e identidade efetiva PostgreSQL de QA na porta 55441, ambiente de testes e base `*_test`; não pode ser habilitada na base ativa só por mudar a flag.
### SLA: experimento autorizado, compromissos ainda pendentes
Os prazos abaixo podem ser experimentados como configuração proposta em QA. Calendário, feriados, responsáveis/suplentes reais e compromissos operacionais não estão aprovados/configurados; não preencher `due_at` real por inferência. Pausas continuam por tarefa afetada, com espera externa separada e sem prometer prazo de entidade externa.
**Incidente comum:** proposta de triagem em um dia útil. **Incidente crítico:** resposta/triagem imediata, sem aguardar o relógio dos casos comuns; conter o risco por ação autorizada, registar motivo, alertar e apresentar próxima ação. A definição operacional de criticidade, canais e escala de responsáveis permanece por configurar; não inventar tempo de resolução ou pessoa de prevenção.
## Proposta original — histórico e divergências preservados
## Fundamento e compatibilidade
@@ -47,6 +80,8 @@ Regras comuns propostas: referências documentais pertencem à mesma oportunidad
## SLA operacional proposto — não é prazo legal
Os prazos abaixo são **propostas de compromissos internos de atendimento do portal**, não prazos de autoridades, financiadores ou outras entidades externas. A janela de 15 dias para correção pelo Promotor é uma proposta operacional de resposta/lembrete, não prazo legal nem motivo de rejeição automática. A espera externa é acompanhada separadamente; não prometer data de decisão externa nem consumir o relógio interno da tarefa pausada.
Versão proposta `opportunity-sla-proposal-20261010-v1`. **Nenhuma data `due_at` foi preenchida por esta proposta.** Dias úteis são uma configuração a aprovar, com fuso `Africa/Maputo`, calendário e feriados explicitamente registados; não inventar calendário oficial. Relógios por tarefa, não um prazo único que bloqueia todo o percurso.
| Tarefa | Prazo proposto | Responsável funcional | Início, pausa e escalamento propostos |
@@ -78,3 +113,5 @@ O estado técnico legado `published` é preservado; o texto da interface também
Após decisão D-M1–M4: registar versões imutáveis pelo serviço de versões existente, declarar bindings e gates no adaptador sem cadastros paralelos; editores por categoria; testes de campos parciais/422/novas sessões; Promotor → correção → aprovação independente → disponibilização por âmbito; organização alheia e múltiplos papéis negados. Cada nova versão mantém migração/adaptador compatível e reversão por desativação de disponibilização, preservando o snapshot submetido. D-S1/S2 e SLA são decisões independentes.
Instalação de recursos/perfis/percurso permanece **030000 → 040000 → 190000**. Instalar schema não aprova este documento, não atribui perfis, não inicia SLA e não publica geometrias. C37, dashboard Promotor QA21 e cenários A/B exigem interface; nenhuma contagem global de testes os encerra.
O backup/restauro demonstrado refere-se exclusivamente à base QA. A preparação da futura ativação deve conferir a base ativa efetiva e a revisão candidata correspondente — código, dependências, configuração e manifesto — e verificar o backup/restauro dessa base e dos seus originais sob essa revisão. O ensaio QA não comprova esse passo futuro; a candidata permanece por fixar e a ativação continua separada.
@@ -1,9 +1,172 @@
<!-- qa-territory-resource-c86-c87-20261010:begin -->
## Lote territorial C86/C87 — 2026-10-10T20:48:06.634373+02:00
**C37 encerrado somente para autenticação QA18, por confirmação manual expressa. Não foi repetido.** Aceitação ligada aos recibos de logout/recuperação em `c37-manual-close-20261010/acceptance.json`. A/B, auditoria integral e ativação continuam independentes.
| Requisito/caso | Correção e prova QA | Limite do fecho |
| --- | --- | --- |
| OP02; GEO01/02; C86 | API antes aceitava PT com província MZ por200; agora422 junto a country_code, preservando versão/payload/auditoria. UI limpa somente a seleção administrativa no preenchimento ao mudar país; estrangeiro incompleto pode guardar e recuperar. Conferência deixou de exigir província/distrito MZ a Portugal. | Aplicado a recursos de localização; não encerra origem, filtros ou estatísticas das solicitações. Registos anteriores não foram migrados/corrigidos silenciosamente. |
| GEO01/02/03 | QA21 oportunidade51: MZ→CaboDelgado→ANCUABE→guardar→nova sessão→campos e mapa recuperados→F5. Distrito `5b03104f-9c47-5542-8de3-e81b8d5fdd55`; dois limites disponíveis e dois polígonos renderizados. Segundo recurso PT recuperado sem IDs MZ. Troca de pai limpa descendentes. | Demais consumidores/projetos/clusters/corredores e níveis inferiores não encerrados. Tiles externos bloqueados no Chromium; mapa base não demonstrado. Não houve nova importação cartográfica ou inferência de parcelas/direitos. |
| OP02; autorização | QA23 organização alheia: consulta403 e escrita403. PostgreSQL QA READ ONLY confirmou os2recursos v1 e o parentesco geográfico; ligações territoriais aceites permaneceram 0. | Outros perfis, documentos/derivadas e lifecycle/workflow conservam provas/pendências próprias. |
| GEO03; C87 | Lista repetia “limites oficiais por verificar” mesmo para administrativos do INE. Texto agora distingue edição administrativa de parcela; fonte e edição2019-06-07 preservadas. Popup já distinguia o propósito, lista foi alinhada. Chromium390px e imagem inspecionada. | A aceitação oficial não foi reaberta. A edição não afirma atualidade jurídica ou disponibilidade de investimento. Outras camadas e geometria de parcela têm critérios próprios. |
Provas sanitizadas: `territory-resource-chromium-20261010.json`, `territory-resource-persistence-qa.json`, `territory-resource-map-shot-20261010.json` e regressões `location-country-*qa.json`. Suite de recursos5/71 passou antes do ajuste de conferência; regressão específica final1/20 passou depois. TypeScript passou após o seletor; compilação QA finalexit0. Contagens de testes não substituem os percursos acima. Falhas do harness (versão Inertia omitida, seletor por atributo incorreto e caixa dos nomes) foram preservadas como tentativas; não são falhas do portal. Nome oficial ANCUABE não foi renomeado.
Mesma matriz:3393requisitos/3393linhas/6845ocorrências; quatro requisitos recebem subcritérios, sem alteração de estados globais. Países/origem/estatísticas restantes,173 correspondências anteriores com Mandie/Guro ambíguo, documentos, Response/§27, workflow/restauração e decisões modulares/SLA permanecem abertos. Não iniciar novas jornadas educativas.
**Código preparado e demonstração QA, sem disponibilidade ativa.** Zero migrações/instalações novas e zero escritas ativas. Manifesto ativo SHA256 `75352185c2a85c34ab726613db864a598e301a5b342ebbc80ac8e678eed17a94` intacto. O backup/restauro QA18:19 liga-se ao fingerprint anterior; não foi apresentado como prova da revisão nova. Para futura candidata: renovar verificações/backup/restauro associados à revisão, testar compatibilidade/reversão no clone do ativo e preservar **030000→040000→09/190000**, sem repetir instalações QA. O backup ativo READ ONLY17:48 permanece histórico; nenhum corte autorizado.
<!-- qa-territory-resource-c86-c87-20261010:end -->
<!-- c37-manual-close-20261010:begin -->
## C37 encerrado — autenticação QA18 — 2026-10-10T20:26:29.902032+02:00
**Confirmação manual expressa do responsável registada. C37 está encerrado exclusivamente no âmbito da autenticação QA18.** Login → MFA → redirecionamento → sessão autenticada → atualização → logout → nova sessão/recuperação estão ligados às provas anteriores, sem repetir ensaios nem alterar conta, chave ou MFA. O logout manual de 20:17:15 foi confirmado pelo responsável e pelo rastreio: POST302→/, autenticação e estado persistido vazios.
Aceitação e hashes dos recibos: `c37-manual-close-20261010/acceptance.json`. Referências: `c37-qa18-chromium-20261010.json`, `logout-finalize-20261010/manual-qa18-logout.json`, `portal-logout-chromium-20261010.json` e recuperação do dossier em `investor-ab-chromium-20261010.json`. A prova QA21 do botão é complementar; a conta18 tem a sua própria confirmação. Horário local UTC+02; datas técnicas dos recibos conservadas em UTC.
O fecho **não encerra A/B, auditoria integral, móvel/acessibilidade integral, outros perfis ou ativação ativa**. Os cinco requisitos relacionados continuam parciais pelos seus outros critérios; contagens preservadas: 3393 requisitos/3393 linhas/6845 ocorrências. Menções históricas abaixo a C37/autenticação18 pendente são superadas somente por este fecho. C85 permanece corrigido em QA. Próximo trabalho independente: gravação/recuperação territorial e consistência país–hierarquia por consumidor, países/origem/estatísticas, documentos e workflow, preservando decisões próprias de perfis modulares/SLA.
<!-- c37-manual-close-20261010:end -->
<!-- qa-logout-results-20261010:begin -->
## Logout disponível e confirmação manual — 2026-10-10T18:20:44.687863+00:00
**C85 corrigido em QA; nenhuma alteração no ativo.** A ação existia no menu de utilizador do dashboard, mas não na navegação do investidor. O cabeçalho comum mostra agora a conta atual e **Terminar sessão**, sem obrigar a regressar ao dashboard. Reutiliza POST `/logout` do Fortify e limpa o cache Inertia; chaves, MFA, perfis e dados guardados foram preservados.
O responsável confirmou o botão e o logout. Rastreio QA18 às **2026-10-10T18:17:15+00:00** (20:17:15 em Moçambique): POST logout302→/, conta18→sem autenticação e estado persistido vazio. C37 recebe apenas esse critério; F5 explícito e novo login/MFA QA18 após a saída continuam pendentes. O GET/login anterior com conta autenticada não era um novo login. A declaração geral de testes efetuados não substitui evidências dos restantes critérios.
Chromium QA21, contextos isolados: ação visível em investimentos, dossier, artigo e simulador; logout direto do dossier em desktop e 320px; API privada401 e página privada→login; nova sessão com MFA normal recuperou case/results/steps/tasks com hash igual e F5 manteve acesso. Seis verificações de cabeçalho em 320/390/1440px sem violações automáticas ou overflow; foco por teclado demonstrado. Não consumiu códigos QA18. Não encerra A/B, leitor de ecrã ou acessibilidade integral.
Provas sanitizadas: `portal-logout-chromium-20261010.json` e `logout-finalize-20261010/manual-qa18-logout.json`. Cinco requisitos receberam subcritérios; permanecem **3393 requisitos/3393 linhas/6845 ocorrências**, com estados globais inalterados.
Regressão de instalador/publicação: 9 testes/93 asserções e TypeScript passaram. Backup/restauro QA renovado **2026-10-10T18:19:43+00:00**, 295 tabelas/103 ficheiros, em `profile_restore_20261010_181928_test:55441`, associado ao novo fingerprint que inclui os dois menus. O backup ativo READ ONLY de 17:48:52 UTC permanece um retrato anterior; candidata global, compatibilidade/reversão no clone do ativo e ativação continuam pendentes. Preservar **030000 → 040000 → 09/190000**, sem repetir instalações QA nem copiar contas/dossiers de teste.
As afirmações históricas abaixo sobre logout manual ainda por testar são superadas somente por esta confirmação; nenhuma aceitação adicional inferida.
<!-- qa-logout-results-20261010:end -->
<!-- qa-visual-results-20261010:begin -->
## Ensaios visuais e pendências atuais — 2026-10-10T17:57:54.604301+00:00
Trabalho exclusivo na QA da porta 18097, base `desk_restore_20261009_174726_test:55441`. Nenhuma alteração na base ativa. A mesma matriz mantém **3393 requisitos / 3393 linhas / 6845 ocorrências de origem**: 3235 parciais, 145 não implementados, 5 dependentes de decisão, 1 implementado mas não ativado e 7 concluídos e demonstrados. Os estados globais não mudaram; foram atualizados subcritérios com provas próprias. A ausência de requisitos globalmente classificados “com falha” não significa ausência de casos abertos.
| Requisito | Demonstrado em QA | Critérios ainda pendentes |
| --- | --- | --- |
| C37; UI01; PROFILE24–25 | MFA manual QA18 aceite às 17:40:21 e 17:46:42 UTC; autenticação persistida e dashboard 200. Responsável confirmou acesso. O 403 inicial era do relatório administrativo reservado. Mensagem e retorno ao painel corrigidos; Chromium QA21 provou recusa 403, retorno e F5 sem perder sessão. | F5, logout e novo login no navegador habitual QA18. Não declarar o loop anterior resolvido por inferência: o rastreio não demonstra um ciclo persistente. Uma atualização Inertia 409 isolada não é perda de autenticação. |
| A/B; GUIDE01; SIM02; DESK01–02; CRM01 | Chromium desktop QA18: criação → gravação → nova sessão → recuperação → aplicabilidade → ferramenta → resultado no dossier → preparação no Balcão → acompanhamento interno → nova recuperação. Cinco fases e 19 etapas preservadas. A dispensa S10; B aguarda informação externa em S10 enquanto S02 avança. Resultado: equilíbrio de 20 unidades e défice A=0 / B=70.000 MZN. QA23 recebe 403/404; gestor não vê resultados financeiros privados. | Balcão demonstrado em preparação DRAFT, sem submissão ou decisão externa. Restantes critérios especializados, fiscalidade, documentos e consumidores não encerrados. Repetição final A/B móvel ainda necessária. |
| DASH01; PROFILE05/32 | QA21, a partir do dashboard: “Minhas oportunidades” e “Nova oportunidade” → rascunho incompleto #50 → guardar → nova sessão → recuperar → editar. Promotor solicita arquivo; Publicador QA25 arquiva com fundamento; estado recuperado. | Restauração por perfil ausente no workflow. Definir e implementar ator/regra coerentes sem conceder arquivo direto ao Promotor por inferência. Workflow e contratos especializados ainda exigem demonstrações próprias. |
| COUNTRY01–04/06/07 | Residência PT guardada no perfil e recuperada após F5. Negócio e fundiário DRAFT: país PT / origem FR guardados e recuperados. Balcão A/B: PT / ZA recuperados noutra sessão. Quatro formulários públicos apresentam 249 países cada. Gestão: filtros ZA/unknown, quatro manifestações sem origem e zero pedidos submetidos ZA, com rascunhos excluídos. | Nos formulários públicos, a presença de opções não prova submissão ou recuperação. Concluir cada gravação e estatística positiva de origem conhecida no UI. Nos rascunhos de negócio/fundiário, F5 não substitui nova sessão. |
| GEO01–03 | Oportunidade própria QA21 #51 e criação de projeto, cluster e corredor: Cabo Delgado registado → API com uma geometria acessível → um polígono SVG visível em cada consumidor. | Seleções desse ensaio não guardadas. Recuperar relações por consumidor e testar descendentes, restantes mapas e perfis. Tiles externos bloqueados no ensaio, sem confirmação da base externa. 408 associações anteriores e 173 pendentes preservadas; Mandie/Guro ambíguo. |
| LEG02/03/06/07; C77–C83 | Corrigidos landmarks, ARIA, hierarquia de títulos, reflow, foco das tabelas e idioma HTML pt-MZ em QA. Quinze execuções dirigidas de axe, em 320/390/1440px, sem violações automáticas ou overflow. Três páginas com skiplink primeiro e tabelas financeiras operadas por teclado. | Cobertura integral, leitor de ecrã, dispositivos reais, mais perfis e editor C74. O dossier QA21 verifica o componente móvel corrigido, sem substituir a repetição final A/B. |
| DOC01; C84 | Retirada mensagem global falsa de ClamD pendente; scan e motor apresentados por ficheiro/versão. Os 22 scans reais anteriores permanecem demonstrados. | PDFs, exportações, partilhas, derivadas e operações restantes exigem provas próprias. Um original limpo não comprova a derivada. |
| OPS01 | Backup QA 2026-10-10T17:48:11+00:00: 295 tabelas e 103 ficheiros restaurados. Ativo READ ONLY, backup 2026-10-10T17:48:52+00:00: 288 tabelas restauradas numa base QA distinta e cinco ficheiros conferidos no arquivo. Instalador/publicação: 9 testes / 93 asserções, além de TypeScript. | Aplicação, compatibilidade e reversão da futura revisão candidata no clone fresco do ativo. Revisão candidata global e ativação continuam por fixar. Contagens de testes não substituem critérios funcionais. |
**C80 corrigido:** a resposta tardia do GET do rascunho podia substituir preenchimento já iniciado. Edição e avanço ficam bloqueados até a recuperação terminar; o retorno exige o snapshot completo guardado. Chromium com resposta deliberadamente atrasada comprovou bloqueio, recuperação e libertação. A/B recuperaram os inputs corretos noutra sessão, preservando versões anteriores.
**Recusa manual do mapa às 17:52:33 UTC:** o pedido à oportunidade 51 foi feito com QA18, não QA21; recebeu 403 com sessão preservada. É uma prova de autorização, não de apresentação do mapa. Usar QA21 (`profiles-v1-promoter@example.invalid`) para esse consumidor; o Chromium anterior QA21 recebeu 200. Nenhuma permissão alargada para contornar a recusa.
**Provas privadas sanitizadas:** `investor-ab-chromium-20261010.json`, `promoter-dashboard-chromium-20261010.json`, `consumers-mobile-accessibility-chromium-20261010.json`, `visual-followup-chromium-20261010.json`, `qa-report-access-chromium-20261010.json`, `c37-qa18-post-mfa-destination-20261010.json`, `visual-persistence-qa.json`, `profile-installation-backup-qa.json` e `virtual-desk/backup-proof.json`. A tentativa antiga de QA21 na oportunidade 30 recebeu 403 e não contou como mapa; foi substituída pelo consumidor próprio 51. Tentativas e falhas de harness preservadas; sem códigos, segredos ou cookies nas evidências entregáveis.
Mercado Financeiro permanece privado, sem corpo inventado; rotas públicas 404. Energia e Mineração mantêm artigos distintos e Turismo não criou associações geográficas automáticas. Fontes oficiais aceites não foram reabertas. Próximo lote: C37 manual, restauração por perfil, submissão/estatística por formulário, gravação/recuperação territorial, documentos, Response/§27 e workflow. Perfis modulares/SLA continuam decisões separadas; não iniciar novas jornadas educativas.
As versões históricas abaixo ficam preservadas. Afirmações antigas sobre falta de navegador, A/B não testados ou ClamD global pendente são superadas apenas pelos subcritérios acima. Código preparado, QA demonstrada e disponibilidade ativa são estados distintos.
<!-- qa-visual-results-20261010:end -->
# InvesteMoz — relatório executivo do lote 3
<!-- qa-c71-sectors-result-20261010:begin -->
## Resultado atual — 2026-10-10T16:23:25.695297+00:00
**QA18097 recuperada; nenhuma alteração na base ativa.** C71 técnico corrigido: faltavam26 ficheiros de catálogos partilhados, incluindo `global/2964`, necessários ao `pg_dump`. O agente/processo que os removeu continua desconhecido. O cluster danificado foi preservado e substituído por cluster novo em diretório estável, apenas no alvoQA55441. As 295 tabelas/34.902 linhas e 31 sequências iniciais conferiram; schema/owners/ACL/triggers equivalentes após normalização explícita de 2 constraints varchar/text e slots de colunas removidas. A cópia física retida confere em 75575 ficheiros por nomes, tamanhos ehashes; isso não afirma restauro lógico de todos os bancos históricosQA.
**Backup/restauro posterior ao lote:** 2026-10-10T16: 22:44+00:00,295 tabelas e 103 ficheiros privados conferidos em `profile_restore_20261010_162229_test:55441`. APP_KEY, MFA e contas preservados. Não é prova de backup da base ativa nem fixa candidata global. QA/login voltou aHTTP200; manutenção arquivada. 2FAS Auth continua compatível com TOTP; a rejeição manualQA18 registada foi de palavra-passe antes do MFA, sem justificar troca de aplicação.
| Requisito / caso | Evidência deste lote | Lacuna concreta |
| --- | --- | --- |
| AUD-OPS-01 / C71 | Cluster saudável, dados/sequências/permissões preservados, backup/restauro renovado,0escritasativas. Estado global passa de com falha para parcial. | C37/QA18,A/B e outros consumidores; candidata e backup ativo/autorização própria. |
| AUD-LEG-01/05/07 |22 ficheiros (17 artigos+5 imagens) importados privadamente, hashes recuperados,22 scans ClamD reais; replay sem duplicações/eventos/scans novos. |32 das 50 versões observadas continuam não importadas; sitemap misto/paginação/links externos/C68 e cobertura integral. |
| AUD-LEG-02/03/06 / C72 |16 artigos novos com texto públicos, título deInfraestruturas preservado. Chromium desktop: imagens/pesquisa/artigos→MFA21→4 dossiers→retorno→logout→nova sessão→F5/replay. HTTP e persistência separados,8 dossiers no total. | Móvel/acessibilidade/mais perfis e percursosA/B completos não encerrados. |
| AUD-LEG-04 |Leitura pública; guardar requer login. Anónimo/QA23 sem contexto privado; QA23recebe403 ao dossier alheio. Estado de consulta/proteçãoQA mantido concluído no âmbito demonstrado. | Disponibilização ativa e demais aceitações são independentes. |
| Mercado Financeiro |OrigemHTML e respostaInertiaHTTP200 contêm corpo vazio; exemplar22 no lote fica privado na biblioteca (recurso23). `/mercado-financeiro` e respetiva exploração404,sem página vazia apresentada como entrega. | Obter conteúdo real/versão com texto; não usar Fintech nem inventar corpo. |
| C74 |Imagens semAssunto eram recusadas porPUT422. QA aceita tema vazio sem inferir setor; interface tornaAssunto opcional. Cinco metadados vazios guardados/recuperados; teste dedicado1/11. | Editor administrativo sem ensaio visual próprio; alteração protegida pela guardaQA. |
**Contagens canónicas:**3393 requisitos/3393 linhas, 6845 ocorrências de origem; 3235 parciais, 145 não implementados, 5 dependentes de decisão, 1 implementado mas não ativado, 7 concluídos e demonstrados, 0 com falha. Casos adicionais e aceitações abertas continuam visíveis dentro dos requisitos parciais;0 com falha global não significa portal sem erros. BacklogCSV é projeção reconciliada dos mesmos3393 IDs, incluindo39 anteriormente omitidos, não outra matriz.
**RotasQA:** `/explorar/energia-mineracao` → `/energia-mineracao`, `/energia`, `/mineracao`; `/explorar/turismo` → `/turismo` e 11 artigos territoriais; `/explorar/infraestruturas` → `/infraestruturas`. Agronegócio preservado. Energia/Mineração são fontes distintas; Turismo mantém texto editorial e 0associações territoriais automáticas. Todos os dados de fonte/autoria/datas/hash/URLs/versões originais são conservados. Os artigos não criaram oportunidades (49antes/depois), geometrias ou perfis. Divulgação foi uma ação autenticada do gestorQA14 pelo mecanismo existente, separada da importaçãoCLIQA (atribuição `--actor`, não autenticação ativa); nenhum desafioMFA foi observado nessa conta. QA21/QA23 usaramMFA normalmente.
**Próximo trabalho executável:** restantes32 versões/acervo eC68 por ficheiro; Response/§27/validações, PDFs/derivadas epaíses/origem/estatísticas por consumidor;173 correspondências territoriais ainda abertas comMandie/Guroambíguo. Não acrescentar jornadas educativas para contornar essas pendências. Perfis modulares eSLA conservam decisões próprias. C37/QA18,A/B,móvel/acessibilidade permanecem abertos; este lote não demonstraCRUD/workflow de oportunidades nem os encerra.
**Ativação futura:** revisão/hashes/flags e aceitação global→leitura/backup/restauro do ativo correspondente→código/build compatíveis com flags desligadas→030000→040000→09/190000 somente se ausentes→importação privada de conteúdo/cartografia com prévia e scan→divulgação separada→disponibilização incremental. QA já tem as 3 instalações;não as repetir. Previsão desta extensão: 22 originais (reutilizar hashes existentes),21 potencialmente divulgáveis e 1 exemplar vazio privado;0 contas/dossiers/opportunities/geometrias transferidos. O CLIrecusa aplicação ativa: falta preparar/demonstrar uma ação autenticada emlote que preserve exatamente as identidades/versionamento existentes; upload unitário não foi provado equivalente. Cartografia:408 candidatas/173 pendentes no retratoQA anterior, não importadas por esta frente. `ready=true` continua preparação, não instalação/disponibilização.
**Reversão:** retirar divulgação em ações auditadas e ocultar consumidores/flag, preservando originais, versões, consentimentos/referências eeventos. Restauro técnico usa o novo dump íntegro e chave estável numa QA distinta,comparação antes de corte;não voltar ao cluster danificado. Nunca drop de tabelas utilizadas nem copiar fixturesQA para o ativo. Provas: `legacy-sectors-{apply,replay,http,browser,persistence}-qa.json`, `legacy-financial-origin-20261010/proof.json`, `portal-qa-recovery/20261010_154445/*-proof.json` e `profile-installation-backup-qa.json` (privadas, sem credenciais/cookies/códigos nas capturas).
**Versões históricas preservadas abaixo:** frases sobreC71aberto,22 ficheiros apenas preparados ou rotas apenas deagronegócio descrevem os incrementos anteriores e são substituídas, para o estado atual, pelas provas desta secção.
<!-- qa-c71-sectors-result-20261010:end -->
<!-- legacy-manual-sectors-20261010:begin -->
## Confirmação manual e reconciliação setorial — 10/10/2026
Responsável confirmou leitura pública → autenticação → criar/escolher dossier → guardar referência → nova sessão → recuperar dossier e abrir artigo. AUD-LEG-03/04/06/07 receberam apenas os subcritérios correspondentes; estados e contagens da matriz inalterados. Conta/perfil/dispositivo não indicados. Móvel, acessibilidade, outros perfis, cobertura integral, C37/QA18 e A/B continuam pendentes.
Quatro setores reconciliados: Energia & Mineração (índice + dois artigos), Turismo (artigo +11 referências territoriais), Infraestruturas (título corrigido em código) e Mercado Financeiro (corpo vazio preservado). **17 artigos +5 imagens** preparados; prévia CLI em transação READ ONLY:22 candidatos,0 duplicados,0 conflitos; nenhuma aplicação/scan/publicação nova. C72 corrigido na transformação dos originais, sem prova visual dos novos setores. C71 continua P0: recuperar integridade/backup/restauro antes de aplicação QA. Biblioteca/entidades/componentes existentes reutilizados; rotas dos restantes setores ainda por parametrizar. Ver `legado-piloto-qa.md` e inventários existentes.
<!-- legacy-manual-sectors-20261010:end -->
<!-- qa-legacy-browser-20261010:begin -->
## Verificação do piloto na interface — 10/10/2026, exclusivamente QA
**O piloto existente foi demonstrado em Chromium instalado**, com contextos separados, sem usar o perfil pessoal do utilizador. O controlo nativo continua indisponível (`native pipe`, os error 2, duas sondagens); esse problema não impediu renderização, cliques e capturas por Playwright. C37/QA18 permanece pendente; este ensaio usou Promotor QA21 e outra conta sintética QA23.
Página pública→pesquisa com/sem resultado→artigo→carteira/mapas/biblioteca/oportunidade publicada→CTA→login/MFA da conta QA21→dossier guardado→fonte/retorno→logout→novo contexto/MFA→recuperação e F5→continuação/repetição passaram. Leitura anónima e QA23 não recebem o título/ID do contexto privado; dossier negado 403 à QA23. A criação na interface consta da tentativa inicial preservada; as repetições recuperam o mesmo dossier sem criar outro. Cinco fases / 19 etapas observadas. Persistência em transação READ ONLY: uma referência, um evento, um documento por hash; originais e scan preservados; projeto independente com zero referências.
Desktop 1440 px / móvel 390 px: fotografia 900×506, Noto Sans / Noto Serif carregadas, cores reais adaptadas, texto integral de 4.702 carateres, foco de 3 px por teclado e nenhum deslocamento horizontal. Sete capturas privadas. **C70 corrigido:** título de separador “Laravel” substituído por “InvesteMoz QA” apenas no build QA; bundle ativo preservado. Isto não certifica acessibilidade integral nem demonstra todas as geometrias/consumidores do portal.
**AUD-LEG-04 concluído e demonstrado em QA**, por consulta pública/proteção privada. AUD-LEG-01/02/03/05/06/07 continuam parciais pelas lacunas explicitadas na matriz: cobertura histórica, controlo assistivo sistemático, relações territoriais concretas, restante mapa de URLs e anexos. Nenhum I0–I3/A-B/workflow/C37 é encerrado por esta prova.
Recibos: `legacy-content-browser-qa.json`, `legacy-content-browser-persistence-qa.json`; testes `tools/legacy-content-browser-qa.cjs` e `tools/legacy-content-browser-persistence-qa.php`. Capturas e credenciais permanecem privadas; não foram registados Cookie, X-XSRF-TOKEN ou corpo com o código MFA. O ZIP anteriormente entregue é um snapshot anterior à verificação, não prova de execução remota.
**C71 / P0 — novo backup QA bloqueado:** `pg_dump` falhou às 14:48–14:49UTC: `global/2964` ausente, catálogo `pg_db_role_setting`; diagnóstico identifica 26 ficheiros de catálogos partilhados ausentes no cluster 55441, situado em Temp. Não atribuir a remoção à limpeza automática sem evidência. O backup/restauro válido de 13:35:39UTC (295 tabelas/81 ficheiros) permanece histórico. Preservação posterior READ ONLY: 295 tabelas/34.902 linhas/31 sequências; não equivale a backup/restauro verificado. Readiness QA foi invalidada e o CLI passa a devolver exit 1 controlado. Próxima ação: recuperação em ambiente isolado, preservação/conferência integral e nova prova de restauro antes de retomar escritas. Nenhum catálogo recriado às cegas nem acesso/escrita na base ativa. Ver `qa-readable-preservation.json`, `profile-installation-backup-failure-qa.json` e C71 em `casos-reproducao.md`.
**Contagens da mesma matriz:** 3393 requisitos/3393 linhas; 6845 ocorrências de origem. Estados: 3234 parcial; 145 não implementado; 5 dependente de decisão concreta; 1 implementado mas não ativado; 1 com falha; 7 concluído e demonstrado.
<!-- qa-legacy-browser-20261010:end -->
<!-- qa-legacy-20261010:begin -->
## Histórico — legado agronegócio antes do ensaio Chromium
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:imagem histórica 404. Cobertura documental histórica permanece aberta.
Mesma matriz: **3393 requisitos únicos = 3393 linhas lógicas; 6845 ocorrências de origem**. Estados: 3236 parcial; 145 não implementado; 5 dependente de decisão concreta; 1 implementado mas não ativado; 6 concluído e demonstrado. 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. **Estado histórico, antes do ensaio Chromium: navegador integrado indisponível; aceitação visual então pendente**, com roteiro executável. C37/QA18, Promotor21 visual e A/B continuam abertos. Perfis modulares/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ção formal pendentes.
<!-- qa-legacy-20261010:end -->
<!-- qa-response-20261010:begin -->
## Incremento atual — Response/§27 e propostas para decisão
Somente QA **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Mesmo inventário/matriz; nenhum fecho integral ou escrita ativa.
- **AUD-OP-04:** `/minhas-oportunidades/{id}/response` integra a leitura do editor existente. Preserva IDs, códigos, referências UUID e estados atuais. Dez casos #30–39 recuperados noutra sessão MFA; Validador permitido, organização alheia 403 e anónimo 401. A versão do adaptador declara expressamente que a resposta normativa completa continua parcial.
- **C65 / QA40:** §27 exige unicidade de número cadastral por oportunidade. Rascunho incompleto/duplicado permanece recuperável; submissão devolve 422 junto a `canonical.land_rights.parcels.1.cadastral_number`. Após correção, submissão aceite. Números ausentes não são inventados.
- **C66 / QA41:** `succession` constava no contrato original mas era recusada pela lista semântica de venda. Lista reconciliada; criação/edição, submissão e nova sessão demonstradas. Não alterar silenciosamente outras modalidades.
- **Ainda parcial:** algoritmo de completude, metadados/identidade de itens inline, equivalência explícita dos estados e recuperação dos autores de alteração/exclusão dos recursos. §27 não cria tabelas paralelas. Condições sem regra inequívoca continuam como decisões D-S1/S2.
Provas: `opportunity-response-http-qa.json`, `OpportunityCanonicalWorkflowTest`, `profile-installation-checks-qa.json` e schema original preservado. O recibo identifica separadamente a tentativa anterior em que passaram os dez critérios de leitura e o ensaio semântico posterior; falhas do harness foram conservadas, não apresentadas como falhas do portal.
**Propostas modulares e SLA continuam não aprovados.** O ficheiro `propostas-perfis-modulares-sla.md` explicita compromissos internos de atendimento, janela operacional do Promotor, pausas por tarefa, espera externa separada e ausência de decisão automática. Nenhum `due_at`, titular ou perfil modular ativado.
**Ativação:** o restauro reportado é QA. Para uma futura ativação, conferir a identidade, schema, dados e configuração efetivos da base ativa para a revisão candidata, capturar hashes/manifesto dessa revisão e provar o seu backup/restauro. As provas QA ou `ready=true` não substituem essa ligação. Sequência 030000 → 040000 → 190000; C37, QA21 e A/B visual pendentes, candidata não fixada.
**Próximo trabalho independente:** completar metadados/Response sem novos cadastros; PDFs gerados/exportações/derivadas por consumidor; países/origem com filtros e estatísticas por formulário; correspondências inequívocas, mantendo Mandie/Guro ambíguo. Cartografia permanece 408 resolvidas e 173 lacunas neste incremento.
<!-- qa-response-20261010:end -->
<!-- qa-installation-cartography-20261010:begin -->
## Estado atual — instalador, documentos e cartografia
QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Ativo inspecionado apenas por leitura em 2026-10-10T10:32:41+00:00: 030000, 040000 e 09/190000 ausentes; zero geometrias; manifesto preservado. **C37, Promotor QA21 e A/B permanecem visualmente pendentes. Candidata à ativação ainda não fixada.**
QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Ativo inspecionado apenas por leitura em 2026-10-10T12:26:56+00:00: 030000, 040000 e 09/190000 ausentes; zero geometrias; manifesto preservado. **C37, Promotor QA21 e A/B permanecem visualmente pendentes. Candidata à ativação ainda não fixada.**
| Requisitos | Código preparado / prova QA | Critério ainda aberto |
| --- | --- | --- |
@@ -14,7 +177,7 @@ QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_tes
| OP-03 / OP-04 | Código da interface e resposta distinguem aprovação interna, disponibilização restrita e catálogo público. Casos #30 restrito, #33 público e #32 aprovado sem publicação recuperados pela API; consulta da política pública efetiva. | Interface; Response integral/§27 e condições sem regra inequívoca continuam parciais. Testes dos seis contratos não encerram a demonstração visual. |
| PROFILE-22/28 | [Quatro perfis e SLA com regra, fundamento, efeito e decisões concretas](propostas-perfis-modulares-sla.md). | Propostas **inativas**; quatro categorias continuam com rascunhos recuperáveis, mas submissão/publicação bloqueadas. Nenhum prazo ou responsável real configurado. |
Prova de recuperação QA: dump consistente → nova base `profile_restore_20261010_104956_test:55441` → **295 tabelas com linhas/hashes iguais e 69 ficheiros fisicamente recuperados por hash**; todos os ficheiros referenciados recuperados por hash. Arquivo privado inclui o runtime e a chave QA estável. Readiness só atesta preparação técnica desta revisão. Repetir após fixar a candidata e preparar backup ativo apenas numa futura ativação autorizada.
Prova de recuperação QA: dump consistente → nova base `profile_restore_20261010_123125_test:55441` → **295 tabelas com linhas/hashes iguais e 71 ficheiros fisicamente recuperados por hash**; todos os ficheiros referenciados recuperados por hash. Arquivo privado inclui o runtime e a chave QA estável. Readiness só atesta preparação técnica desta revisão. Repetir após fixar a candidata e preparar backup ativo apenas numa futura ativação autorizada.
**Matriz única:** 3.386 requisitos/linhas e 6.838 ocorrências de origem; todas as fontes, critérios e versões preservados; sem novos fechos globais. Contagens por estado abaixo e na projeção dos pedidos abertos.
@@ -1,9 +1,28 @@
# InvestMoz — contratos e workflow de oportunidades em QA
<!-- qa-response-20261010:begin -->
## Incremento atual — Response/§27 e propostas para decisão
Somente QA **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Mesmo inventário/matriz; nenhum fecho integral ou escrita ativa.
- **AUD-OP-04:** `/minhas-oportunidades/{id}/response` integra a leitura do editor existente. Preserva IDs, códigos, referências UUID e estados atuais. Dez casos #30–39 recuperados noutra sessão MFA; Validador permitido, organização alheia 403 e anónimo 401. A versão do adaptador declara expressamente que a resposta normativa completa continua parcial.
- **C65 / QA40:** §27 exige unicidade de número cadastral por oportunidade. Rascunho incompleto/duplicado permanece recuperável; submissão devolve 422 junto a `canonical.land_rights.parcels.1.cadastral_number`. Após correção, submissão aceite. Números ausentes não são inventados.
- **C66 / QA41:** `succession` constava no contrato original mas era recusada pela lista semântica de venda. Lista reconciliada; criação/edição, submissão e nova sessão demonstradas. Não alterar silenciosamente outras modalidades.
- **Ainda parcial:** algoritmo de completude, metadados/identidade de itens inline, equivalência explícita dos estados e recuperação dos autores de alteração/exclusão dos recursos. §27 não cria tabelas paralelas. Condições sem regra inequívoca continuam como decisões D-S1/S2.
Provas: `opportunity-response-http-qa.json`, `OpportunityCanonicalWorkflowTest`, `profile-installation-checks-qa.json` e schema original preservado. O recibo identifica separadamente a tentativa anterior em que passaram os dez critérios de leitura e o ensaio semântico posterior; falhas do harness foram conservadas, não apresentadas como falhas do portal.
**Propostas modulares e SLA continuam não aprovados.** O ficheiro `propostas-perfis-modulares-sla.md` explicita compromissos internos de atendimento, janela operacional do Promotor, pausas por tarefa, espera externa separada e ausência de decisão automática. Nenhum `due_at`, titular ou perfil modular ativado.
**Ativação:** o restauro reportado é QA. Para uma futura ativação, conferir a identidade, schema, dados e configuração efetivos da base ativa para a revisão candidata, capturar hashes/manifesto dessa revisão e provar o seu backup/restauro. As provas QA ou `ready=true` não substituem essa ligação. Sequência 030000 → 040000 → 190000; C37, QA21 e A/B visual pendentes, candidata não fixada.
**Próximo trabalho independente:** completar metadados/Response sem novos cadastros; PDFs gerados/exportações/derivadas por consumidor; países/origem com filtros e estatísticas por formulário; correspondências inequívocas, mantendo Mandie/Guro ambíguo. Cartografia permanece 408 resolvidas e 173 lacunas neste incremento.
<!-- qa-response-20261010:end -->
<!-- qa-installation-cartography-20261010:begin -->
## Estado atual — instalador, documentos e cartografia
QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Ativo inspecionado apenas por leitura em 2026-10-10T10:32:41+00:00: 030000, 040000 e 09/190000 ausentes; zero geometrias; manifesto preservado. **C37, Promotor QA21 e A/B permanecem visualmente pendentes. Candidata à ativação ainda não fixada.**
QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Ativo inspecionado apenas por leitura em 2026-10-10T12:26:56+00:00: 030000, 040000 e 09/190000 ausentes; zero geometrias; manifesto preservado. **C37, Promotor QA21 e A/B permanecem visualmente pendentes. Candidata à ativação ainda não fixada.**
| Requisitos | Código preparado / prova QA | Critério ainda aberto |
| --- | --- | --- |
@@ -14,7 +33,7 @@ QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_tes
| OP-03 / OP-04 | Código da interface e resposta distinguem aprovação interna, disponibilização restrita e catálogo público. Casos #30 restrito, #33 público e #32 aprovado sem publicação recuperados pela API; consulta da política pública efetiva. | Interface; Response integral/§27 e condições sem regra inequívoca continuam parciais. Testes dos seis contratos não encerram a demonstração visual. |
| PROFILE-22/28 | [Quatro perfis e SLA com regra, fundamento, efeito e decisões concretas](propostas-perfis-modulares-sla.md). | Propostas **inativas**; quatro categorias continuam com rascunhos recuperáveis, mas submissão/publicação bloqueadas. Nenhum prazo ou responsável real configurado. |
Prova de recuperação QA: dump consistente → nova base `profile_restore_20261010_104956_test:55441` → **295 tabelas com linhas/hashes iguais e 69 ficheiros fisicamente recuperados por hash**; todos os ficheiros referenciados recuperados por hash. Arquivo privado inclui o runtime e a chave QA estável. Readiness só atesta preparação técnica desta revisão. Repetir após fixar a candidata e preparar backup ativo apenas numa futura ativação autorizada.
Prova de recuperação QA: dump consistente → nova base `profile_restore_20261010_123125_test:55441` → **295 tabelas com linhas/hashes iguais e 71 ficheiros fisicamente recuperados por hash**; todos os ficheiros referenciados recuperados por hash. Arquivo privado inclui o runtime e a chave QA estável. Readiness só atesta preparação técnica desta revisão. Repetir após fixar a candidata e preparar backup ativo apenas numa futura ativação autorizada.
**Matriz única:** 3.386 requisitos/linhas e 6.838 ocorrências de origem; todas as fontes, critérios e versões preservados; sem novos fechos globais. Contagens por estado abaixo e na projeção dos pedidos abertos.
@@ -1,9 +1,55 @@
<!-- qa-visual-results-20261010:begin -->
## Ensaios visuais e pendências atuais — 2026-10-10T17:57:54.604301+00:00
Trabalho exclusivo na QA da porta 18097, base `desk_restore_20261009_174726_test:55441`. Nenhuma alteração na base ativa. A mesma matriz mantém **3393 requisitos / 3393 linhas / 6845 ocorrências de origem**: 3235 parciais, 145 não implementados, 5 dependentes de decisão, 1 implementado mas não ativado e 7 concluídos e demonstrados. Os estados globais não mudaram; foram atualizados subcritérios com provas próprias. A ausência de requisitos globalmente classificados “com falha” não significa ausência de casos abertos.
| Requisito | Demonstrado em QA | Critérios ainda pendentes |
| --- | --- | --- |
| C37; UI01; PROFILE24–25 | MFA manual QA18 aceite às 17:40:21 e 17:46:42 UTC; autenticação persistida e dashboard 200. Responsável confirmou acesso. O 403 inicial era do relatório administrativo reservado. Mensagem e retorno ao painel corrigidos; Chromium QA21 provou recusa 403, retorno e F5 sem perder sessão. | F5, logout e novo login no navegador habitual QA18. Não declarar o loop anterior resolvido por inferência: o rastreio não demonstra um ciclo persistente. Uma atualização Inertia 409 isolada não é perda de autenticação. |
| A/B; GUIDE01; SIM02; DESK01–02; CRM01 | Chromium desktop QA18: criação → gravação → nova sessão → recuperação → aplicabilidade → ferramenta → resultado no dossier → preparação no Balcão → acompanhamento interno → nova recuperação. Cinco fases e 19 etapas preservadas. A dispensa S10; B aguarda informação externa em S10 enquanto S02 avança. Resultado: equilíbrio de 20 unidades e défice A=0 / B=70.000 MZN. QA23 recebe 403/404; gestor não vê resultados financeiros privados. | Balcão demonstrado em preparação DRAFT, sem submissão ou decisão externa. Restantes critérios especializados, fiscalidade, documentos e consumidores não encerrados. Repetição final A/B móvel ainda necessária. |
| DASH01; PROFILE05/32 | QA21, a partir do dashboard: “Minhas oportunidades” e “Nova oportunidade” → rascunho incompleto #50 → guardar → nova sessão → recuperar → editar. Promotor solicita arquivo; Publicador QA25 arquiva com fundamento; estado recuperado. | Restauração por perfil ausente no workflow. Definir e implementar ator/regra coerentes sem conceder arquivo direto ao Promotor por inferência. Workflow e contratos especializados ainda exigem demonstrações próprias. |
| COUNTRY01–04/06/07 | Residência PT guardada no perfil e recuperada após F5. Negócio e fundiário DRAFT: país PT / origem FR guardados e recuperados. Balcão A/B: PT / ZA recuperados noutra sessão. Quatro formulários públicos apresentam 249 países cada. Gestão: filtros ZA/unknown, quatro manifestações sem origem e zero pedidos submetidos ZA, com rascunhos excluídos. | Nos formulários públicos, a presença de opções não prova submissão ou recuperação. Concluir cada gravação e estatística positiva de origem conhecida no UI. Nos rascunhos de negócio/fundiário, F5 não substitui nova sessão. |
| GEO01–03 | Oportunidade própria QA21 #51 e criação de projeto, cluster e corredor: Cabo Delgado registado → API com uma geometria acessível → um polígono SVG visível em cada consumidor. | Seleções desse ensaio não guardadas. Recuperar relações por consumidor e testar descendentes, restantes mapas e perfis. Tiles externos bloqueados no ensaio, sem confirmação da base externa. 408 associações anteriores e 173 pendentes preservadas; Mandie/Guro ambíguo. |
| LEG02/03/06/07; C77–C83 | Corrigidos landmarks, ARIA, hierarquia de títulos, reflow, foco das tabelas e idioma HTML pt-MZ em QA. Quinze execuções dirigidas de axe, em 320/390/1440px, sem violações automáticas ou overflow. Três páginas com skiplink primeiro e tabelas financeiras operadas por teclado. | Cobertura integral, leitor de ecrã, dispositivos reais, mais perfis e editor C74. O dossier QA21 verifica o componente móvel corrigido, sem substituir a repetição final A/B. |
| DOC01; C84 | Retirada mensagem global falsa de ClamD pendente; scan e motor apresentados por ficheiro/versão. Os 22 scans reais anteriores permanecem demonstrados. | PDFs, exportações, partilhas, derivadas e operações restantes exigem provas próprias. Um original limpo não comprova a derivada. |
| OPS01 | Backup QA 2026-10-10T17:48:11+00:00: 295 tabelas e 103 ficheiros restaurados. Ativo READ ONLY, backup 2026-10-10T17:48:52+00:00: 288 tabelas restauradas numa base QA distinta e cinco ficheiros conferidos no arquivo. Instalador/publicação: 9 testes / 93 asserções, além de TypeScript. | Aplicação, compatibilidade e reversão da futura revisão candidata no clone fresco do ativo. Revisão candidata global e ativação continuam por fixar. Contagens de testes não substituem critérios funcionais. |
**C80 corrigido:** a resposta tardia do GET do rascunho podia substituir preenchimento já iniciado. Edição e avanço ficam bloqueados até a recuperação terminar; o retorno exige o snapshot completo guardado. Chromium com resposta deliberadamente atrasada comprovou bloqueio, recuperação e libertação. A/B recuperaram os inputs corretos noutra sessão, preservando versões anteriores.
**Recusa manual do mapa às 17:52:33 UTC:** o pedido à oportunidade 51 foi feito com QA18, não QA21; recebeu 403 com sessão preservada. É uma prova de autorização, não de apresentação do mapa. Usar QA21 (`profiles-v1-promoter@example.invalid`) para esse consumidor; o Chromium anterior QA21 recebeu 200. Nenhuma permissão alargada para contornar a recusa.
**Provas privadas sanitizadas:** `investor-ab-chromium-20261010.json`, `promoter-dashboard-chromium-20261010.json`, `consumers-mobile-accessibility-chromium-20261010.json`, `visual-followup-chromium-20261010.json`, `qa-report-access-chromium-20261010.json`, `c37-qa18-post-mfa-destination-20261010.json`, `visual-persistence-qa.json`, `profile-installation-backup-qa.json` e `virtual-desk/backup-proof.json`. A tentativa antiga de QA21 na oportunidade 30 recebeu 403 e não contou como mapa; foi substituída pelo consumidor próprio 51. Tentativas e falhas de harness preservadas; sem códigos, segredos ou cookies nas evidências entregáveis.
Mercado Financeiro permanece privado, sem corpo inventado; rotas públicas 404. Energia e Mineração mantêm artigos distintos e Turismo não criou associações geográficas automáticas. Fontes oficiais aceites não foram reabertas. Próximo lote: C37 manual, restauração por perfil, submissão/estatística por formulário, gravação/recuperação territorial, documentos, Response/§27 e workflow. Perfis modulares/SLA continuam decisões separadas; não iniciar novas jornadas educativas.
As versões históricas abaixo ficam preservadas. Afirmações antigas sobre falta de navegador, A/B não testados ou ClamD global pendente são superadas apenas pelos subcritérios acima. Código preparado, QA demonstrada e disponibilidade ativa são estados distintos.
<!-- qa-visual-results-20261010:end -->
# Gestão de Oportunidades integrada no Dashboard
<!-- qa-response-20261010:begin -->
## Incremento atual — Response/§27 e propostas para decisão
Somente QA **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Mesmo inventário/matriz; nenhum fecho integral ou escrita ativa.
- **AUD-OP-04:** `/minhas-oportunidades/{id}/response` integra a leitura do editor existente. Preserva IDs, códigos, referências UUID e estados atuais. Dez casos #30–39 recuperados noutra sessão MFA; Validador permitido, organização alheia 403 e anónimo 401. A versão do adaptador declara expressamente que a resposta normativa completa continua parcial.
- **C65 / QA40:** §27 exige unicidade de número cadastral por oportunidade. Rascunho incompleto/duplicado permanece recuperável; submissão devolve 422 junto a `canonical.land_rights.parcels.1.cadastral_number`. Após correção, submissão aceite. Números ausentes não são inventados.
- **C66 / QA41:** `succession` constava no contrato original mas era recusada pela lista semântica de venda. Lista reconciliada; criação/edição, submissão e nova sessão demonstradas. Não alterar silenciosamente outras modalidades.
- **Ainda parcial:** algoritmo de completude, metadados/identidade de itens inline, equivalência explícita dos estados e recuperação dos autores de alteração/exclusão dos recursos. §27 não cria tabelas paralelas. Condições sem regra inequívoca continuam como decisões D-S1/S2.
Provas: `opportunity-response-http-qa.json`, `OpportunityCanonicalWorkflowTest`, `profile-installation-checks-qa.json` e schema original preservado. O recibo identifica separadamente a tentativa anterior em que passaram os dez critérios de leitura e o ensaio semântico posterior; falhas do harness foram conservadas, não apresentadas como falhas do portal.
**Propostas modulares e SLA continuam não aprovados.** O ficheiro `propostas-perfis-modulares-sla.md` explicita compromissos internos de atendimento, janela operacional do Promotor, pausas por tarefa, espera externa separada e ausência de decisão automática. Nenhum `due_at`, titular ou perfil modular ativado.
**Ativação:** o restauro reportado é QA. Para uma futura ativação, conferir a identidade, schema, dados e configuração efetivos da base ativa para a revisão candidata, capturar hashes/manifesto dessa revisão e provar o seu backup/restauro. As provas QA ou `ready=true` não substituem essa ligação. Sequência 030000 → 040000 → 190000; C37, QA21 e A/B visual pendentes, candidata não fixada.
**Próximo trabalho independente:** completar metadados/Response sem novos cadastros; PDFs gerados/exportações/derivadas por consumidor; países/origem com filtros e estatísticas por formulário; correspondências inequívocas, mantendo Mandie/Guro ambíguo. Cartografia permanece 408 resolvidas e 173 lacunas neste incremento.
<!-- qa-response-20261010:end -->
<!-- qa-installation-cartography-20261010:begin -->
## Estado atual — instalador, documentos e cartografia
QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Ativo inspecionado apenas por leitura em 2026-10-10T10:32:41+00:00: 030000, 040000 e 09/190000 ausentes; zero geometrias; manifesto preservado. **C37, Promotor QA21 e A/B permanecem visualmente pendentes. Candidata à ativação ainda não fixada.**
QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_test:55441`. Ativo inspecionado apenas por leitura em 2026-10-10T12:26:56+00:00: 030000, 040000 e 09/190000 ausentes; zero geometrias; manifesto preservado. **C37, Promotor QA21 e A/B permanecem visualmente pendentes. Candidata à ativação ainda não fixada.**
| Requisitos | Código preparado / prova QA | Critério ainda aberto |
| --- | --- | --- |
@@ -14,7 +60,7 @@ QA exclusivamente: **http://127.0.0.1:18097**, `desk_restore_20261009_174726_tes
| OP-03 / OP-04 | Código da interface e resposta distinguem aprovação interna, disponibilização restrita e catálogo público. Casos #30 restrito, #33 público e #32 aprovado sem publicação recuperados pela API; consulta da política pública efetiva. | Interface; Response integral/§27 e condições sem regra inequívoca continuam parciais. Testes dos seis contratos não encerram a demonstração visual. |
| PROFILE-22/28 | [Quatro perfis e SLA com regra, fundamento, efeito e decisões concretas](auditoria-portal/propostas-perfis-modulares-sla.md). | Propostas **inativas**; quatro categorias continuam com rascunhos recuperáveis, mas submissão/publicação bloqueadas. Nenhum prazo ou responsável real configurado. |
Prova de recuperação QA: dump consistente → nova base `profile_restore_20261010_104956_test:55441` → **295 tabelas com linhas/hashes iguais e 69 ficheiros fisicamente recuperados por hash**; todos os ficheiros referenciados recuperados por hash. Arquivo privado inclui o runtime e a chave QA estável. Readiness só atesta preparação técnica desta revisão. Repetir após fixar a candidata e preparar backup ativo apenas numa futura ativação autorizada.
Prova de recuperação QA: dump consistente → nova base `profile_restore_20261010_123125_test:55441` → **295 tabelas com linhas/hashes iguais e 71 ficheiros fisicamente recuperados por hash**; todos os ficheiros referenciados recuperados por hash. Arquivo privado inclui o runtime e a chave QA estável. Readiness só atesta preparação técnica desta revisão. Repetir após fixar a candidata e preparar backup ativo apenas numa futura ativação autorizada.
**Matriz única:** 3.386 requisitos/linhas e 6.838 ocorrências de origem; todas as fontes, critérios e versões preservados; sem novos fechos globais. Contagens por estado abaixo e na projeção dos pedidos abertos.
@@ -195,6 +195,7 @@ export default function InteractiveInvestmentMap({
<div
ref={element}
className="relative z-0 h-[460px] w-full rounded"
role="region"
aria-label="Mapa; navegação por setas e teclas mais e menos"
/>
{points === null ? (
@@ -255,7 +256,9 @@ export default function InteractiveInvestmentMap({
<a className="underline" href={area.url}>
{area.title}
</a>{' '}
· Polígono cadastrado, limites oficiais por verificar ·
· {area.purpose === 'administrative_boundary'
? 'Limite administrativo da edição indicada; não representa um local de investimento'
: 'Polígono cadastrado; comparação com limite administrativo pendente'} ·
Fonte: {area.source_reference ?? 'Não indicada'} · Data:{' '}
{area.source_date ?? 'Não documentada'}
</p>
@@ -27,6 +27,8 @@ function resourceTerritory(resource?: Resource) {
}
export type CanonicalOptions = {
version: string;
modularProfiles?: Record<string, string>;
historyVersion?: string | null;
schema: Schema;
definitions: Record<string, Schema>;
contacts: { id: string; full_name: string }[];
@@ -70,6 +72,53 @@ const labels: Record<string, string> = {
real_estate: "Imobiliário",
land_rights: "DUAT e direitos fundiários",
business_transaction: "Negócio",
module_profile_version: "Versão do contrato QA (seleção explícita)",
module_details: "Condições do módulo",
mode: "Percurso: procura, oferta ou modalidade",
conditions_status: "Condições propostas ou compromisso documentado",
object: "Objeto",
representation_document_id: "Documento de representação",
object_document_id: "Documento do objeto/procedimento",
disclosure_authorization_document_id: "Autorização de divulgação da intenção/chamada",
external_decision_document_id: "Ato competente do direito atribuído",
granting_entity: "Entidade concedente declarada",
procedure_regime: "Regime/procedimento declarado",
scope: "Âmbito territorial ou digital",
term_value: "Duração ou prazo",
term_unit: "Unidade da duração",
remuneration_obligations: "Remuneração e obrigações",
participation_conditions: "Condições de participação",
land_required: "Precisa de terra?",
construction_required: "Inclui construção?",
applicable_authorizations: "Autorizações aplicáveis e situação",
instrument: "Instrumento de financiamento",
offered_capacity: "Capacidade oferecida pelo financiador",
eligibility_criteria: "Critérios declarados do financiador",
capacity_document_id: "Evidência da capacidade declarada",
financial_plan_document_id: "Plano financeiro do projeto",
secured_evidence_document_id: "Evidência do financiamento assegurado",
use_of_funds: "Finalidade do financiamento procurado",
disbursement_calendar: "Calendário proposto de desembolso",
repayment_periodicity: "Periodicidade de reembolso",
proposed_rights: "Direitos propostos",
equity_percentage: "Percentagem de capital proposta",
terms: "Condições",
scenario: "Cenário identificado",
source: "Fonte identificada",
terms_date: "Data da fonte/condições",
commitment_document_id: "Documento do compromisso assumido",
acceptance_criteria: "Critérios de aceitação do fornecimento",
quotation_method: "Método de cotação",
total_price: "Preço total (impostos identificados separadamente)",
declared_taxes: "Impostos declarados, sem cálculo implícito",
advance_or_guarantee: "Há adiantamento ou garantia?",
subtype: "Subtipo para classificação funcional",
recipients: "Destinatários",
expected_output: "Resultado esperado",
history_status: "Situação do histórico financeiro",
history_declaration: "Declaração de ausência de exercícios completos",
history_declaration_document_id: "Documento da declaração",
projections: "Projeções identificadas — não são contas realizadas",
transaction_type: "Modalidade",
property_type: "Tipo de imóvel",
construction_status: "Estado construtivo",
@@ -173,12 +222,10 @@ const labels: Record<string, string> = {
direct_jobs: "Empregos diretos",
employment_type: "Tipo de emprego",
geometry: "Geometria da parcela",
module_profile_version: "Perfil modular aprovado",
start_date: "Data de início",
end_date: "Data de fim",
responsible_name: "Responsável declarado",
dependencies: "Marcos dos quais depende",
scenario: "Cenário analisado",
npv: "Valor atual líquido",
irr: "Taxa interna de retorno (%)",
payback_years: "Prazo de retorno (anos)",
@@ -568,7 +615,7 @@ export default function OpportunityCanonicalFields({
: ["business_sale", "business_partnership", "franchise"].includes(
category,
)
? ["business_transaction"]
? ["module_profile_version", "business_transaction"]
: category === "investment_project"
? [
"opportunity_type",
@@ -594,7 +641,18 @@ export default function OpportunityCanonicalFields({
"risks",
"relations",
]
: ["module_profile_version"];
: ["module_profile_version", "module_details", ...(category === "financing" ? ["total_investment", "investment_breakdown", "secured_financing", "funding_sought"] : [])];
const details = (value.module_details ?? {}) as Data;
const mode = String(details.mode ?? "");
const modularFields = ["mode", "object", "representation_document_id", "object_document_id",
...(category === "concession" ? ["granting_entity", "procedure_regime", "scope", "term_value", "term_unit", "remuneration_obligations", "participation_conditions", "land_required", "construction_required", "applicable_authorizations", "start_date", "end_date", "disclosure_authorization_document_id", "external_decision_document_id", "territorial_right_document_id", "construction_license_document_id"] : []),
...(["financing", "supply"].includes(category) ? ["conditions_status", "terms", "scenario", "source", "terms_date", "commitment_document_id"] : []),
...(category === "financing" ? ["instrument", "disbursement_calendar", "term_value", "term_unit", "repayment_periodicity", "equity_percentage", "proposed_rights",
...(mode === "funding_offer" ? ["offered_capacity", "eligibility_criteria", "capacity_document_id"] : ["use_of_funds", "financial_plan_document_id", "secured_evidence_document_id"])] : []),
...(category === "supply" ? ["quantity", "unit", "destination", "start_date", "end_date", "acceptance_criteria", "quotation_method", "unit_price", "total_price", "declared_taxes", "advance_or_guarantee", ...(mode === "supply_offer" ? ["capacity_document_id"] : [])] : []),
...(category === "other" ? ["subtype", "purpose", "recipients", "expected_output", "risks"] : []),
];
const modes = category === "concession" ? ["intention_call", "awarded_right"] : category === "financing" ? ["seeking_funding", "funding_offer"] : category === "supply" ? ["purchase_request", "supply_offer"] : ["classified_other"];
return (
<fieldset disabled={disabled} className="space-y-4 rounded border p-4">
<legend>Contrato da oportunidade — {category}</legend>
@@ -603,7 +661,23 @@ export default function OpportunityCanonicalFields({
aplicáveis e os recursos guardados. Validação interna não
equivale a autorização externa.
</p>
{options.modularProfiles?.[category] && (
<p className="rounded border border-amber-400 p-3">
Primeira versão QA: {options.modularProfiles[category]}. Selecione-a explicitamente no campo de versão.
Financiamento distingue procura do projeto de oferta do financiador; fornecimento distingue pedido de compra de oferta.
Condições propostas não representam compromisso nem decisão externa.
</p>
)}
{Array.from(new Set([...common, ...specialized])).map((key) => {
if (key === "module_profile_version") {
const current = String(value[key] ?? "");
const version = options.modularProfiles?.[category] ?? (["business_sale", "business_partnership", "franchise"].includes(category) ? options.historyVersion : null);
return <label key={key} className="block">{labels[key]}<select className={control} value={current} onChange={(e) => set(key, e.target.value || null)}>
<option value="">Contrato original / por selecionar</option>
{current && current !== version && <option value={current}>{current} — versão existente preservada</option>}
{version && <option value={version}>{version}</option>}
</select><FieldErrors path={"canonical." + key} errors={errors} /></label>;
}
if (
[
"sector_id",
@@ -731,7 +805,11 @@ export default function OpportunityCanonicalFields({
</fieldset>
);
}
const schema = options.schema.properties?.[key];
let schema = options.schema.properties?.[key];
if (key === "module_details" && schema?.properties) {
schema = { ...schema, properties: Object.fromEntries(Object.entries(schema.properties).filter(([field]) => modularFields.includes(field))) };
schema.properties!.mode = { enum: modes };
}
return schema ? (
<Editor
key={key}
@@ -1,5 +1,7 @@
import type { ProfileAccess } from '@/types/profile-access';
import { Link, usePage } from '@inertiajs/react';
import { Link, router, usePage } from '@inertiajs/react';
import { LogOut } from 'lucide-react';
import { logout } from '@/routes';
import { useEffect, useId, useRef, useState } from 'react';
import type { OpportunityManagementAccess } from '@/types/opportunity-management';
import {
@@ -11,10 +13,14 @@ import {
export function PortalGroupedMenu() {
const { props, url } = usePage<{
auth: { user: unknown; canManagePortal: boolean };
auth: {
user: { name?: string; email?: string } | null;
canManagePortal: boolean;
};
existingBusinessEnabled?: boolean;
educationEnabled?: boolean;
aiEnabled?: boolean;
legacyContentEnabled?: boolean;
portalOperator?: string | null;
profileAccess?: ProfileAccess | null;
opportunityManagement?: OpportunityManagementAccess | null;
@@ -25,6 +31,7 @@ export function PortalGroupedMenu() {
businesses: props.existingBusinessEnabled,
education: props.educationEnabled,
ai: props.aiEnabled,
legacyContent: props.legacyContentEnabled,
opportunityManagement: props.opportunityManagement,
profileAccess: props.profileAccess,
});
@@ -33,6 +40,7 @@ export function PortalGroupedMenu() {
const [open, setOpen] = useState<string | null>(null);
const [branch, setBranch] = useState<string | null>(null);
const [offset, setOffset] = useState(0);
const [loggingOut, setLoggingOut] = useState(false);
const root = useRef<HTMLElement>(null);
const id = useId();
useEffect(() => {
@@ -150,6 +158,33 @@ export function PortalGroupedMenu() {
)}
</p>
</div>
{props.auth?.user && (
<nav
aria-label="Sessão da conta"
className="mb-3 flex flex-wrap items-center justify-between gap-3 rounded-lg bg-slate-50 px-3 py-2"
>
<p className="min-w-0 break-words text-sm text-slate-700 [overflow-wrap:anywhere]">
Sessão: {props.auth.user.name || props.auth.user.email}
</p>
<Link
href={logout()}
as="button"
type="button"
disabled={loggingOut}
onClick={() => {
close();
router.flushAll();
}}
onStart={() => setLoggingOut(true)}
onFinish={() => setLoggingOut(false)}
data-test="portal-logout-button"
className="inline-flex min-h-11 shrink-0 items-center gap-2 rounded-lg border border-slate-400 bg-white px-3 py-2 text-sm font-semibold text-slate-900 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 disabled:cursor-wait disabled:opacity-60"
>
<LogOut aria-hidden="true" className="size-4" />
{loggingOut ? 'A terminar sessão…' : 'Terminar sessão'}
</Link>
</nav>
)}
<nav aria-label="Áreas do portal">
<ul className="flex flex-wrap gap-2">
{groups.map((group) => (
@@ -293,7 +293,7 @@ export function SimulationCashflowPlanner({
{projection.vatRefundRecognized}. Estes valores não
foram somados à caixa.
</p>
<div className="overflow-x-auto">
<div className="overflow-x-auto focus-visible:outline-2" tabIndex={0} role="region" aria-label="Tabela do fluxo de caixa">
<table className="min-w-full text-sm">
<caption className="sr-only">
Projecção mensal de tesouraria
@@ -369,7 +369,7 @@ export function SimulationFinancialTools({
efectiva converte a taxa anual para a
periodicidade indicada.
</p>
<div className="max-h-64 overflow-auto">
<div className="max-h-64 overflow-auto focus-visible:outline-2" tabIndex={0} role="region" aria-label="Plano de reembolso indicativo">
<table className="min-w-full text-xs">
<caption className="sr-only">
Plano de reembolso indicativo
+19 -3
View File
@@ -1154,6 +1154,7 @@ export function SimulationForm() {
useEffect(() => {
let cancelled = false;
setReady(false);
void (async () => {
let local: SimulationDraft = {};
const requestedDraftId = new URLSearchParams(
@@ -1229,6 +1230,7 @@ export function SimulationForm() {
}
}
if (payload.data) {
if (cancelled) return;
draftIdRef.current = payload.data.id;
draftEtagRef.current = response.headers.get('ETag');
setDraftVersionNo(payload.data.version_no);
@@ -1475,7 +1477,7 @@ export function SimulationForm() {
]);
async function returnToInvestment() {
if (!contextId || !draftIdRef.current) return;
if (!ready || !contextId || !draftIdRef.current) return;
setContinuityError('');
try {
await saveQueueRef.current;
@@ -1496,8 +1498,14 @@ export function SimulationForm() {
'Não foi possível recuperar o rascunho guardado.',
);
if (
saved.data.economic_snapshot.financial_tools_snapshot !==
draft.financial_tools_snapshot
simulationDraftFingerprint(
restoreSimulationDraft(
saved.data.economic_snapshot,
Array.isArray(saved.data.personal_snapshot)
? {}
: saved.data.personal_snapshot,
),
) !== simulationDraftFingerprint(draft)
)
throw new Error(
'Aguarde a gravação das entradas da ferramenta e tente novamente.',
@@ -1912,6 +1920,7 @@ export function SimulationForm() {
value: string | string[],
label?: string | string[],
) {
if (!ready) return;
setContinueLaterSaved(false);
setServerFieldErrors((previous) => {
const next = { ...previous };
@@ -2035,6 +2044,7 @@ export function SimulationForm() {
}
function navigate(nextStep: number) {
if (!ready) return;
setOpenedTool(null);
if (nextStep > step) {
for (let index = step; index < nextStep; index++) {
@@ -2390,7 +2400,12 @@ export function SimulationForm() {
<section
className="space-y-6 rounded-xl border bg-white p-4 sm:p-6"
aria-labelledby="simulator-step-title"
aria-busy={!ready}
>
{!ready && (
<p role="status">A recuperar o rascunho. Aguarde para editar.</p>
)}
<fieldset disabled={!ready} className="min-w-0 space-y-6">
{canSavePersonalData && draftVersionNo && draftIdRef.current && (
<a
className="inline-block underline"
@@ -4336,6 +4351,7 @@ export function SimulationForm() {
associados à conta. A submissão formal ainda não está disponível
neste formulário.
</p>
</fieldset>
</section>
);
}
+8 -4
View File
@@ -165,6 +165,8 @@ function Sidebar({
return (
<div
data-slot="sidebar"
role="complementary"
aria-label="Navegação da conta"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className
@@ -179,10 +181,6 @@ function Sidebar({
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
@@ -195,6 +193,10 @@ function Sidebar({
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Navegação da conta</SheetTitle>
<SheetDescription>Menu móvel do portal.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
@@ -209,6 +211,8 @@ function Sidebar({
data-variant={variant}
data-side={side}
data-slot="sidebar"
role="complementary"
aria-label="Navegação da conta"
>
{/* This is what handles the sidebar gap on desktop */}
<div
@@ -55,7 +55,7 @@ export function UserMenuContent({ user }: Props) {
data-test="logout-button"
>
<LogOut className="mr-2" />
Log out
Terminar sessão
</Link>
</DropdownMenuItem>
</>
+7 -1
View File
@@ -18,6 +18,8 @@ export const portalLinks = [
['/apoio', 'Apoio ao Investidor'],
] as const;
const names: Record<string, string> = {
'/explorar/agronegocio': 'Explorar agronegócio',
'/agronegocio': 'Artigo de agronegócio',
'/investidor/negocios': 'Os meus negócios',
'/management/negocios': 'Revisão de negócios',
...Object.fromEntries(portalLinks),
@@ -92,13 +94,15 @@ export default function PortalNavigation({
return (
<MenuPlacement.Provider value={true}>
<div className="pb-8 lg:pb-24 [&_button]:scroll-mb-24 [&_input]:scroll-mb-24 [&_select]:scroll-mb-24 [&_textarea]:scroll-mb-24">
{portalShell && <PortalMenu primary />}
<nav aria-label="Atalho para o conteúdo">
<a
href="#portal-content"
className="sr-only focus:not-sr-only focus:block focus:bg-white focus:p-4 focus:text-emerald-900"
>
Saltar para o conteúdo
</a>
</nav>
{portalShell && <PortalMenu primary />}
<nav
aria-label="Caminho de navegação"
className="bg-background text-foreground flex flex-wrap items-center gap-2 border-b px-5 py-3 text-sm"
@@ -127,12 +131,14 @@ export default function PortalNavigation({
{portalShell && <JourneyCompanion />}
{children}
</div>
<aside aria-label="Contacto de apoio ao investidor">
<a
href="/apoio"
className="mx-3 mt-4 block max-w-[calc(100%-1.5rem)] rounded-xl bg-emerald-800 px-5 py-3 text-center text-sm font-semibold text-white shadow-xl focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-emerald-600"
>
Contactar Apoio ao Investidor
</a>
</aside>
</div>
</MenuPlacement.Provider>
);
+68 -8
View File
@@ -13,6 +13,7 @@ export type PortalNavAccess = {
businesses?: boolean;
education?: boolean;
ai?: boolean;
legacyContent?: boolean;
profileAccess?: ProfileAccess | null;
opportunityManagement?: OpportunityManagementAccess | null;
};
@@ -32,6 +33,15 @@ export function portalNavigation(access: PortalNavAccess): PortalNavItem[] {
children: [
link('opportunities', 'Oportunidades', '/oportunidades'),
link('sectors', 'Sectores', '/sectores'),
...(access.legacyContent
? [
link(
'legacy-agronegocio',
'Explorar agronegócio',
'/explorar/agronegocio',
),
]
: []),
{
id: 'structures',
label: 'Carteira e estruturas',
@@ -279,14 +289,64 @@ export function portalNavigation(access: PortalNavAccess): PortalNavItem[] {
});
if (access.authenticated && access.profileAccess) {
const profile = access.profileAccess;
groups.push({id: 'profile-opportunities', label: 'Perfil e oportunidades', children: [
...(profile.mine ? [link('my-opportunities', 'Minhas oportunidades', profile.mine)] : []),
...(profile.create ? [link('new-my-opportunity', 'Nova oportunidade', profile.create)] : []),
link('promoter-profile', profile.mine ? 'Autorizações de Promotor' : 'Solicitar perfil de Promotor', profile.request),
...(!profile.mine && profile.review ? [link('profile-review', 'Revisão de oportunidades', profile.review)] : []),
...(profile.audit ? [link('profile-audit', 'Auditoria de acessos', profile.audit)] : []),
...(profile.administration ? [link('profile-administration', 'Gestão de perfis', profile.administration)] : []),
]});
groups.push({
id: 'profile-opportunities',
label: 'Perfil e oportunidades',
children: [
...(profile.mine
? [
link(
'my-opportunities',
'Minhas oportunidades',
profile.mine,
),
]
: []),
...(profile.create
? [
link(
'new-my-opportunity',
'Nova oportunidade',
profile.create,
),
]
: []),
link(
'promoter-profile',
profile.mine
? 'Autorizações de Promotor'
: 'Solicitar perfil de Promotor',
profile.request,
),
...(!profile.mine && profile.review
? [
link(
'profile-review',
'Revisão de oportunidades',
profile.review,
),
]
: []),
...(profile.audit
? [
link(
'profile-audit',
'Auditoria de acessos',
profile.audit,
),
]
: []),
...(profile.administration
? [
link(
'profile-administration',
'Gestão de perfis',
profile.administration,
),
]
: []),
],
});
}
return groups;
}
+6 -3
View File
@@ -102,7 +102,7 @@ function TopBar({
requestCount: number;
}) {
return (
<header className="sticky top-0 z-30 border-b border-slate-200 bg-white/95 backdrop-blur">
<nav aria-label="Navegação das manifestações" className="sticky top-0 z-30 border-b border-slate-200 bg-white/95 backdrop-blur">
<div className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-3 px-5 py-4">
<button
onClick={() => onNavigate('home')}
@@ -129,7 +129,7 @@ function TopBar({
</button>
</div>
</div>
</header>
</nav>
);
}
@@ -863,6 +863,7 @@ function InterestForm({
<Field label="País" error={errors.country}>
<CountrySelect
countries={countries}
label="País do requerente"
value={form.country}
onChange={(value) => update('country', value)}
required
@@ -876,6 +877,7 @@ function InterestForm({
>
<CountrySelect
countries={countries}
label="País de origem declarado desta solicitação"
value={form.declared_origin_country_code ?? ''}
onChange={(value) =>
update('declared_origin_country_code', value)
@@ -1100,7 +1102,7 @@ export default function PortalInvestidorApp({
<div className="min-h-screen bg-slate-50 font-sans text-slate-900">
<TopBar onNavigate={navigate} requestCount={requestCount} />
<PortalMenu />
<main>
{view === 'home' && (
<>
<Hero
@@ -1329,6 +1331,7 @@ export default function PortalInvestidorApp({
/>
)}
</main>
<Footer />
<style>{`
+2 -2
View File
@@ -47,7 +47,7 @@ export default function Dashboard({
} | null;
}) {
return (
<main className="space-y-8 p-4 md:p-8">
<div className="space-y-8 p-4 md:p-8">
<Head title="Painel" />
<h1 className="text-2xl font-semibold">Painel do portal</h1>
<ProfileNavigation />
@@ -305,7 +305,7 @@ export default function Dashboard({
<Link href="/" className="inline-block underline">
Voltar ao portal público
</Link>
</main>
</div>
);
}
Dashboard.layout = { breadcrumbs: [{ title: 'Painel', href: dashboard() }] };
+30
View File
@@ -38,6 +38,7 @@ export default function RequestPage({
payments,
caseVersion,
countries,
toolPrefill,
}: {
request: {
id: string;
@@ -80,6 +81,12 @@ export default function RequestPage({
payments: DeskPayment[];
caseVersion: number;
countries: Country[];
toolPrefill: {
description: string;
draft_id: string;
draft_version: number;
snapshot_sha256: string;
} | null;
events: {
id: string;
action: string;
@@ -142,6 +149,29 @@ export default function RequestPage({
</a>
)}
</section>
{canEdit && toolPrefill && (
<section className={card + ' space-y-3'} aria-label="Descrição guardada no projeto">
<h2 className="text-xl font-bold">Reutilizar dados do projeto</h2>
<p>A descrição está vazia neste pedido. Pode reutilizar o resumo já guardado na ferramenta deste dossier, versão {toolPrefill.draft_version}.</p>
<blockquote className="rounded border p-3 whitespace-pre-wrap">{toolPrefill.description}</blockquote>
<p>Esta ação guarda o rascunho com os dados preenchidos nesta página. Reveja-os antes de submeter.</p>
<button
type="button"
className={button}
disabled={form.processing || !!form.data.payload.description?.trim()}
onClick={() => {
form.transform((data) => ({
...data,
version: request.version,
submit: false,
payload: {...data.payload, description: toolPrefill.description},
tool_prefill: {draft_id:toolPrefill.draft_id, draft_version:toolPrefill.draft_version, snapshot_sha256:toolPrefill.snapshot_sha256},
}));
form.put(`/balcao-virtual/pedidos/${request.id}`, {onSuccess: () => form.setData('payload', {...form.data.payload, description:toolPrefill.description})});
}}
>Reutilizar descrição guardada e guardar rascunho</button>
</section>
)}
<section className={card + ' space-y-3'}>
<h2 className="text-xl font-bold">
Verificações automáticas de preparação
+36 -2
View File
@@ -1,5 +1,6 @@
import { Head, Link, useForm } from '@inertiajs/react';
import { Head, Link, router, useForm } from '@inertiajs/react';
import { useState } from 'react';
import type { LegacyArticle } from '@/pages/portal/legacy';
export type InvestmentContext = {
id: string;
@@ -10,15 +11,31 @@ export type InvestmentContext = {
export default function InvestmentContexts({
contexts,
contentSource,
}: {
contexts: InvestmentContext[];
contentSource?: LegacyArticle;
}) {
const [key] = useState(() => globalThis.crypto.randomUUID());
const form = useForm({ title: '', client_key: key, reuse_consent: false });
const form = useForm({
title: '',
client_key: key,
reuse_consent: false,
content_resource_id: contentSource?.id,
});
return (
<main className="mx-auto max-w-5xl space-y-6 p-4 md:p-8">
<Head title="Os meus investimentos" />
<h1 className="text-2xl font-semibold">Os meus investimentos</h1>
{contentSource && (
<p>
Preparar a partir de{' '}
<Link className="underline" href={contentSource.href}>
{contentSource.title}
</Link>
. A fonte e a versão serão guardadas no dossier escolhido.
</p>
)}
<p>
Prepare cada investimento no seu dossier. Pode trabalhar em
várias etapas em paralelo.
@@ -36,6 +53,23 @@ export default function InvestmentContexts({
Guardado em{' '}
{new Date(item.updated_at).toLocaleString('pt-MZ')}
</p>
{contentSource && (
<button
type="button"
className="mt-3 rounded border p-2"
onClick={() =>
router.post(
`/investimentos/${item.id}/conteudos`,
{
version: item.version,
resource_id: contentSource.id,
},
)
}
>
Continuar neste dossier com o artigo
</button>
)}
</li>
))}
</ul>
+41 -3
View File
@@ -31,6 +31,13 @@ type Path = {
next: { code: string; title: string; href: string; action: string } | null;
tools: Record<string, string | null>;
results: PathResult[];
content_sources: {
resource_id: number;
title: string;
return_href: string;
source_url: string;
metadata_version: number;
}[];
documents: {
id: string;
document_id: string;
@@ -349,12 +356,43 @@ export default function InvestmentPath(path: Path) {
);
}
return (
<main className="mx-auto max-w-6xl space-y-6 p-4 md:p-8">
<main className="mx-auto min-w-0 max-w-6xl space-y-6 p-4 [overflow-wrap:anywhere] [&_label]:min-w-0 [&_select]:min-w-0 [&_select]:max-w-full [&_input]:min-w-0 [&_input]:max-w-full md:p-8">
<Head title={path.case.title} />
<Link className="underline" href="/investimentos">
Os meus investimentos
</Link>
<h1 className="text-2xl font-semibold">{path.case.title}</h1>
{!!path.content_sources?.length && (
<section
className="space-y-2 rounded-xl border p-4"
aria-label="Fontes da ideia"
>
<h2 className="font-semibold">
Conteúdos associados à ideia
</h2>
<ul>
{path.content_sources.map((source) => (
<li
key={`${source.resource_id}-${source.metadata_version}`}
>
<Link
className="underline"
href={`${source.return_href}?context=${path.case.id}`}
>
{source.title}
</Link>{' '}
·{' '}
<Link
className="underline"
href={`/documentos/${source.resource_id}`}
>
Exemplar e origem
</Link>
</li>
))}
</ul>
</section>
)}
{flash?.success && <output>{flash.success}</output>}
{error && <p role="alert">{error}</p>}
<section className="rounded-xl border p-4">
@@ -580,7 +618,7 @@ export default function InvestmentPath(path: Path) {
</h2>
{path.canWrite && (
<form
className="grid gap-3"
className="grid min-w-0 gap-3 [&_label]:min-w-0 [&_select]:min-w-0 [&_select]:max-w-full [&_input]:min-w-0 [&_input]:max-w-full"
onSubmit={(event) => {
event.preventDefault();
task.transform((data) => ({
@@ -630,7 +668,7 @@ export default function InvestmentPath(path: Path) {
<input
required
type="datetime-local"
className="rounded border p-2"
className="w-full min-w-0 rounded border p-2"
value={task.data.due_at}
onChange={(e) =>
task.setData('due_at', e.target.value)
@@ -157,6 +157,7 @@ export default function OpportunityResources({
const [busy, setBusy] = useState(false);
const [message, setMessage] = useState('');
const [errors, setErrors] = useState<Record<string, string>>({});
const foreignCountry = Boolean(payload.country_code && payload.country_code !== 'MZ');
const set = (name: string, value: Payload[string]) =>
setPayload((previous) => ({ ...previous, [name]: value }));
function select(nextKind: Kind, record: Resource | null = null) {
@@ -239,7 +240,7 @@ export default function OpportunityResources({
}
setMessage(data.message);
setEditing(null);
setPayload({});
setPayload(kind === 'locations' ? {country_code:'MZ'} : {});
setPath([]);
setGeometry('');
setFile(null);
@@ -272,8 +273,9 @@ export default function OpportunityResources({
</Link>
<p>
Recursos de preparação. Não alteram automaticamente a ficha
pública ou relações territoriais aceites. ClamD real pendente;
scan simulado nunca verifica ficheiros reais.
pública ou relações territoriais aceites. O estado e o motor
do scan são apresentados por ficheiro e versão; um scan
simulado nunca verifica ficheiros reais.
</p>
<div
id="resource-result"
@@ -300,9 +302,8 @@ export default function OpportunityResources({
<p className="rounded border p-3">
Pode guardar anexos para preparação. Cada ficheiro permanece
sem download ou pré-visualização enquanto o seu scan não
estiver concluído. ClamD pendente não impede guardar
metadados, locais ou rascunhos; testes com scanner simulado
não verificam os seus ficheiros.
estiver concluído. Guardar metadados, locais ou rascunhos
é independente do scan e não publica ficheiros.
</p>
)}
<section>
@@ -411,10 +412,37 @@ export default function OpportunityResources({
</h2>
{kind === 'locations' && (
<div>
<Field label="País da localização" error={errors.country_code}><select className={control} disabled={readOnly} value={String(payload.country_code ?? '')} onChange={e=>set('country_code',e.target.value)}><option value="">Por indicar</option>{countries.map(c=><option key={c.code} value={c.code}>{c.name}</option>)}</select></Field>
{payload.country_code !== 'MZ' && <p>A hierarquia abaixo corresponde a Moçambique. Uma localização noutro país requer o seu próprio cadastro; não use territórios moçambicanos para a representar.</p>}
<Field label="País da localização" error={errors.country_code}>
<select
className={control}
disabled={readOnly}
value={String(payload.country_code ?? '')}
onChange={(event) => {
const country = event.target.value;
set('country_code', country || null);
if (country && country !== 'MZ' && path.length) {
setPath([]);
setMessage('A seleção territorial de Moçambique foi retirada do preenchimento ao mudar de país. Guarde para registar a alteração; a geometria própria foi preservada.');
}
}}
>
<option value="">Por indicar</option>
{countries.map(c=><option key={c.code} value={c.code}>{c.name}</option>)}
</select>
</Field>
{foreignCountry && (
<p>A hierarquia abaixo corresponde a Moçambique e não é aplicável ao país escolhido. Pode guardar uma localização incompleta noutro país sem esta hierarquia.</p>
)}
{foreignCountry && path.length > 0 && (
<p role="alert">
Este registo conserva uma seleção territorial de Moçambique incompatível com o país. Escolha Moçambique ou{' '}
<button type="button" className="underline" disabled={readOnly} onClick={() => setPath([])}>
retirar a seleção territorial deste preenchimento
</button>. O registo guardado só muda quando guardar.
</p>
)}
<TerritoryGeometryPreview
territoryId={path.at(-1)?.id}
territoryId={foreignCountry ? undefined : path.at(-1)?.id}
management
endpoint={basePath === '/minhas-oportunidades' ? '/minhas-oportunidades/territory-geometries' : undefined}
/>
@@ -424,6 +452,7 @@ export default function OpportunityResources({
index={index}
parent={path[index - 1]?.id ?? ''}
selected={path[index]}
disabled={readOnly || foreignCountry}
endpoint={basePath === '/minhas-oportunidades' ? '/minhas-oportunidades/territory-options' : '/management/territory-options'}
onChange={(value) =>
setPath((previous) =>
+2 -2
View File
@@ -55,7 +55,7 @@ export function Management({
aiEnabled?: boolean;
}>().props;
return (
<main className="space-y-6 p-4 md:p-8">
<div className="space-y-6 p-4 md:p-8">
<Head title={title} />
{auth?.canManagePortal && basePath !== '/minhas-oportunidades' ? <nav
className="flex flex-wrap gap-4 text-sm underline"
@@ -116,7 +116,7 @@ export function Management({
</p>
)}
{children}
</main>
</div>
);
}
+301
View File
@@ -0,0 +1,301 @@
import { Head, Link, useForm } from '@inertiajs/react';
export type LegacyArticle = {
id: number;
title: string;
author: string;
legacy_id: number;
legacy_category: string;
published_at: string;
modified_at: string;
source_url: string;
href: string;
landing_href: string;
start_href: string;
group: { label: string; heading: string };
related_articles: { id: number; slug: string; title: string; href: string }[];
html: string;
library_href: string;
image: { href: string; alt: string; source_url: string } | null;
sector: { name: string; slug: string } | null;
territorial_references: string[];
territorial_status: string;
opportunities: {
title: string;
slug: string;
province: string;
summary: string;
}[];
};
export default function LegacySector({
article,
articles = [article],
view,
query = '',
match = true,
contentContext,
}: {
article: LegacyArticle;
articles?: LegacyArticle[];
view: 'landing' | 'article';
query?: string;
match?: boolean;
contentContext?: { id: string; title: string } | null;
}) {
const search = useForm({ q: query });
return (
<main className="legacy-agronegocio mx-auto max-w-6xl space-y-8 rounded-2xl bg-[#f6f3ea] p-4 sm:p-8">
<Head
title={
view === 'landing' ? `Explorar ${article.group.label}` : article.title
}
/>
<style>{`
.legacy-agronegocio { color: #10352b; font-family: 'Noto Sans', system-ui, sans-serif; }
.legacy-agronegocio h1, .legacy-agronegocio h2 { font-family: 'Noto Serif', Georgia, serif; }
.legacy-agronegocio a, .legacy-agronegocio button { outline-offset: 4px; }
.legacy-agronegocio a:focus-visible, .legacy-agronegocio button:focus-visible { outline: 3px solid #b22222; }
.legacy-article { font-size: 1.0625rem; line-height: 1.8; overflow-wrap: anywhere; }
.legacy-article p, .legacy-article ul, .legacy-article ol { margin-block: 1.2em; }
.legacy-article ul { list-style: disc; padding-inline-start: 1.6em; }
.legacy-article ol { list-style: decimal; padding-inline-start: 1.6em; }
.legacy-article a { text-decoration: underline; }
`}</style>
<nav className="flex flex-wrap gap-4" aria-label={article.group.label}>
<Link href={article.landing_href} className="underline">
Explorar {article.group.label}
</Link>
<Link href="/sectores" className="underline">
Todos os setores
</Link>
<Link href={article.library_href} className="underline">
Exemplar na biblioteca
</Link>
</nav>
<header className="overflow-hidden rounded-2xl border bg-white">
{article.image && (
<img
className="aspect-video w-full object-cover"
src={article.image.href}
alt={article.image.alt}
width={900}
height={506}
/>
)}
<div className="space-y-3 p-5 sm:p-8">
<p className="text-sm font-semibold tracking-wide text-[#b22222] uppercase">
Conhecer · preparar · acompanhar
</p>
<h1 className="text-3xl font-semibold sm:text-4xl">
{view === 'landing'
? article.group.heading
: article.title}
</h1>
<p>
Explore o setor e prepare a sua ideia com o contexto
guardado no dossier.
</p>
<p className="text-sm">
Fonte: {article.author} · Publicado em{' '}
<time dateTime={article.published_at}>
{article.published_at.slice(0, 10)}
</time>{' '}
· Atualizado em{' '}
<time dateTime={article.modified_at}>
{article.modified_at.slice(0, 10)}
</time>
</p>
</div>
</header>
{view === 'landing' ? (
<section className="space-y-4" aria-labelledby="learn-title">
<h2 id="learn-title" className="text-2xl font-semibold">
Compreender o setor
</h2>
<form
onSubmit={(e) => {
e.preventDefault();
search.get(article.landing_href);
}}
className="flex flex-wrap gap-3"
>
<label className="grid flex-1 gap-1">
Pesquisar no conteúdo
<input
className="min-w-0 rounded border p-3"
value={search.data.q}
onChange={(e) =>
search.setData('q', e.target.value)
}
maxLength={120}
type="search"
/>
</label>
<button
className="self-end rounded bg-[#b22222] px-5 py-3 text-white"
disabled={search.processing}
>
Pesquisar
</button>
</form>
{match ? (
<div className="grid gap-4 sm:grid-cols-2">
{articles.map((item) => (
<article key={item.id} className="space-y-3 rounded-xl border bg-white p-5">
<h3 className="text-xl font-semibold">
<Link className="underline" href={item.href}>
{item.title}
</Link>
</h3>
<p>
Conteúdo editorial preservado do portal Tempo.
</p>
<Link
className="inline-block rounded bg-[#10352b] px-5 py-3 text-white"
href={item.href}
>
Ler o artigo
</Link>
</article>
))}
</div>
) : (
<p role="status">
Nenhum conteúdo corresponde à pesquisa.
</p>
)}
</section>
) : (
<section
aria-label="Artigo educativo"
className="legacy-article max-w-3xl"
dangerouslySetInnerHTML={{ __html: article.html }}
/>
)}
{view === 'article' && article.related_articles.length > 0 && (
<section className="space-y-3" aria-labelledby="related-title">
<h2 id="related-title" className="text-2xl font-semibold">Outras leituras no setor</h2>
<ul className="grid gap-3 sm:grid-cols-2">
{article.related_articles.map((item) => (
<li key={item.id}><Link className="underline" href={item.href}>{item.title}</Link></li>
))}
</ul>
</section>
)}
<section
className="space-y-4 rounded-xl border bg-white p-5"
aria-labelledby="locations-title"
>
<h2 id="locations-title" className="text-2xl font-semibold">
Contexto territorial
</h2>
<ul className="flex flex-wrap gap-3">
{article.territorial_references.map((name) => (
<li key={name} className="rounded border px-3 py-2">
{name}
</li>
))}
</ul>
<p>{article.territorial_status}</p>
<div className="flex flex-wrap gap-4">
<Link href="/carteira?kind=corridor" className="underline">
Consultar os corredores cadastrados
</Link>
<Link href="/zonas" className="underline">
Consultar localizações e mapas disponíveis
</Link>
</div>
</section>
<section
className="space-y-4"
aria-labelledby="opportunities-title"
>
<h2 id="opportunities-title" className="text-2xl font-semibold">
Oportunidades publicadas no setor
</h2>
<p>
Cadastros independentes do artigo, com a sua própria autoria
e situação de publicação.
</p>
{article.opportunities.length ? (
<ul className="grid gap-4 sm:grid-cols-2">
{article.opportunities.map((o) => (
<li
key={o.slug}
className="rounded-xl border bg-white p-5"
>
<h3 className="font-semibold">
<Link
className="underline"
href={`/oportunidades/${o.slug}`}
>
{o.title}
</Link>
</h3>
<p>{o.province}</p>
<p>{o.summary}</p>
</li>
))}
</ul>
) : (
<p>
Ainda não há oportunidades publicadas neste setor. Pode
desenvolver uma ideia no seu dossier.
</p>
)}
</section>
<section
className="space-y-3 rounded-xl bg-[#10352b] p-6 text-white"
aria-labelledby="prepare-title"
>
<h2 id="prepare-title" className="text-2xl font-semibold">
Preparar a sua ideia
</h2>
<p>
A leitura é pública. Para guardar dados pessoais, escolha ou
crie um dossier na sua conta.
</p>
<Link
className="inline-block rounded bg-white px-5 py-3 font-semibold text-[#10352b]"
href={
contentContext
? `/investimentos/${contentContext.id}`
: article.start_href
}
>
{contentContext
? `Continuar: ${contentContext.title}`
: 'Iniciar ou continuar uma ideia'}
</Link>
</section>
<footer className="space-y-2 border-t pt-4 text-sm">
<p>
Conteúdo editorial de {article.author}. Categoria original:{' '}
{article.legacy_category}. Não representa uma oportunidade
concreta, autorização de investimento ou cálculo.
</p>
<a
className="underline"
href={article.source_url}
rel="noopener noreferrer"
>
Consultar a página original
</a>
{article.image && (
<p>
<a
className="underline"
href={article.image.source_url}
rel="noopener noreferrer"
>
Origem da imagem
</a>{' '}
· Autoria individual e data de criação não indicadas no
legado.
</p>
)}
</footer>
</main>
);
}
+4 -2
View File
@@ -161,7 +161,9 @@ export default function Manage({
requirementCatalogue,
requirementAssociationsEnabled,
disclosureAuthorization,
legacyContentEnabled = false,
}: {
legacyContentEnabled?: boolean;
documents: DocumentFile[];
resources: Resource[];
requirementCatalogue: { key: string; label: string; tax: string }[];
@@ -533,9 +535,9 @@ export default function Manage({
}
/>
</Field>
<Field label="Assunto">
<Field label={legacyContentEnabled ? 'Assunto (opcional)' : 'Assunto'}>
<input
required
required={!legacyContentEnabled}
className={control}
value={form.data.topic}
onChange={(e) =>
+2
View File
@@ -264,6 +264,7 @@ function RequestForm({
{field === 'country' ? (
<CountrySelect
countries={countries}
label="País do requerente"
value={form.data.country}
onChange={(value) =>
form.setData('country', value)
@@ -290,6 +291,7 @@ function RequestForm({
País de origem declarado desta solicitação
<CountrySelect
countries={countries}
label="País de origem declarado desta solicitação"
value={form.data.declared_origin_country_code}
onChange={(value) =>
form.setData('declared_origin_country_code', value)
+1 -1
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['dark' => ($appearance ?? 'system') == 'dark'])>
<html lang="{{ str_replace('_', '-', config('portal.interface_language', app()->getLocale())) }}" @class(['dark' => ($appearance ?? 'system') == 'dark'])>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
+3
View File
@@ -1,12 +1,15 @@
<?php
use App\Http\Controllers\InvestorPathController as Path;
use App\Http\Controllers\LegacyContentController;
use App\Http\Middleware\PrivateInvestmentResponse;
use App\Http\Middleware\VirtualDeskInstalled;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth', 'verified', PrivateInvestmentResponse::class, VirtualDeskInstalled::class, 'throttle:60,1'])->prefix('investimentos')->group(function () {
Route::get('/', [Path::class, 'index']);
Route::get('/a-partir-de/{legacySlug}', [LegacyContentController::class, 'start'])->whereIn('legacySlug', array_keys(config('legacy_content.articles', [])));
Route::post('/{id}/conteudos', [LegacyContentController::class, 'attach'])->whereUuid('id');
Route::post('/', [Path::class, 'create']);
Route::get('/{id}', [Path::class, 'show'])->whereUuid('id');
Route::post('/{id}/aplicabilidade', [Path::class, 'facts'])->whereUuid('id');
+8 -3
View File
@@ -22,11 +22,13 @@ use App\Http\Controllers\InvestmentStructureController;
use App\Http\Controllers\InvestmentStructureRelationController;
use App\Http\Controllers\InvestmentTerritoryController;
use App\Http\Controllers\InvestorWorkspaceController;
use App\Http\Controllers\LegacyContentController;
use App\Http\Controllers\OpportunityManagementController;
use App\Http\Controllers\OpportunityOriginReviewController;
use App\Http\Controllers\OpportunityResourceController;
use App\Http\Controllers\PortalManagementController;
use App\Http\Controllers\PortalProfileController;
use App\Http\Controllers\PortalProfileInstallationController;
use App\Http\Controllers\PortalServicesController;
use App\Http\Controllers\PublicBusinessOpportunityController;
use App\Http\Controllers\PublicGeographyController;
@@ -51,6 +53,8 @@ Route::get('/Simulador', [PortalServicesController::class, 'page'])->defaults('p
Route::get('/documentos/{id}', [PortalServicesController::class, 'showDocument'])->whereNumber('id')->name('services.document');
Route::get('/documentos/{id}/view', [PortalServicesController::class, 'viewDocument'])->whereNumber('id')->name('services.document.view');
Route::get('/documentos/{id}/download', [PortalServicesController::class, 'download'])->whereNumber('id')->name('services.download');
Route::get('/explorar/{sectorSlug}', [LegacyContentController::class, 'landing'])->whereIn('sectorSlug', array_keys(config('legacy_content.groups', [])));
Route::get('/{legacySlug}', [LegacyContentController::class, 'article'])->whereIn('legacySlug', array_keys(config('legacy_content.articles', [])));
Route::post('/apoio', [PortalServicesController::class, 'submit'])->middleware('throttle:10,1')->name('services.submit');
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/balcao-virtual', [PortalServicesController::class, 'page'])->defaults('page', 'desk')->name('services.desk');
@@ -112,9 +116,9 @@ Route::get('/api/v1/investment/territory-geometries', [InvestmentTerritoryContro
// Installation must be reachable before the profile schema/feature is enabled.
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/management/profile-install', [\App\Http\Controllers\PortalProfileInstallationController::class, 'preview']);
Route::post('/management/profile-install', [\App\Http\Controllers\PortalProfileInstallationController::class, 'apply']);
Route::post('/management/profile-install/bootstrap', [\App\Http\Controllers\PortalProfileInstallationController::class, 'bootstrap']);
Route::get('/management/profile-install', [PortalProfileInstallationController::class, 'preview']);
Route::post('/management/profile-install', [PortalProfileInstallationController::class, 'apply']);
Route::post('/management/profile-install/bootstrap', [PortalProfileInstallationController::class, 'bootstrap']);
});
Route::middleware(['auth', 'verified', EnsurePortalProfiles::class])->group(function () {
@@ -138,6 +142,7 @@ Route::middleware(['auth', 'verified', EnsurePortalProfiles::class])->group(func
Route::get('/minhas-oportunidades/create', [$profiles, 'edit']);
Route::get('/minhas-oportunidades/{id}/edit', [$profiles, 'edit'])->whereNumber('id');
Route::get('/minhas-oportunidades/{id}/view', [$profiles, 'edit'])->whereNumber('id');
Route::get('/minhas-oportunidades/{id}/response', [OpportunityManagementController::class, 'canonicalResponse'])->whereNumber('id');
Route::post('/minhas-oportunidades/drafts', [OpportunityManagementController::class, 'save']);
Route::put('/minhas-oportunidades/{id}/draft', [OpportunityManagementController::class, 'save'])->whereNumber('id');
Route::post('/minhas-oportunidades/{id}/workflow', [$profiles, 'workflow'])->whereNumber('id');
+30
View File
@@ -50,6 +50,36 @@ class InvestorPathTest extends TestCase
'incomes' => [['label' => 'Receitas QA', 'amount' => '100']], 'expenses' => [['label' => 'Custos QA', 'amount' => '50']]]])]];
}
public function test_existing_empty_desk_draft_can_explicitly_reuse_current_contextual_summary_without_overwriting_data(): void
{
$service = $this->actingAs($this->manager)->postJson('/management/balcao/servicos', ['code' => 'project-preparation', 'confirm' => true])->assertCreated()->json('id');
$id = $this->context('Dossier anterior à ferramenta');
$support = $this->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id');
$data = [...$this->snapshot('Resumo posterior'), 'context' => $id];
$draft = $this->postJson('/api/v1/simulation-drafts', $data, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
$initial = $this->getJson('/balcao-virtual/pedidos/'.$support)->assertOk()->assertJsonPath('request.payload.description', '')->json();
$preview = $initial['toolPrefill'] ?? null;
$this->assertNotNull($preview, 'The existing empty draft needs an explicit contextual reuse action.');
$this->assertSame($draft, $preview['draft_id']);
$this->assertSame($data['economic_snapshot']['project_summary'], $preview['description']);
$this->actingAs($this->manager)->getJson('/balcao-virtual/pedidos/'.$support)->assertJsonPath('toolPrefill', null);
$this->actingAs($this->owner);
$payload = [...$initial['request']['payload'], 'description' => $preview['description'], 'country' => 'PT', 'declared_origin_country_code' => 'ZA'];
$source = ['draft_id' => $preview['draft_id'], 'draft_version' => $preview['draft_version'], 'snapshot_sha256' => $preview['snapshot_sha256']];
$this->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $initial['request']['version'], 'payload' => $payload, 'tool_prefill' => [...$source, 'draft_id' => (string) Str::uuid()]])->assertUnprocessable();
$this->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $initial['request']['version'], 'payload' => $payload, 'tool_prefill' => [...$source, 'draft_version' => $source['draft_version'] + 1]])->assertUnprocessable();
$this->getJson('/balcao-virtual/pedidos/'.$support)->assertJsonPath('request.version', $initial['request']['version'])->assertJsonPath('request.payload.description', '');
$this->assertDatabaseCount('desk_revisions', 0);
$this->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $initial['request']['version'], 'payload' => $payload, 'tool_prefill' => $source])->assertOk()->assertJsonPath('status', 'draft');
$recovered = $this->getJson('/balcao-virtual/pedidos/'.$support)->assertJsonPath('request.payload.description', $preview['description'])->assertJsonPath('request.payload.country', 'PT')->assertJsonPath('request.payload.declared_origin_country_code', 'ZA')->assertJsonPath('toolPrefill', null)->json();
$this->assertSame('Dossier anterior à ferramenta', $recovered['request']['payload']['title']);
$this->assertDatabaseCount('desk_requests', 1);
$this->assertDatabaseCount('desk_revisions', 1);
$this->assertDatabaseHas('desk_events', ['request_id' => $support, 'action' => 'request_prefill_reused']);
$this->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $recovered['request']['version'], 'payload' => [...$payload, 'description' => 'Tentativa de substituir texto existente'], 'tool_prefill' => $source])->assertUnprocessable();
$this->actingAs(User::factory()->create())->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $recovered['request']['version'], 'payload' => $payload, 'tool_prefill' => $source])->assertForbidden();
}
public function test_two_independent_investments_recover_tool_evidence_support_and_internal_tasks_without_external_calls(): void
{
$service = $this->actingAs($this->manager)->postJson('/management/balcao/servicos', ['code' => 'project-preparation', 'confirm' => true])->assertCreated()->json('id');
+150
View File
@@ -0,0 +1,150 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\LegacyContent;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class LegacyContentTest extends TestCase
{
use RefreshDatabase;
private int $resource;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
Storage::fake('local');
config(['portal.legacy_content_qa_enabled' => true, 'investor-path.enabled' => true]);
$bytes = json_encode(['format' => 'investmoz-legacy-article-v1', 'source_url' => 'https://investemoz.tempo.co.mz/agronegocio',
'content_html' => '<p>Conhecer corredores</p><script>secret()</script><a href="javascript:alert(1)" onclick="bad()">Link</a>',
'title' => 'Agronegocio', 'author' => 'Autor sintético', 'legacy_id' => 171985, 'legacy_category' => 'Oportunidades',
'published_at' => '2026-04-06T09:19:07+00:00', 'modified_at' => '2026-04-20T12:41:43+00:00'], JSON_THROW_ON_ERROR);
$id = (string) Str::uuid();
Storage::disk('local')->put('legacy/'.$id, $bytes);
DB::table('documents')->insert(['id' => $id, 'source_system' => 'synthetic-test', 'external_id' => 'legacy-tempo-post-171985-20260420',
'file_name' => 'source.json', 'media_type' => 'application/json', 'storage_key' => 'legacy/'.$id,
'sha256' => hash('sha256', $bytes), 'malware_scanned_sha256' => hash('sha256', $bytes), 'malware_scan_status' => 'clean',
'size_bytes' => strlen($bytes), 'status' => 'active', 'rights_status' => 'authorized', 'data_classification' => 'public']);
$this->resource = DB::table('portal_resources')->insertGetId(['document_id' => $id, 'title' => 'Agronegocio',
'category' => 'education', 'topic' => 'agronegocio', 'published' => true, 'rights_status' => 'authorized',
'source_url' => 'https://investemoz.tempo.co.mz/agronegocio', 'source_reference' => 'Exemplar sintético; não oficial.', 'metadata_version' => 1]);
}
public function test_public_read_search_and_sanitization_do_not_create_opportunities_and_private_sources_are_hidden(): void
{
$count = DB::table('opportunities')->count();
$this->get('/agronegocio')->assertOk()->assertInertia(fn (Assert $page) => $page->component('portal/legacy')
->where('article.id', $this->resource)->where('article.calculation_use', false));
$html = app(LegacyContent::class)->article()['html'];
$this->assertStringNotContainsString('script', $html);
$this->assertStringNotContainsString('javascript', $html);
$this->assertStringNotContainsString('onclick', $html);
$this->get('/explorar/agronegocio?q=corredores')->assertOk()->assertInertia(fn (Assert $page) => $page->where('match', true));
$this->get('/explorar/agronegocio?q=absent')->assertOk()->assertInertia(fn (Assert $page) => $page->where('match', false));
$this->getJson('/investimentos/a-partir-de/agronegocio')->assertUnauthorized();
$this->assertSame($count, DB::table('opportunities')->count());
DB::table('portal_resources')->where('id', $this->resource)->update(['published' => false]);
$this->get('/agronegocio')->assertNotFound();
config(['portal.legacy_content_qa_enabled' => false]);
$this->get('/explorar/agronegocio')->assertNotFound();
}
public function test_original_heading_text_is_preserved_with_an_accessible_article_hierarchy(): void
{
$source = '<h3>Turismo</h3><h3>Territórios</h3><h4>Localizações</h4><script>unsafe()</script>';
$html = app(LegacyContent::class)->sanitize($source);
$this->assertSame('<h2>Turismo</h2><h2>Territórios</h2><h3>Localizações</h3>', $html);
$this->assertSame('<h2>Secção</h2><h3>Subsecção</h3>', app(LegacyContent::class)->sanitize('<h1>Secção</h1><h3>Subsecção</h3>'));
$this->assertStringContainsString('<h3>Turismo</h3>', $source);
}
public function test_source_is_saved_once_in_existing_dossier_recovered_and_isolated_from_other_accounts_and_projects(): void
{
$owner = User::factory()->create();
$other = User::factory()->create();
$data = ['title' => 'Ideia sintética', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, 'content_resource_id' => $this->resource];
$case = $this->actingAs($owner)->postJson('/investimentos', $data)->assertCreated()->json('id');
$first = $this->getJson('/investimentos/'.$case)->assertOk()->json();
$this->assertCount(19, $first['steps']);
$this->assertCount(5, $first['phases']);
$this->assertSame($this->resource, $first['content_sources'][0]['resource_id']);
$this->postJson('/investimentos/'.$case.'/conteudos', ['version' => $first['case']['version'], 'resource_id' => $this->resource])->assertOk();
$again = $this->getJson('/investimentos/'.$case)->assertOk()->json();
$this->assertSame($first['case']['version'], $again['case']['version']);
$this->assertSame($first['content_sources'], $again['content_sources']);
$this->assertSame($case, $this->postJson('/investimentos', $data)->assertCreated()->json('id'));
$this->actingAs($other)->getJson('/investimentos/'.$case)->assertForbidden();
$this->postJson('/investimentos/'.$case.'/conteudos', ['version' => $first['case']['version'], 'resource_id' => $this->resource])->assertForbidden();
$this->actingAs($owner);
unset($data['content_resource_id']);
$data['client_key'] = (string) Str::uuid();
$independent = $this->postJson('/investimentos', $data)->assertCreated()->json('id');
$this->assertSame([], $this->getJson('/investimentos/'.$independent)->assertOk()->json('content_sources'));
$before = DB::table('desk_cases')->count();
$data['client_key'] = (string) Str::uuid();
$data['content_resource_id'] = $this->resource + 100;
$this->postJson('/investimentos', $data)->assertUnprocessable();
$this->assertSame($before, DB::table('desk_cases')->count());
}
private function sectorArticle(string $slug, string $title, string $body): int
{
$identity = config('legacy_content.articles.'.$slug);
$document = (string) Str::uuid();
$bytes = json_encode(['format' => 'investmoz-legacy-article-v1', 'source_url' => $identity['source_url'],
'content_html' => $body, 'title' => $title, 'author' => 'Autor sintético', 'legacy_id' => 123,
'legacy_category' => 'Turismo', 'published_at' => '2026-04-01T00:00:00+00:00', 'modified_at' => '2026-04-02T00:00:00+00:00'], JSON_THROW_ON_ERROR);
Storage::disk('local')->put('legacy/'.$document, $bytes);
DB::table('documents')->insert(['id' => $document, 'source_system' => 'synthetic-test', 'external_id' => $identity['external_id'],
'file_name' => 'source.json', 'media_type' => 'application/json', 'storage_key' => 'legacy/'.$document,
'sha256' => hash('sha256', $bytes), 'malware_scanned_sha256' => hash('sha256', $bytes), 'malware_scan_status' => 'clean',
'size_bytes' => strlen($bytes), 'status' => 'active', 'rights_status' => 'authorized', 'data_classification' => 'public']);
return DB::table('portal_resources')->insertGetId(['document_id' => $document, 'title' => $title, 'category' => 'education',
'topic' => $identity['sector_slug'], 'published' => true, 'rights_status' => 'authorized',
'source_url' => $identity['source_url'], 'source_reference' => 'Exemplar sintético.', 'metadata_version' => 1]);
}
public function test_energy_and_mining_remain_distinct_and_each_reference_returns_to_its_article(): void
{
$this->sectorArticle('energia-mineracao', 'Energia e Mineração', '<a href="https://investemoz.tempo.co.mz/energia/">Energia</a><a href="https://investemoz.tempo.co.mz/mineracao">Mineração</a>');
$energy = $this->sectorArticle('energia', 'Energia', '<p>Rede e geração.</p>');
$mining = $this->sectorArticle('mineracao', 'Mineração', '<p>Recursos minerais.</p>');
$this->get('/explorar/energia-mineracao?q=geração')->assertOk()->assertInertia(fn (Assert $page) =>
$page->has('articles', 1)->where('articles.0.id', $energy));
$index = app(LegacyContent::class)->article('energia-mineracao');
$this->assertStringContainsString('href="/energia"', $index['html']);
$this->assertStringContainsString('href="/mineracao"', $index['html']);
$owner = User::factory()->create();
$case = $this->actingAs($owner)->postJson('/investimentos', ['title' => 'Energia sintética', 'client_key' => (string) Str::uuid(),
'reuse_consent' => true, 'content_resource_id' => $energy])->assertCreated()->json('id');
$first = $this->getJson('/investimentos/'.$case)->assertOk()->json();
$this->postJson('/investimentos/'.$case.'/conteudos', ['version' => $first['case']['version'], 'resource_id' => $mining])->assertOk();
$sources = $this->getJson('/investimentos/'.$case)->assertOk()->json('content_sources');
$this->assertSame(['/energia', '/mineracao'], array_column($sources, 'return_href'));
$this->assertSame([$energy, $mining], array_column($sources, 'resource_id'));
DB::table('portal_resources')->where('id', $mining)->update(['published' => false]);
$this->get('/mineracao')->assertNotFound();
$this->assertStringContainsString('https://investemoz.tempo.co.mz/mineracao', app(LegacyContent::class)->article('energia-mineracao')['html']);
}
public function test_tourism_is_textual_and_empty_financial_source_is_not_a_published_article(): void
{
$this->sectorArticle('turismo', 'Turismo', '<p>Província da Zambézia e turismo.</p>');
$tourism = app(LegacyContent::class)->article('turismo');
$this->assertSame([], $tourism['territorial_references']);
$this->assertFalse(DB::table('portal_resource_links')->where('target_type', 'territory')->exists());
$finance = $this->sectorArticle('mercado-financeiro', 'Mercado Financeiro', '');
$this->get('/mercado-financeiro')->assertNotFound();
$this->get('/explorar/mercado-financeiro')->assertNotFound();
$this->actingAs(User::factory()->create())->postJson('/investimentos', ['title' => 'Ideia', 'client_key' => (string) Str::uuid(),
'reuse_consent' => true, 'content_resource_id' => $finance])->assertUnprocessable();
}
}
@@ -5,7 +5,10 @@ namespace Tests\Feature;
use App\Models\Opportunity;
use App\Models\User;
use App\Services\OpportunityCanonicalContract;
use App\Services\OpportunityCanonicalResponse;
use App\Services\OpportunityCategoryFields;
use App\Services\OpportunityManagement;
use App\Services\OpportunityModularProfiles;
use App\Services\PortalProfileAdministration;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Crypt;
@@ -81,6 +84,69 @@ class OpportunityCanonicalWorkflowTest extends TestCase
$this->assertNotEmpty($schema['$defs']['businessTransactionCreate']['allOf']);
}
public function test_read_response_preserves_ids_resources_permissions_and_does_not_invent_validation(): void
{
[$id, $promoter, $validator, , $payload, $document] = $this->fixture();
$url = '/minhas-oportunidades/'.$id.'/response';
$record = Opportunity::findOrFail($id);
$before = $record->getRawOriginal();
$response = $this->loginFixture($promoter)->getJson($url)->assertOk()
->assertHeader('Cache-Control', 'no-store, private')->json();
$this->assertSame(OpportunityCanonicalResponse::VERSION, $response['adapter_version']);
$this->assertSame($id, $response['data']['id']);
$this->assertSame($record->reference, $response['data']['opportunity_code']);
$this->assertSame($document, $response['data']['documents'][0]['id']);
$this->assertSame($payload['locations'], $response['resource_references']['locations']);
$this->assertNull($response['data']['approved_by']);
$this->assertFalse($response['compatibility']['normative_response_complete']);
$this->assertFalse($response['validation_result']['revalidated_on_read']);
$this->assertArrayNotHasKey('storage_key', $response['data']['documents'][0]);
$this->assertArrayNotHasKey('completion_percentage', $response['data']);
$this->assertSame($before, $record->fresh()->getRawOriginal());
$this->loginFixture($validator)->getJson($url)->assertOk();
$otherOrg = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $otherOrg, 'name' => 'Other synthetic QA organization', 'organization_type' => 'private']);
$other = $this->actor('promoter', $otherOrg);
$this->loginFixture($other)->getJson($url)->assertForbidden();
$this->action($promoter, $id, 'submit');
$submitted = $this->loginFixture($promoter)->getJson($url)->assertOk()->json();
$this->assertSame($promoter->id, $submitted['data']['submitted_by']);
$this->assertNotNull($submitted['data']['submitted_at']);
$this->assertNull($submitted['data']['approved_by']);
$this->assertNotNull($submitted['validation_result']['submission_hash']);
$this->assertEquals($payload['real_estate'], $submitted['data']['real_estate']);
$this->assertArrayNotHasKey('id', $submitted['data']['real_estate']);
}
public function test_cadastral_uniqueness_and_original_succession_modality_are_enforced_without_filling_drafts(): void
{
$fields = app(OpportunityCategoryFields::class);
$record = new Opportunity;
$record->id = 1;
$payload = ['opportunity_category' => 'land_rights', 'land_rights' => ['duat_status' => 'none',
'operation_type' => 'investment_with_duat', 'exploitation_plan_required' => false, 'communities_affected' => false,
'parcels' => [['cadastral_number' => 'QA-001'], ['cadastral_number' => 'QA-001']]]];
$errors = [];
$fields->validate($payload, $record, [], $errors);
$this->assertSame(['canonical.land_rights.parcels.1.cadastral_number'], array_keys($errors));
foreach ([[null, null], ['QA-001', 'QA-002']] as $numbers) {
$payload['land_rights']['parcels'] = array_map(fn ($number) => ['cadastral_number' => $number], $numbers);
$errors = [];
$fields->validate($payload, $record, [], $errors);
$this->assertSame([], $errors);
}
$business = ['transaction_type' => 'succession', 'available_financial_years' => 1,
'financial_periods' => [['year' => (int) date('Y') - 1]], 'litigation_contingencies' => [['type' => 'none']],
'confidentiality_level' => 'internal', 'valuation' => ['value' => 100, 'date' => today()->toDateString()]];
$errors = [];
$fields->validate(['opportunity_category' => 'business_sale', 'currency' => 'MZN', 'visibility' => 'internal', 'business_transaction' => $business], $record, [], $errors);
$this->assertSame([], $errors);
$errors = [];
$fields->validate(['opportunity_category' => 'business_partnership', 'currency' => 'MZN', 'visibility' => 'internal', 'business_transaction' => $business], $record, [], $errors);
$this->assertArrayHasKey('canonical.business_transaction.transaction_type', $errors);
$this->assertSame(['land_rights' => ['parcels' => [['cadastral_number' => null]]]], app(OpportunityCanonicalContract::class)->draft(['land_rights' => ['parcels' => [['cadastral_number' => null]]]]));
}
public function test_correction_approval_publication_material_revision_and_suspension_preserve_authorization(): void
{
[$id, $promoter, $validator, $publisher, $payload, $document] = $this->fixture();
@@ -204,8 +270,129 @@ class OpportunityCanonicalWorkflowTest extends TestCase
$this->action($validator, $id, 'approve', [], 422);
}
public function test_qa_modular_paths_use_distinct_semantics_and_existing_independent_workflow(): void
{
config(['portal.opportunity_modular_profiles_qa_enabled' => true]);
foreach (['concession' => ['intention_call', 'awarded_right'], 'financing' => ['seeking_funding', 'funding_offer'],
'supply' => ['purchase_request', 'supply_offer'], 'other' => ['classified_other']] as $category => $modes) {
foreach ($modes as $mode) {
[$id, $promoter, $validator, $publisher, $payload, $document] = $this->fixture($category, $mode);
$this->assertEquals($payload, app(OpportunityManagement::class)->values(Opportunity::findOrFail($id))['canonical']);
if ($category === 'other') {
$this->action($promoter, $id, 'submit', [], 422);
config(['portal.opportunity_other_qa_subtypes.qa-example' => ['approved' => true, 'version' => 'synthetic-acceptance-v1',
'decision_reference' => 'Synthetic policy test only; not a real approved business subtype', 'gates' => ['technical_review', 'legal_territorial_review']]]);
}
$this->action($promoter, $id, 'submit');
$this->action($validator, $id, 'start_review');
$this->action($validator, $id, 'request_correction', ['correction_items' => ['canonical.executive_summary']]);
$payload['executive_summary'] .= ' Correção QA recuperável.';
$this->loginFixture($promoter)->putJson('/minhas-oportunidades/'.$id.'/draft', ['etag' => app(OpportunityManagement::class)->etag(Opportunity::findOrFail($id)), 'canonical' => $payload, 'reason' => self::REASON])->assertOk();
$this->action($promoter, $id, 'submit');
foreach (app(OpportunityCanonicalContract::class)->gates(Opportunity::findOrFail($id)) as $gate) {
$this->action($validator, $id, 'review_gate', ['gate' => $gate, 'gate_decision' => 'approved', 'evidence_document_ids' => [$document]]);
}
$this->action($validator, $id, 'approve');
$this->action($publisher, $id, 'publish');
$response = $this->loginFixture($promoter)->getJson('/minhas-oportunidades/'.$id.'/response')->assertOk()->json();
$this->assertSame($mode, $response['data']['module_details']['mode']);
$this->assertSame('restricted', $response['publication']['scope']);
$this->assertSame(0, Opportunity::published()->whereKey($id)->count());
$this->assertLessThanOrEqual(500, mb_strlen(Opportunity::findOrFail($id)->summary));
$this->assertSame($payload['executive_summary'], $response['data']['executive_summary']);
$this->assertNotEmpty(Opportunity::findOrFail($id)->workflow_context['approval']['gates']);
if ($mode === 'intention_call') {
$this->assertStringContainsString('não é direito atribuído', Opportunity::findOrFail($id)->summary);
}
}
}
}
public function test_modular_semantic_failures_are_field_local_and_never_block_incomplete_draft_recovery(): void
{
config(['portal.opportunity_modular_profiles_qa_enabled' => true]);
foreach ([['financing', 'seeking_funding', 'funding_sought', 999999],
['financing', 'funding_offer', 'total_investment', 100],
['concession', 'awarded_right', 'module_details.external_decision_document_id', null],
['supply', 'supply_offer', 'module_details.total_price', 99]] as [$category, $mode, $path, $value]) {
[$id, $promoter, , , $payload] = $this->fixture($category, $mode);
data_set($payload, $path, $value);
$this->loginFixture($promoter)->putJson('/minhas-oportunidades/'.$id.'/draft', ['etag' => app(OpportunityManagement::class)->etag(Opportunity::findOrFail($id)), 'canonical' => $payload, 'reason' => self::REASON])->assertOk();
$this->getJson('/minhas-oportunidades/'.$id.'/response')->assertOk()->assertJsonPath('data.module_details.mode', $mode);
$this->assertEquals($payload, app(OpportunityManagement::class)->values(Opportunity::findOrFail($id))['canonical']);
$this->postJson('/minhas-oportunidades/'.$id.'/workflow', ['etag' => app(OpportunityManagement::class)->etag(Opportunity::findOrFail($id)), 'action' => 'submit', 'reason' => self::REASON])->assertUnprocessable()->assertJsonValidationErrors('canonical.'.$path);
}
$service = app(OpportunityCanonicalContract::class);
$this->assertSame(['module_details' => ['mode' => 'funding_offer']], $service->draft(['module_details' => ['mode' => 'funding_offer']]));
config(['portal.opportunity_modular_profiles_qa_enabled' => false]);
$this->assertFalse(app(OpportunityModularProfiles::class)->enabled());
$this->assertArrayNotHasKey('module_details', $service->schema(true)['$defs']['opportunityCreate']['properties']);
}
public function test_new_entity_history_is_opt_in_and_does_not_fabricate_accounts(): void
{
config(['portal.opportunity_modular_profiles_qa_enabled' => true]);
$schema = app(OpportunityCanonicalContract::class)->schema();
$this->assertSame(0, $schema['$defs']['businessTransactionCreate']['properties']['available_financial_years']['minimum']);
$bt = ['transaction_type' => 'succession', 'available_financial_years' => 0, 'financial_periods' => [],
'history_status' => 'no_completed_years', 'history_declaration' => 'Entidade sintética recém-criada sem exercícios completos.',
'history_declaration_document_id' => 'qa-doc', 'projections' => ['scenario' => 'Projeção QA', 'source' => 'Plano QA', 'date' => '2026-10-10', 'document_id' => 'qa-doc'],
'litigation_contingencies' => [['type' => 'none']], 'confidentiality_level' => 'internal', 'valuation' => ['value' => 100, 'date' => '2026-10-10']];
$p = ['opportunity_category' => 'business_sale', 'module_profile_version' => OpportunityModularProfiles::HISTORY_VERSION, 'business_transaction' => $bt, 'visibility' => 'internal', 'currency' => 'MZN'];
$errors = [];
app(OpportunityCategoryFields::class)->validate($p, new Opportunity, ['documents' => ['qa-doc' => []]], $errors);
$this->assertSame([], $errors);
unset($p['module_profile_version']);
$errors = [];
app(OpportunityCategoryFields::class)->validate($p, new Opportunity, ['documents' => ['qa-doc' => []]], $errors);
$this->assertArrayHasKey('canonical.module_profile_version', $errors);
}
/** @param array<string,mixed> $payload
* @return array<string,mixed> */
private function modularPayload(string $category, string $mode, array $payload, string $document): array
{
unset($payload['real_estate']);
$payload['opportunity_category'] = $category;
$payload['module_profile_version'] = OpportunityModularProfiles::PROFILES[$category];
$m = ['mode' => $mode, 'object' => 'Objeto sintético QA', 'representation_document_id' => $document, 'object_document_id' => $document];
if ($category === 'concession') {
$m += ['granting_entity' => 'Entidade sintética, não autoridade real', 'procedure_regime' => 'Procedimento sintético QA', 'scope' => 'Distrito QA',
'term_value' => 12, 'term_unit' => 'months', 'remuneration_obligations' => 'Obrigações sintéticas QA', 'participation_conditions' => 'Condições sintéticas QA',
'land_required' => false, 'construction_required' => false, 'disclosure_authorization_document_id' => $document];
if ($mode === 'awarded_right') {
$m['external_decision_document_id'] = $document;
}
}
if (in_array($category, ['financing', 'supply'], true)) {
$m += ['conditions_status' => 'proposed', 'terms' => 'Condições indicativas QA, sem compromisso', 'scenario' => 'Cenário QA', 'source' => 'Fonte sintética QA', 'terms_date' => '2026-10-10'];
}
if ($category === 'financing') {
$m += ['instrument' => 'debt', 'term_value' => 12, 'term_unit' => 'months', 'repayment_periodicity' => 'mensal', 'disbursement_calendar' => 'Calendário proposto QA'];
if ($mode === 'seeking_funding') {
$payload += ['total_investment' => 1000, 'investment_breakdown' => [['label' => 'QA', 'amount' => 1000, 'currency' => 'MZN']], 'secured_financing' => [], 'funding_sought' => 500];
$m += ['use_of_funds' => 'Preparação QA', 'financial_plan_document_id' => $document];
} else {
$m += ['offered_capacity' => 100000, 'eligibility_criteria' => 'Critérios propostos QA', 'capacity_document_id' => $document];
}
}
if ($category === 'supply') {
$m += ['quantity' => 10, 'unit' => 'unidade', 'destination' => 'Distrito QA', 'start_date' => '2026-10-11', 'end_date' => '2026-11-11',
'acceptance_criteria' => 'Critérios QA', 'unit_price' => 10, 'total_price' => 100, 'advance_or_guarantee' => false];
if ($mode === 'supply_offer') {
$m['capacity_document_id'] = $document;
}
}
if ($category === 'other') {
$m += ['subtype' => 'qa-example', 'purpose' => 'Ensaio QA', 'recipients' => 'Destinatários sintéticos', 'expected_output' => 'Resultado sintético', 'risks' => 'Riscos declarados QA'];
}
$payload['module_details'] = $m;
return $payload;
}
/** @return array{int,User,User,User,array<string,mixed>,string} */
private function fixture(): array
private function fixture(string $category = 'real_estate', string $mode = ''): array
{
$org = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $org, 'name' => 'Synthetic QA organization', 'organization_type' => 'private']);
@@ -214,11 +401,11 @@ class OpportunityCanonicalWorkflowTest extends TestCase
$validator = $this->actor('validator', $org, ['technical_reviewer', 'legal_reviewer', 'validation_coordinator']);
$publisher = $this->actor('publisher', $org, ['publication_coordinator']);
$version = (string) Str::uuid();
DB::table('reference_catalog_versions')->insert(['id' => $version, 'catalog_code' => 'qa-only', 'version_no' => 1, 'source_system' => 'synthetic-policy-fixture', 'source_version' => 'qa', 'source_reference' => 'Not official', 'status' => 'published']);
DB::table('reference_catalog_versions')->insert(['id' => $version, 'catalog_code' => 'qa-only-'.$org, 'version_no' => 1, 'source_system' => 'synthetic-policy-fixture', 'source_version' => 'qa', 'source_reference' => 'Not official', 'status' => 'published']);
$ids = [];
$parent = null;
foreach (range(1, 3) as $i) {
$sector = DB::table('sectors')->insertGetId(['name' => 'QA '.$i, 'slug' => 'qa-'.$i, 'is_active' => true]);
$sector = DB::table('sectors')->insertGetId(['name' => 'QA '.$i, 'slug' => 'qa-'.$org.'-'.$i, 'is_active' => true]);
$id = (string) Str::uuid();
DB::table('economic_activities')->insert(['id' => $id, 'parent_id' => $parent, 'sector_id' => $sector, 'catalog_version_id' => $version, 'code' => 'qa-'.$i, 'label_pt_mz' => 'QA '.$i, 'status' => 'published', 'valid_from' => today()]);
$ids[] = [$id, $sector];
@@ -229,7 +416,7 @@ class OpportunityCanonicalWorkflowTest extends TestCase
DB::table('contacts')->insert(['id' => $contact, 'full_name' => 'Synthetic QA contact']);
DB::table('investors')->insert(['id' => $investor, 'organization_id' => $org, 'legal_name' => 'Synthetic QA entity', 'investor_type' => 'institutional', 'status' => 'active']);
DB::table('investor_contacts')->insert(['investor_id' => $investor, 'contact_id' => $contact]);
$draft = $this->loginFixture($promoter)->postJson('/minhas-oportunidades/drafts', ['creation_key' => (string) Str::uuid(), 'organization_id' => $org, 'opportunity_category' => 'real_estate', 'reason' => self::REASON])->assertCreated()->json();
$draft = $this->loginFixture($promoter)->postJson('/minhas-oportunidades/drafts', ['creation_key' => (string) Str::uuid(), 'organization_id' => $org, 'opportunity_category' => $category, 'reason' => self::REASON])->assertCreated()->json();
$id = $draft['id'];
$location = (string) Str::uuid();
$provinceId = (string) Str::uuid();
@@ -250,6 +437,9 @@ class OpportunityCanonicalWorkflowTest extends TestCase
DB::table('authorities')->insert(['id' => $authority, 'catalog_version_id' => $version, 'organization_id' => $org, 'code' => 'QA-'.$id, 'authority_type' => 'qa-synthetic', 'label_pt_mz' => 'Entidade sintética QA', 'source_reference' => 'Synthetic policy fixture; not official', 'valid_from' => '2026-01-01', 'status' => 'published']);
$payload['real_estate']['utilities'] = array_map(fn ($type) => ['type' => $type, 'status' => 'existing'], ['electricity', 'water', 'sanitation', 'telecommunications', 'access']);
$payload['real_estate'] += ['title_document_id' => $document, 'title_holder_name' => 'Synthetic QA organization', 'issuing_authority_id' => $authority];
if ($category !== 'real_estate') {
$payload = $this->modularPayload($category, $mode, $payload, $document);
}
$this->loginFixture($promoter)->putJson('/minhas-oportunidades/'.$id.'/draft', ['etag' => $draft['etag'], 'canonical' => $payload, 'reason' => self::REASON])->assertOk();
return [$id, $promoter, $validator, $publisher, $payload, $document];
@@ -14,6 +14,7 @@ use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia;
use Tests\Support\CleanScannerFixture;
use Tests\TestCase;
@@ -96,6 +97,38 @@ class OpportunityResourcesTest extends TestCase
$this->assertDatabaseHas('opportunity_location_drafts', ['id' => $id, 'deleted_at' => null]);
}
public function test_foreign_country_rejects_mozambican_hierarchy_without_changing_the_saved_draft(): void
{
[$user, $o] = $this->setupDraft();
$province = (string) Str::uuid();
$district = (string) Str::uuid();
DB::table('geo_locations')->insert([
['id' => $province, 'code' => $province, 'name' => 'Synthetic QA Mozambique province', 'location_type' => 'province', 'parent_id' => null, 'data_classification' => 'public'],
['id' => $district, 'code' => $district, 'name' => 'Synthetic QA Mozambique district', 'location_type' => 'district', 'parent_id' => $province, 'data_classification' => 'public'],
]);
$payload = ['title' => 'Synthetic QA saved location', 'country_code' => 'MZ', 'province_id' => $province, 'district_id' => $district, 'role' => 'market'];
$id = $this->postJson('/management/opportunities/'.$o->id.'/resources/locations', $this->payload($o, $payload))->assertCreated()->json('id');
$before = DB::table('opportunity_location_drafts')->where('id', $id)->first();
$auditBefore = DB::table('audit_events')->where('source_system', 'opportunity-resources')->count();
$input = [...$this->payload($o->fresh(), [...$payload, 'country_code' => 'PT']), 'version' => 1];
$this->postJson('/management/opportunities/'.$o->id.'/resources/locations/'.$id, $input)
->assertUnprocessable()->assertJsonValidationErrors('country_code');
$after = DB::table('opportunity_location_drafts')->where('id', $id)->first();
$this->assertSame($before->payload, $after->payload);
$this->assertSame(1, $after->version);
$this->assertSame($auditBefore, DB::table('audit_events')->where('source_system', 'opportunity-resources')->count());
$foreign = $this->postJson('/management/opportunities/'.$o->id.'/resources/locations',
$this->payload($o->fresh(), ['title' => 'Synthetic QA foreign incomplete location', 'country_code' => 'PT', 'role' => 'market']))->assertCreated()->json('id');
$saved = json_decode(DB::table('opportunity_location_drafts')->where('id', $foreign)->value('payload'), true);
$this->assertSame('PT', $saved['country_code']);
$this->assertEmpty($saved['province_id'] ?? null);
$this->assertSame(0, DB::table('opportunity_locations')->count());
$this->get('/management/opportunities/'.$o->id.'/resources')->assertInertia(fn (AssertableInertia $page) => $page
->where('assessment', fn ($issues) => collect($issues)->every(fn ($issue) => ! str_contains($issue, 'província') && ! str_contains($issue, 'distrito'))));
}
public function test_invalid_upload_and_cross_opportunity_links_do_not_create_resources(): void
{
[$user,$o] = $this->setupDraft();
@@ -0,0 +1,53 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Session\ArraySessionHandler;
use Illuminate\Session\Store;
use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider;
use PragmaRX\Google2FA\Google2FA;
use Symfony\Component\HttpFoundation\Response;
use Tests\TestCase;
class PortalQaMfaTracePrivacyTest extends TestCase
{
use RefreshDatabase;
public function test_diagnostic_distinguishes_replay_without_consuming_code_or_authenticating(): void
{
require_once base_path('tools/portal-qa-auth-trace.php');
config(['cache.default' => 'array']);
$engine = new Google2FA;
$secret = $engine->generateSecretKey();
User::factory()->create(['id' => 18, 'email' => 'investor-mfa-qa@example.invalid', 'two_factor_secret' => encrypt($secret), 'two_factor_confirmed_at' => now()]);
$code = $engine->getCurrentOtp($secret);
$request = Request::create('/two-factor-challenge', 'POST', ['code' => $code]);
$session = new Store('qa_trace_unit', new ArraySessionHandler(120));
$session->start();
$session->put('login.id', 18);
$request->setLaravelSession($session);
$request->attributes->set('qa_auth_trace', 'unit');
$trace = new \PortalQaSessionTrace;
$trace->handle($request, fn () => new Response('controlled'));
$first = $request->attributes->get('qa_mfa_input_check');
$this->assertTrue($first['secret_decrypts']);
$this->assertTrue($first['matches_configured_time_window']);
$this->assertFalse($first['previously_used_in_cache']);
$this->assertTrue($first['accepted_by_replay_check']);
$this->assertFalse(auth()->check());
// Fortify must still accept the unused code: the diagnostic cannot consume it.
$this->assertTrue(app(TwoFactorAuthenticationProvider::class)->verify($secret, $code));
$trace->handle($request, fn () => new Response('controlled'));
$second = $request->attributes->get('qa_mfa_input_check');
$this->assertTrue($second['matches_configured_time_window']);
$this->assertTrue($second['previously_used_in_cache']);
$this->assertFalse($second['accepted_by_replay_check']);
$this->assertFalse(auth()->check());
$encoded = json_encode($second, JSON_THROW_ON_ERROR);
foreach ([$secret, $code, md5($code)] as $value) $this->assertStringNotContainsString($value, $encoded);
$this->assertSame(18, $session->get('login.id'));
}
}
+24
View File
@@ -290,6 +290,30 @@ class PortalServicesTest extends TestCase
$this->assertDatabaseCount('portal_resource_revisions', 8);
}
public function test_qa_library_preserves_an_original_without_inventing_a_topic_or_auto_publishing(): void
{
Storage::fake('local');
$manager = User::factory()->create();
config(['portal.managers' => [$manager->email], 'portal.legacy_content_qa_enabled' => false]);
$this->actingAs($manager)->post('/management/portal-services/upload', [
'file' => UploadedFile::fake()->createWithContent('original-image-reference.txt', 'Synthetic original without a thematic classification.'),
])->assertSessionHasNoErrors();
$document = DB::table('documents')->firstOrFail();
$payload = [...$this->documentResourcePayload($document->id, 'Original sem assunto'), 'topic' => '', 'category' => 'media',
'associations' => [], 'data_classification' => 'public'];
$this->post('/management/portal-services/resources', $payload)->assertSessionHasErrors('topic');
$this->assertDatabaseCount('portal_resources', 0);
config(['portal.legacy_content_qa_enabled' => true]);
$this->post('/management/portal-services/resources', $payload)->assertSessionHasNoErrors();
$resource = DB::table('portal_resources')->firstOrFail();
$this->assertSame('', $resource->topic);
$this->assertFalse($resource->published);
$this->assertDatabaseCount('portal_resource_links', 0);
$this->get('/documentos/'.$resource->id.'/download')->assertNotFound();
$this->post('/management/portal-services/resources/'.$resource->id, ['published' => true])->assertSessionHasNoErrors();
$this->get('/documentos/'.$resource->id.'/download')->assertOk();
}
public function test_manager_classifies_a_reviewed_document_public_before_separate_publication(): void
{
Storage::fake('local');
+28
View File
@@ -0,0 +1,28 @@
<?php
use App\Services\LegacyContent;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
// Read-only verification; never run migrations, installers, seeders or key generation.
require dirname(__DIR__).'/vendor/autoload.php';
$app = require dirname(__DIR__).'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
DB::statement('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
$actual = DB::selectOne('SELECT current_database() AS database, inet_server_port() AS port');
if ($actual->database !== 'investemoz' || (int) $actual->port !== 5432) {
throw new RuntimeException('Expected active read-only identity; no mutation performed.');
}
$result = ['target' => $actual, 'transaction_read_only' => DB::selectOne('SHOW transaction_read_only')->transaction_read_only,
'legacy_available' => app(LegacyContent::class)->available(),
'legacy_documents' => DB::table('documents')->where('external_id', 'like', 'legacy-tempo-%')->count(),
'installed_queried_migrations' => DB::table('migrations')->where(function ($query) {
$query->where('migration', 'like', '%030000%')->orWhere('migration', 'like', '%040000%')->orWhere('migration', 'like', '%190000%');
})->pluck('migration'),
'active_bundle_sha256' => hash_file('sha256', public_path('build/manifest.json')), 'active_writes' => 0];
$result['required_extensions_installed'] = [];
foreach (['2026_10_09_030000_extend_opportunity_resources', '2026_10_10_040000_create_portal_profile_authorization', '2026_10_09_190000_prepare_investor_path'] as $migration) {
$result['required_extensions_installed'][$migration] = DB::table('migrations')->where('migration', $migration)->exists();
}
file_put_contents(storage_path('app/private/investmoz-v2/portal-audit/legacy-active-readonly.json'), json_encode($result, JSON_PRETTY_PRINT));
echo json_encode($result);
@@ -0,0 +1,58 @@
<?php
use App\Services\VirtualDesk;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
require_once __DIR__.'/portal-qa-environment.php';
$target = portalQaEnvironment();
require dirname(__DIR__).'/vendor/autoload.php';
$app = require dirname(__DIR__).'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
portalQaAssertTarget($target);
$directory = storage_path('app/private/investmoz-v2/portal-audit');
$browser = json_decode(file_get_contents($directory.'/legacy-content-browser-qa.json'), true, 512, JSON_THROW_ON_ERROR);
if (($browser['result'] ?? null) !== 'passed' || ($browser['target'] ?? null) !== $target['database'].':55441') {
throw new RuntimeException('Completed browser proof on the same QA required.');
}
DB::beginTransaction();
try {
DB::statement('SET TRANSACTION READ ONLY');
$case = DB::table('desk_cases')->where('id', $browser['dossier_id'])->firstOrFail();
$payload = app(VirtualDesk::class)->decode($case->payload);
$references = $payload['investor_path']['content_sources'] ?? [];
$events = DB::table('desk_events')->where('case_id', $case->id)->where('action', 'legacy_content_referenced')->count();
if ((int) $case->user_id !== 21 || count($references) !== 1 || $events !== 1 || (int) $references[0]['resource_id'] !== 6) {
throw new RuntimeException('Browser association is not isolated/idempotent.');
}
$resources = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
->whereIn('r.id', [5, 6])->get(['r.id', 'r.published', 'r.metadata_version', 'd.sha256', 'd.storage_key', 'd.malware_scan_status', 'd.malware_scanned_sha256']);
$originals = [];
foreach ($resources as $row) {
$sha = trim($row->sha256);
$file = Storage::disk('local')->path($row->storage_key);
$verified = hash_equals($sha, hash_file('sha256', $file));
$duplicates = DB::table('documents')->where('sha256', $row->sha256)->whereNull('deleted_at')->count();
if (! $verified || $duplicates !== 1 || ! $row->published || $row->malware_scan_status !== 'clean'
|| ! hash_equals($sha, trim($row->malware_scanned_sha256))) {
throw new RuntimeException('Original/public resource integrity changed.');
}
$originals[] = ['resource_id' => $row->id, 'original_hash_verified' => true, 'same_bytes_as_scan' => true,
'document_count_for_hash' => $duplicates, 'metadata_version' => $row->metadata_version];
}
$http = json_decode(file_get_contents($directory.'/legacy-content-http-qa.json'), true, 512, JSON_THROW_ON_ERROR);
$independent = DB::table('desk_cases')->where('id', $http['dossier']['independent_id'])->firstOrFail();
$independentPayload = app(VirtualDesk::class)->decode($independent->payload);
if (($independentPayload['investor_path']['content_sources'] ?? []) !== []) {
throw new RuntimeException('Unexpected reference in independent project.');
}
$result = ['verified_at' => now()->toIso8601String(), 'target' => $target['database'].':55441',
'transaction_read_only' => true, 'actor_id' => 21, 'dossier_id' => $case->id, 'version' => $case->version,
'reference_count' => count($references), 'reference_event_count' => $events, 'originals' => $originals,
'independent_project_reference_count' => 0, 'active_access' => false, 'database_writes' => 0];
file_put_contents($directory.'/legacy-content-browser-persistence-qa.json', json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo json_encode($result, JSON_UNESCAPED_UNICODE);
} finally {
DB::rollBack();
}
+264
View File
@@ -0,0 +1,264 @@
/** Real Chromium render/click acceptance on the existing, isolated QA server. */
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { chromium } = require('C:/Users/macbook/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
const ROOT = path.resolve(__dirname, '..');
const PRIVATE = path.join(ROOT, 'storage/app/private/investmoz-v2/portal-audit');
const BASE = 'http://127.0.0.1:18097';
const TARGET = 'desk_restore_20261009_174726_test:55441';
const receiptPath = path.join(PRIVATE, 'legacy-content-browser-qa.json');
const actors = JSON.parse(fs.readFileSync(path.join(PRIVATE, 'profiles-v1-actors-private.json'), 'utf8'));
assert.equal(actors.promoter.id, 21, 'Use the designated synthetic Promotor QA21');
const previous = fs.existsSync(receiptPath) ? JSON.parse(fs.readFileSync(receiptPath, 'utf8')) : {};
if (previous.started_at) {
const archive = path.join(PRIVATE, 'legacy-browser-attempt-' + previous.started_at.replace(/[^0-9TZ-]/g, '-') + '.json');
if (!fs.existsSync(archive)) fs.copyFileSync(receiptPath, archive);
}
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
const screenshotDir = path.join(PRIVATE, 'legacy-browser-' + stamp);
fs.mkdirSync(screenshotDir, { recursive: true });
const proof = { started_at: new Date().toISOString(), base: BASE, target: TARGET,
browser: 'installed Chromium 1243, isolated headless contexts', native_control: 'pipe unavailable; two probes',
interface_observed: true, active_writes: 0, checks: {}, events: [], screenshots: [],
dossier_id: previous.dossier_id || null, continuation_id: previous.continuation_id || null,
title: previous.title || 'QA visual — ideia de agronegócio ' + stamp, last_step: 'initialization' };
let browser;
function save() { fs.writeFileSync(receiptPath, JSON.stringify(proof, null, 2)); }
function step(name) { proof.last_step = name; save(); console.log(JSON.stringify({ step: name })); }
function totp(secret) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let bits = 0, value = 0; const bytes = [];
for (const char of secret.toUpperCase().replace(/=+$/, '')) {
const digit = alphabet.indexOf(char); assert(digit >= 0);
value = (value << 5) | digit; bits += 5;
if (bits >= 8) { bytes.push((value >>> (bits - 8)) & 255); bits -= 8; }
}
const counter = Buffer.alloc(8); counter.writeBigUInt64BE(BigInt(Math.floor(Date.now() / 30000)));
const digest = crypto.createHmac('sha1', Buffer.from(bytes)).update(counter).digest();
const offset = digest[19] & 15;
return String((digest.readUInt32BE(offset) & 0x7fffffff) % 1000000).padStart(6, '0');
}
async function context(viewport = { width: 1440, height: 1000 }) {
const ctx = await browser.newContext({ viewport, locale: 'pt-MZ', reducedMotion: 'reduce' });
ctx.qaConfirmed = false;
await ctx.route('**/*', async (route) => {
const request = route.request(), url = new URL(request.url());
if (url.origin !== BASE) { proof.events.push({ external_request_blocked: url.origin }); return route.abort(); }
if (!['GET', 'HEAD'].includes(request.method())) assert(ctx.qaConfirmed, 'Confirm actual QA before any write');
await route.continue();
});
ctx.on('response', (response) => {
const request = response.request(), url = new URL(response.url());
if (url.origin === BASE && !url.pathname.startsWith('/build-qa-')) {
const headers = response.headers();
proof.events.push({ method: request.method(), path: url.pathname, status: response.status(),
target: headers['x-investmoz-qa-target'] || null,
redirect_path: headers.location ? new URL(headers.location, BASE).pathname : null });
}
});
return ctx;
}
async function open(page, route) {
const response = await page.goto(BASE + route, { waitUntil: 'networkidle' });
assert.equal(response.headers()['x-investmoz-qa-target'], TARGET);
page.context().qaConfirmed = true;
return response;
}
async function screenshot(page, name, fullPage = true) {
const file = path.join(screenshotDir, name + '.png');
await page.screenshot({ path: file, fullPage });
proof.screenshots.push({ name, path: path.relative(ROOT, file).replaceAll('\\', '/') });
}
async function login(page, role) {
const account = actors[role];
if (!page.url().startsWith(BASE + '/login')) await open(page, '/login');
await page.locator('#email').fill(account.email);
await page.locator('#password').fill(account.password);
await page.locator('[data-test="login-button"]').click();
await page.waitForURL((url) => url.pathname === '/two-factor-challenge', { timeout: 15000 });
assert(account.totp_secret, 'Normal MFA is required; never bypass it');
await page.locator('input[name="code"]').fill(totp(account.totp_secret));
await page.getByRole('button', { name: 'Continue', exact: true }).click();
await page.waitForURL((url) => !['/login', '/two-factor-challenge'].includes(url.pathname), { timeout: 15000 });
await page.waitForLoadState('networkidle');
const cookies = await page.context().cookies(BASE);
proof.checks['mfa_' + role] = { actor_id: account.id, final_path: new URL(page.url()).pathname,
cookies: cookies.map(({ name, domain, path, secure, sameSite, httpOnly }) => ({ name, domain, path, secure, sameSite, httpOnly })) };
}
async function logout(page) {
await open(page, '/dashboard');
const triggers = page.locator('button[aria-haspopup="menu"]');
const count = await triggers.count();
for (let i = count - 1; i >= 0; i--) {
await triggers.nth(i).click();
const action = page.locator('[data-test="logout-button"]');
if (await action.isVisible()) { await action.click(); await page.waitForURL(BASE + '/'); return; }
await page.keyboard.press('Escape');
}
throw new Error('Visible logout action missing on dashboard');
}
async function assertDossier(page, id) {
await page.waitForURL(BASE + '/investimentos/' + id);
assert.equal(new URL(page.url()).pathname, '/investimentos/' + id);
await page.getByRole('heading', { name: proof.title, exact: true }).waitFor();
assert.equal(await page.getByRole('heading', { name: /^Fase \d+ ·/ }).count(), 5);
assert.equal(await page.locator('article[id^="S"]').count(), 19);
assert.equal(await page.getByRole('region', { name: 'Fontes da ideia' }).locator('li').count(), 1);
await page.getByRole('heading', { name: 'Próxima acção', exact: true }).waitFor();
}
(async () => {
browser = await chromium.launch({ headless: true,
executablePath: 'C:/Users/macbook/AppData/Local/ms-playwright/chromium-1243/chrome-win64/chrome.exe' });
step('public desktop rendering');
const anonymous = await context(), page = await anonymous.newPage();
page.setDefaultTimeout(15000);
const first = await open(page, '/explorar/agronegocio'); assert.equal(first.status(), 200);
assert.equal(await page.title(), 'Explorar agronegócio - InvesteMoz QA');
proof.checks.qa_browser_title = true;
await page.getByRole('heading', { name: 'Investir no agronegócio', exact: true }).waitFor();
await page.locator('main img').waitFor();
await page.evaluate(() => document.fonts.ready);
proof.checks.desktop = await page.locator('main').evaluate((main) => ({
background: getComputedStyle(main).backgroundColor, text: getComputedStyle(main).color,
font: getComputedStyle(main).fontFamily, headingFont: getComputedStyle(main.querySelector('h1')).fontFamily,
notoSansLoaded: document.fonts.check('16px "Noto Sans"'), notoSerifLoaded: document.fonts.check('600 36px "Noto Serif"'),
photoLoaded: main.querySelector('img').complete && main.querySelector('img').naturalWidth === 900,
horizontalOverflow: document.documentElement.scrollWidth > innerWidth,
}));
assert(proof.checks.desktop.photoLoaded); assert(!proof.checks.desktop.horizontalOverflow);
await screenshot(page, 'public-landing-desktop');
step('visible search hit and miss');
await page.getByRole('searchbox').fill('corredores');
await page.getByRole('button', { name: 'Pesquisar', exact: true }).click();
await page.waitForURL(/q=corredores/); await page.getByRole('link', { name: 'Ler o artigo', exact: true }).waitFor();
await page.getByRole('searchbox').fill('zzzz-no-content-qa');
await page.getByRole('button', { name: 'Pesquisar', exact: true }).click();
await page.getByText('Nenhum conteúdo corresponde à pesquisa.', { exact: true }).waitFor();
assert.equal(await page.getByRole('link', { name: 'Ler o artigo', exact: true }).count(), 0);
await screenshot(page, 'public-search-no-result');
await page.getByRole('searchbox').fill('');
await page.getByRole('button', { name: 'Pesquisar', exact: true }).click();
await page.getByRole('link', { name: 'Ler o artigo', exact: true }).waitFor();
proof.checks.search_hit_miss_clear = true;
step('keyboard article navigation and focus');
const read = page.getByRole('link', { name: 'Ler o artigo', exact: true }); await read.focus();
await page.keyboard.press('Tab'); await page.keyboard.press('Shift+Tab');
proof.checks.focus = await read.evaluate((el) => ({ focused: el === document.activeElement,
outlineStyle: getComputedStyle(el).outlineStyle, outlineWidth: getComputedStyle(el).outlineWidth }));
await page.keyboard.press('Enter'); await page.waitForURL(BASE + '/agronegocio');
const article = page.getByRole('region', { name: 'Artigo educativo' }); await article.waitFor();
assert((await article.innerText()).length > 500);
await screenshot(page, 'public-article-desktop');
proof.checks.public_article = { characters_rendered: (await article.innerText()).length, source_visible: true };
step('consumer links through clicks');
proof.checks.consumer_links = [];
for (const name of ['Consultar os corredores cadastrados', 'Consultar localizações e mapas disponíveis', 'Exemplar na biblioteca']) {
const consumer = page.getByRole('link', { name, exact: true });
const destination = new URL(await consumer.getAttribute('href'), BASE);
await consumer.click(); await page.waitForURL(destination.href); await page.waitForLoadState('networkidle');
proof.checks.consumer_links.push({ name, path: new URL(page.url()).pathname,
rendered_heading: (await page.locator('h1').allTextContents()).filter(Boolean).length > 0 });
await page.goBack({ waitUntil: 'networkidle' }); await page.waitForURL(BASE + '/agronegocio'); await article.waitFor();
}
const opportunity = page.locator('section[aria-labelledby="opportunities-title"] a').first();
if (await opportunity.count()) {
const destination = new URL(await opportunity.getAttribute('href'), BASE);
await opportunity.click(); await page.waitForURL(destination.href); await page.waitForLoadState('networkidle');
assert(new URL(page.url()).pathname.startsWith('/oportunidades/'));
proof.checks.published_opportunity_clicked = new URL(page.url()).pathname;
await page.goBack({ waitUntil: 'networkidle' }); await article.waitFor();
}
step('mobile public rendering');
const mobileCtx = await context({ width: 390, height: 844 }), mobile = await mobileCtx.newPage();
await open(mobile, '/explorar/agronegocio'); await mobile.getByRole('heading', { name: 'Investir no agronegócio', exact: true }).waitFor();
proof.checks.mobile = await mobile.evaluate(() => ({ width: innerWidth, contentWidth: document.documentElement.scrollWidth,
photoLoaded: !!document.querySelector('main img')?.naturalWidth }));
assert(proof.checks.mobile.contentWidth <= proof.checks.mobile.width);
await screenshot(mobile, 'public-landing-mobile');
await mobile.getByRole('link', { name: 'Ler o artigo', exact: true }).click(); await mobile.getByRole('region', { name: 'Artigo educativo' }).waitFor();
assert(await mobile.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
await screenshot(mobile, 'public-article-mobile'); await mobileCtx.close();
step('public CTA, normal Promotor QA21 login and MFA');
await page.getByRole('link', { name: 'Iniciar ou continuar uma ideia', exact: true }).click();
await page.waitForURL(BASE + '/login'); await login(page, 'promoter');
assert.equal(new URL(page.url()).pathname, '/investimentos/a-partir-de/agronegocio');
await page.getByRole('heading', { name: 'Os meus investimentos', exact: true }).waitFor();
step('create and save dossier using UI');
const createdThisRun = !proof.dossier_id;
if (createdThisRun) {
await page.getByLabel('Título', { exact: true }).fill(proof.title);
await page.getByLabel('Autorizo reutilizar o meu perfil neste dossier privado.').check();
await page.getByRole('button', { name: 'Criar e guardar dossier', exact: true }).click();
await page.waitForURL(/\/investimentos\/[0-9a-f-]{36}$/);
proof.dossier_id = new URL(page.url()).pathname.split('/').pop(); save();
} else await page.getByRole('link', { name: proof.title, exact: true }).click();
await assertDossier(page, proof.dossier_id); await screenshot(page, 'private-dossier-created', false);
proof.checks.created_from_ui = true;
proof.checks.created_this_run = createdThisRun;
if (!createdThisRun) {
// Creation may be an earlier observed action, not another insert on every rerun.
const files = fs.readdirSync(PRIVATE).filter(name => name.startsWith('legacy-browser-attempt-') && name.endsWith('.json'));
const creation = files.map(name => ({ name, receipt: JSON.parse(fs.readFileSync(path.join(PRIVATE, name), 'utf8')) }))
.find(({ receipt }) => receipt.dossier_id === proof.dossier_id
&& receipt.events?.some(event => event.method === 'POST' && event.path === '/investimentos' && event.status === 302));
assert(creation, 'Existing dossier must have retained evidence of UI creation');
proof.creation_evidence = { receipt: creation.name, started_at: creation.receipt.started_at,
action: 'POST /investimentos through UI; subsequent retries recover this same dossier',
whole_attempt_result: creation.receipt.result };
}
step('source return and same dossier context');
await page.getByRole('region', { name: 'Fontes da ideia' }).locator('a').first().click(); await article.waitFor();
await page.getByRole('link', { name: 'Continuar: ' + proof.title, exact: true }).click();
await assertDossier(page, proof.dossier_id); proof.checks.private_return_context = true;
step('visible logout and new browser session');
await logout(page); await anonymous.close();
const freshCtx = await context(), fresh = await freshCtx.newPage();
await open(fresh, '/investimentos/' + proof.dossier_id); assert.equal(new URL(fresh.url()).pathname, '/login');
await login(fresh, 'promoter'); await assertDossier(fresh, proof.dossier_id);
await fresh.reload({ waitUntil: 'networkidle' }); await assertDossier(fresh, proof.dossier_id);
await screenshot(fresh, 'private-dossier-new-session', false); proof.checks.logout_new_session_reload = true;
step('continue existing dossier and repeat without duplicate');
await open(fresh, '/investimentos/a-partir-de/agronegocio');
const card = fresh.getByRole('listitem').filter({ has: fresh.getByRole('link', { name: proof.title, exact: true }) });
await card.getByRole('button', { name: 'Continuar neste dossier com o artigo', exact: true }).click();
await assertDossier(fresh, proof.dossier_id);
proof.checks.existing_dossier_replay_single_visible_reference = true;
step('anonymous context does not expose private dossier');
const publicCtx = await context(), publicPage = await publicCtx.newPage();
await open(publicPage, '/agronegocio?context=' + proof.dossier_id); await publicPage.getByRole('region', { name: 'Artigo educativo' }).waitFor();
assert.equal(await publicPage.getByText(proof.title, { exact: false }).count(), 0);
await publicPage.getByRole('link', { name: 'Iniciar ou continuar uma ideia', exact: true }).waitFor();
proof.checks.anonymous_private_context_hidden = true; await publicCtx.close();
step('other synthetic account denied and public source retained');
const otherCtx = await context(), other = await otherCtx.newPage();
await open(other, '/login'); await login(other, 'outsider');
const denied = await open(other, '/investimentos/' + proof.dossier_id); assert.equal(denied.status(), 403);
await open(other, '/agronegocio?context=' + proof.dossier_id); await other.getByRole('region', { name: 'Artigo educativo' }).waitFor();
assert.equal(await other.getByText(proof.title, { exact: false }).count(), 0);
proof.checks.other_account_denied = { actor_id: actors.outsider.id, dossier_status: 403, public_article_visible: true };
await otherCtx.close(); await logout(fresh); await freshCtx.close();
proof.result = 'passed'; proof.completed_at = new Date().toISOString(); save();
console.log(JSON.stringify({ result: proof.result, actor_id: 21, dossier_id: proof.dossier_id,
interface_observed: true, checks: Object.keys(proof.checks), screenshots: proof.screenshots.length, active_writes: 0 }));
})().catch(async (error) => {
let message = error.message;
for (const account of Object.values(actors)) {
for (const key of ['password', 'totp_secret', 'secret']) {
if (typeof account[key] === 'string' && account[key]) message = message.replaceAll(account[key], '[REDACTED]');
}
}
message = message.replace(/\b\d{6}\b/g, '[MFA OMITIDO]');
proof.result = 'failed'; proof.failure = { step: proof.last_step, type: error.name,
message: message.slice(0, 2500), pages: [] };
if (browser) for (const ctx of browser.contexts()) for (const page of ctx.pages()) {
proof.failure.pages.push({ path: new URL(page.url()).pathname,
headings: await page.locator('h1').allTextContents().catch(() => []),
error_count: await page.getByRole('alert').count().catch(() => 0) });
}
save();
console.error(JSON.stringify({ step: proof.failure.step, type: proof.failure.type, message: 'Sanitized diagnostic saved privately' })); process.exitCode = 1;
}).finally(async () => { if (browser) await browser.close(); });
+100
View File
@@ -0,0 +1,100 @@
"""Inventory the public legacy graph. Never emit HTML, cookies or CSRF values."""
import concurrent.futures
import hashlib
import html
import json
import re
import time
import urllib.parse
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / 'storage/app/private/investmoz-v2/portal-audit/legacy-discovery'
OUT.mkdir(parents=True, exist_ok=True)
BASE = 'https://investemoz.tempo.co.mz'
HEADERS = {'User-Agent': 'Mozilla/5.0 InvestMoz-QA-content-inventory', 'Accept': 'text/html,application/json,image/*,*/*;q=0.8'}
def fetch(url):
record = {'url': url, 'captured_at_utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}
try:
with urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS), timeout=35) as response:
data = response.read(20 * 1024 * 1024 + 1)
if len(data) > 20 * 1024 * 1024:
raise ValueError('Inventory per-file size limit exceeded')
mime = response.headers.get_content_type()
record.update(status=response.status, final_url=response.url, media_type=mime, bytes=len(data), sha256=hashlib.sha256(data).hexdigest())
suffix = Path(urllib.parse.urlsplit(url).path).suffix or ('.html' if mime == 'text/html' else '.bin')
path = OUT / (record['sha256'] + suffix)
path.write_bytes(data)
record['private_file'] = str(path.relative_to(ROOT)).replace('\\', '/')
if mime == 'text/html':
text = data.decode('utf-8', errors='replace')
match = re.search(r'data-page="([^"]+)"', text)
if match:
page = json.loads(html.unescape(match.group(1)))
# Only public application content, not shared authentication/session props.
record['component'] = page.get('component')
public = {k: v for k, v in page.get('props', {}).items() if k not in ['auth', 'errors', 'flash', 'csrf_token', 'ziggy', 'csrf']}
record['public_props'] = public
title = re.search(r'<title[^>]*>(.*?)</title>', text, re.S)
record['title'] = html.unescape(title.group(1)) if title else None
record['references'] = sorted(set(html.unescape(x) for x in re.findall(r'(?:href|src)=[\"\']([^\"\']+)', text)))
elif mime in ['text/css', 'application/javascript', 'text/javascript']:
text = data.decode('utf-8', errors='replace')
record['references'] = sorted(set(re.findall(r'(?:[\"\'])(/(?:images|storage|uploads|build)/[^\"\'\s]+)[\"\']', text)))
except Exception as error:
record.update(status=getattr(error, 'code', None), error=type(error).__name__)
return record
def urls(value):
if isinstance(value, dict):
for item in value.values():
yield from urls(item)
elif isinstance(value, list):
for item in value:
yield from urls(item)
elif isinstance(value, str):
for match in re.findall(r'(?:https?://[^\s<>"\']+|/(?:storage|images|uploads|build)/[^\s<>"\']+)', html.unescape(value)):
yield match
yield from re.findall(r'(?:href|src)=[\"\']([^\"\']+)', html.unescape(value))
pending = {BASE + '/', BASE + '/portal-do-investidor', BASE + '/agronegocio', BASE + '/robots.txt', BASE + '/sitemap.xml'}
records = {}
for depth in range(5):
batch = sorted(pending - records.keys())
if not batch:
break
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as pool:
for record in pool.map(fetch, batch):
records[record['url']] = record
pending = set()
for record in records.values():
refs = list(record.get('references', [])) + list(urls(record.get('public_props', {})))
props = record.get('public_props', {})
# The legacy exposes public articles as slugs, not necessarily HTML links in the SPA shell.
def slugs(value):
if isinstance(value, dict):
if isinstance(value.get('slug'), str):
yield '/' + value['slug'].lstrip('/')
for item in value.values():
yield from slugs(item)
elif isinstance(value, list):
for item in value:
yield from slugs(item)
refs += list(slugs(props))
for ref in refs:
url = urllib.parse.urljoin(record['url'], ref)
parsed = urllib.parse.urlsplit(url)
is_media = parsed.netloc == 'contentmanager.tempo.co.mz' and parsed.path.startswith('/wp-content/uploads/')
if (parsed.netloc != 'investemoz.tempo.co.mz' and not is_media) or parsed.query or parsed.fragment or re.search(r'/(?:login|register|logout|admin|dashboard|search|pesquisa|api)(?:/|$)', parsed.path):
continue
pending.add(url)
print(json.dumps({'depth': depth, 'inventoried': len(records), 'remaining_discovered': len(pending - records.keys())}), flush=True)
inventory = {'source': BASE, 'captured_at_utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), 'records': list(records.values()), 'unfetched_discovered': sorted(pending - records.keys()), 'note': 'Accessible anonymous graph; query/fragment and authentication endpoints excluded. Raw snapshots remain private and are never served.'}
(OUT / 'inventory.json').write_text(json.dumps(inventory, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({'records': len(records), 'ok': sum(r.get('status') == 200 for r in records.values()), 'html': sum(r.get('media_type') == 'text/html' for r in records.values()), 'pending': len(inventory['unfetched_discovered'])}))
+87
View File
@@ -0,0 +1,87 @@
"""Public reading and authenticated library release/dossier continuity on the guarded QA server."""
import hashlib
import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
from portal_profiles_http_session import BASE, EXPECTED, PRIVATE, Session, actors, events
receipt = json.loads((PRIVATE / 'legacy-import-qa.json').read_text(encoding='utf-8'))
manifest = json.loads(Path('storage/app/private/document-import-staging/legacy-agronegocio/manifest.json').read_text(encoding='utf-8'))
rows = receipt['resources']
article = next(r for r in rows if r['file_name'] == 'agronegocio-source.txt')
image = next(r for r in rows if r['file_name'] == 'Agronegocio.png')
proof = {'url': BASE, 'target': EXPECTED, 'started_at': datetime.now(timezone.utc).isoformat(), 'interface_observed': False, 'active_writes': 0}
anonymous = Session('outsider')
actors['library_manager'] = json.loads((PRIVATE / 'resource-http-actors-private.json').read_text(encoding='utf-8'))['manager']
manager = Session('library_manager').login()
for row in rows:
record = next(r for r in manifest['documents'] if r['source_url'] == row['source_url'])
if not row['published']:
anonymous.call('GET', f"/documentos/{row['id']}/view", status=404, binary=True)
# Separate authenticated metadata/disclosure and publication actions; importer never publishes.
data = {k: record[k] for k in ['title', 'issuer', 'category', 'topic', 'document_type', 'source_reference', 'source_url', 'language', 'version_label', 'description']}
data.update({'document_id': row['document_id'], 'document_date': record.get('document_date'), 'metadata_version': row['metadata_version'],
'territorial_scope': 'unknown', 'source_confirmation': 'identified', 'legal_status': 'not_applicable',
'rights_status': 'authorized', 'data_classification': 'public', 'independent_review': False, 'legislation': False, 'essential': False,
'source_reference': record['source_reference'] + '; utilização e divulgação exclusivamente no piloto QA autorizadas pelo responsável em 10/10/2026; sem habilitação ativa.'})
if row['id'] == article['id']:
data['associations'] = [{'target_type': 'theme', 'target_key': 'agronegocio', 'relationship': 'supports', 'applicability': 'support', 'basis_reference': row['source_url'] + '; título e conteúdo do artigo', 'review_status': 'confirmed'},
{'target_type': 'document', 'target_key': str(image['id']), 'relationship': 'related_to', 'applicability': 'support', 'basis_reference': 'thumbnail do post 171985: ' + image['source_url'], 'review_status': 'confirmed'},
{'target_type': 'stage', 'target_key': 'S01', 'relationship': 'supports', 'applicability': 'support', 'basis_reference': 'Associação editorial do piloto QA: compreender o setor para sistematizar a ideia; não é requisito legal.', 'review_status': 'confirmed'}]
manager.call('PUT', f"/management/portal-services/resources/{row['id']}", data, status=302, binary=True)
manager.call('POST', f"/management/portal-services/resources/{row['id']}", {'published': True}, binary=True)
landing = anonymous.call('GET', '/explorar/agronegocio', inertia=True)['props']
content = anonymous.call('GET', '/agronegocio', inertia=True)['props']['article']
assert landing['legacyContentEnabled'] and content['id'] == article['id'] and content['image']
assert len(content['territorial_references']) == 6 and content['calculation_use'] is False
search_hit = anonymous.call('GET', '/explorar/agronegocio?q=corredores', inertia=True)['props']
search_miss = anonymous.call('GET', '/explorar/agronegocio?q=zzzz-no-content-qa', inertia=True)['props']
assert search_hit['match'] and not search_miss['match']
assert hashlib.sha256(anonymous.call('GET', content['image']['href'], binary=True)).hexdigest() == image['sha256'].strip()
assert hashlib.sha256(anonymous.call('GET', f"/documentos/{article['id']}/download", binary=True)).hexdigest() == article['sha256'].strip()
anonymous.call('GET', '/investimentos/a-partir-de/agronegocio', status=401, binary=True)
proof['public'] = {'resource_id': content['id'], 'image_resource_id': image['id'], 'anonymous_read': True, 'image_hash_verified': True,
'original_hash_verified': True, 'search_hit_and_miss': True, 'territorial_references': content['territorial_references'],
'published_sector_opportunities': len(content['opportunities']), 'no_opportunity_created_from_article': True}
for link in ['/carteira?kind=corridor', '/zonas', content['library_href']] + ['/oportunidades/' + o['slug'] for o in content['opportunities']]:
anonymous.call('GET', link, inertia=True)
proof['public']['consumer_links_checked'] = ['/carteira?kind=corridor', '/zonas', content['library_href']]
owner = Session('promoter').login()
choice = owner.call('GET', '/investimentos/a-partir-de/agronegocio', inertia=True)['props']
assert choice['contentSource']['id'] == article['id']
previous = PRIVATE / 'legacy-content-http-qa.json'
old = json.loads(previous.read_text(encoding='utf-8')) if previous.exists() else {}
case = old.get('dossier', {}).get('id') or owner.call('POST', '/investimentos', {'title': 'Synthetic QA — ideia agronegócio', 'client_key': str(uuid.uuid4()), 'reuse_consent': True, 'content_resource_id': article['id']}, status=201)['id']
first = owner.call('GET', '/investimentos/' + case)
assert first['content_sources'][0]['resource_id'] == article['id'] and len(first['steps']) == 19 and len(first['phases']) == 5
body = {'version': first['case']['version'], 'resource_id': article['id']}
owner.call('POST', f'/investimentos/{case}/conteudos', body)
replay = owner.call('GET', '/investimentos/' + case)
assert replay['case']['version'] == first['case']['version'] and len(replay['content_sources']) == 1
owner.call('POST', '/logout', {}, status=204)
new_session = Session('promoter').login()
recovered = new_session.call('GET', '/investimentos/' + case)
assert recovered['content_sources'] == first['content_sources']
empty = old.get('dossier', {}).get('independent_id') or new_session.call('POST', '/investimentos', {'title': 'Synthetic QA — outra ideia sem artigo', 'client_key': str(uuid.uuid4()), 'reuse_consent': True}, status=201)['id']
assert new_session.call('GET', '/investimentos/' + empty)['content_sources'] == []
continuing = old.get('dossier', {}).get('continue_id') or new_session.call('POST', '/investimentos', {'title': 'Synthetic QA — ideia existente para continuar', 'client_key': str(uuid.uuid4()), 'reuse_consent': True}, status=201)['id']
before_continue = new_session.call('GET', '/investimentos/' + continuing)
new_session.call('POST', f'/investimentos/{continuing}/conteudos', {'version': before_continue['case']['version'], 'resource_id': article['id']})
continued = Session('promoter').login().call('GET', '/investimentos/' + continuing)
assert len(continued['content_sources']) == 1 and continued['content_sources'][0]['resource_id'] == article['id']
other = Session('outsider').login()
other.call('GET', '/investimentos/' + case, status=403, binary=True)
other.call('POST', f'/investimentos/{case}/conteudos', body, status=403, binary=True)
other.call('PUT', f"/management/portal-services/resources/{article['id']}", {}, status=403, binary=True)
assert new_session.call('GET', '/agronegocio?context=' + case, inertia=True)['props']['contentContext']['id'] == case
assert other.call('GET', '/agronegocio?context=' + case, inertia=True)['props']['contentContext'] is None
assert anonymous.call('GET', '/agronegocio?context=' + case, inertia=True)['props']['contentContext'] is None
proof['public']['private_return_context_authorized_and_hidden'] = True
proof['dossier'] = {'id': case, 'independent_id': empty, 'continue_id': continuing, 'existing_context_attached_and_recovered': True, 'new_mfa_session_recovered': True, 'replay_single_reference_no_version_increment': True,
'outside_account_read_write_denied': True, 'public_source_return_href': recovered['content_sources'][0]['return_href'], 'phases': 5, 'steps': 19}
proof.update(result='passed', events=events, completed_at=datetime.now(timezone.utc).isoformat())
(PRIVATE / 'legacy-content-http-qa.json').write_text(json.dumps(proof, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({'result': proof['result'], 'public_resources': [article['id'], image['id']], 'events': len(events), 'new_session_recovery': True, 'interface_observed': False, 'active_writes': 0}))
@@ -0,0 +1,45 @@
"""Preserve uncollected references and inspect the real public visual assets."""
import csv
import hashlib
import json
import re
import sys
import urllib.parse
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'tools'))
# Import helpers without executing the discovery crawl again.
namespace = {'__file__': str(ROOT / 'tools/legacy-content-discovery.py')}
source = (ROOT / 'tools/legacy-content-discovery.py').read_text(encoding='utf-8')
exec(source[:source.index('\npending = ')], namespace)
private = ROOT / 'storage/app/private/investmoz-v2/portal-audit/legacy-discovery'
path = private / 'inventory.json'
inventory = json.loads(path.read_text(encoding='utf-8'))
records = {r['url']: r for r in inventory['records']}
visual_url = 'https://investemoz.tempo.co.mz/build/assets/welcome-DyF43GB5.js'
if visual_url not in records:
records[visual_url] = namespace['fetch'](visual_url)
for record in list(records.values()):
if record.get('media_type') in ['application/javascript', 'text/javascript']:
text = (ROOT / record['private_file']).read_text(encoding='utf-8')
for ref in re.findall(r'/(?:images|assets/logos)/[^\"\'\s]+', text):
url = urllib.parse.urljoin(record['url'], ref)
if url not in records:
records[url] = namespace['fetch'](url)
references = set()
for record in records.values():
for ref in list(record.get('references', [])) + list(namespace['urls'](record.get('public_props', {}))):
url = urllib.parse.urljoin(record['url'], ref)
if urllib.parse.urlsplit(url).scheme in ['http', 'https']:
references.add((record['url'], url))
doc = ROOT / 'docs/investmoz/v2/auditoria-portal/legado-referencias.csv'
with doc.open('w', encoding='utf-8-sig', newline='') as f:
w = csv.writer(f)
w.writerow(['Página de origem', 'Referência', 'Recolha', 'Nota'])
for origin, url in sorted(references):
w.writerow([origin, url, records.get(url, {}).get('status', 'não recolhido'), 'Referência observada; não constitui relação territorial/oportunidade ou validação jurídica.'])
inventory['records'] = list(records.values())
inventory['reference_count'] = len(references)
path.write_text(json.dumps(inventory, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({'records': len(records), 'references': len(references), 'failed': [{'url': r['url'], 'status': r.get('status')} for r in records.values() if r.get('status') != 200]}))
+71
View File
@@ -0,0 +1,71 @@
"""Reconcile the observed graph and prepare a stable library import, never publish."""
import csv
import hashlib
import json
import re
import shutil
import xml.etree.ElementTree as ET
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit/legacy-discovery'
DOCS = ROOT / 'docs/investmoz/v2/auditoria-portal'
STAGE = ROOT / 'storage/app/private/document-import-staging/legacy-agronegocio'
STAGE.mkdir(parents=True, exist_ok=True)
inventory = json.loads((PRIVATE / 'inventory.json').read_text(encoding='utf-8'))
rows = inventory['records']
article = next(r for r in rows if r['url'] == 'https://investemoz.tempo.co.mz/agronegocio')
post, seo = article['public_props']['post'], article['public_props']['seo']
image = next(r for r in rows if r['url'] == post['thumbnail'])
assert article['status'] == image['status'] == 200 and image['media_type'] == 'image/png'
content = {'format': 'investmoz-legacy-article-v1', 'source_url': article['url'], 'legacy_id': post['id'],
'slug': post['slug'], 'title': post['title'], 'author': post['author'], 'legacy_category': post['category'],
'published_at': seo['publishedTime'], 'modified_at': seo['modifiedTime'], 'content_html': post['content'],
'image_source_url': image['url'], 'image_sha256': image['sha256'],
'references': re.findall(r'href=[\"\']([^\"\']+)', post['content'])}
(STAGE / 'agronegocio-source.txt').write_text(json.dumps(content, ensure_ascii=False, indent=2), encoding='utf-8')
shutil.copyfile(ROOT / image['private_file'], STAGE / 'Agronegocio.png')
manifest = {'documents': [
{'external_id': 'legacy-tempo-post-171985-20260420', 'relative_path': 'legacy-agronegocio/agronegocio-source.txt',
'file_name': 'agronegocio-source.txt', 'title': 'Agronegocio', 'issuer': post['author'],
'document_date': seo['publishedTime'][:10], 'version_label': 'Legado modificado ' + seo['modifiedTime'],
'source_url': article['url'], 'source_reference': 'Tempo; post 171985; categoria original Oportunidades; artigo educativo. Publicado ' + seo['publishedTime'] + '; modificado ' + seo['modifiedTime'],
'category': 'education', 'topic': 'agronegocio', 'document_type': 'guide', 'language': 'pt',
'description': 'Corredores de desenvolvimento do agronegócio em Moçambique. Conteúdo editorial do legado; não constitui uma oportunidade concreta nem decisão externa.'},
{'external_id': 'legacy-tempo-agronegocio-image-' + image['sha256'][:16], 'relative_path': 'legacy-agronegocio/Agronegocio.png',
'file_name': 'Agronegocio.png', 'title': 'Imagem Agronegocio — legado Tempo', 'issuer': 'Tempo (alojamento; autoria individual não indicada)',
'version_label': image['sha256'][:32], 'source_url': image['url'], 'source_reference': 'Imagem ligada ao post 171985. Autoria individual e data de criação não indicadas; não inferidas.',
'category': 'media', 'topic': 'agronegocio', 'document_type': 'reference_data', 'language': 'pt',
'description': 'Imagem original do artigo Agronegocio; manter o original sem alterações.'}
]}
(STAGE / 'manifest.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding='utf-8')
with (DOCS / 'legado-inventario.csv').open('w', encoding='utf-8-sig', newline='') as f:
fields = ['URL', 'HTTP', 'Tipo', 'Título', 'ID legado', 'Autor', 'Categoria legado', 'Publicado', 'Modificado', 'Bytes', 'SHA256', 'Destino proposto', 'Situação']
w = csv.DictWriter(f, fieldnames=fields)
w.writeheader()
for r in rows:
p = r.get('public_props', {}).get('post', {})
s = r.get('public_props', {}).get('seo', {})
w.writerow(dict(zip(fields, [r['url'], r.get('status'), r.get('media_type'), p.get('title', r.get('title')), p.get('id'), p.get('author'), p.get('category'), s.get('publishedTime'), s.get('modifiedTime'), r.get('bytes'), r.get('sha256'), '/agronegocio' if r['url'] == article['url'] else 'biblioteca / correspondência por rever', 'piloto preparado' if r['url'] in [article['url'], image['url']] else 'inventariado; não importado'])))
posts = {}
for r in rows:
p = r.get('public_props', {}).get('post')
if p:
s = r.get('public_props', {}).get('seo', {})
key = str(p['id']) + '|' + str(s.get('modifiedTime')) + '|' + hashlib.sha256(p.get('content', '').encode()).hexdigest()
posts.setdefault(key, {'post': p, 'seo': s, 'urls': []})['urls'].append(r['url'])
with (DOCS / 'legado-conteudos-reconciliados.csv').open('w', encoding='utf-8-sig', newline='') as f:
w = csv.writer(f)
w.writerow(['ID legado', 'Título', 'Autor', 'Categoria', 'Publicado', 'Modificado', 'SHA256 conteúdo', 'URLs observadas', 'Recurso QA', 'Situação'])
for key, item in posts.items():
p, s = item['post'], item['seo']
w.writerow([p['id'], p['title'], p.get('author'), p.get('category'), s.get('publishedTime'), s.get('modifiedTime'), key.rsplit('|', 1)[1], ' | '.join(item['urls']), '6' if p['id'] == 171985 else '', 'recurso único QA; não é oportunidade' if p['id'] == 171985 else 'inventariado; cadastro/versão por reconciliar'])
sitemap = next(r for r in rows if r['url'].endswith('/sitemap.xml'))
tree = ET.fromstring((ROOT / sitemap['private_file']).read_bytes())
ns = {'s': 'http://www.sitemaps.org/schemas/sitemap/0.9'}
with (DOCS / 'legado-sitemap-anunciado.csv').open('w', encoding='utf-8-sig', newline='') as f:
w = csv.writer(f)
w.writerow(['URL anunciada', 'Última alteração anunciada', 'Situação'])
for item in tree.findall('s:url', ns):
w.writerow([item.findtext('s:loc', namespaces=ns), item.findtext('s:lastmod', namespaces=ns), 'anunciado no sitemap; domínio infopublica; conteúdo não recolhido'])
print(json.dumps({'graph_records': len(rows), 'reconciled_observed_post_versions': len(posts), 'sitemap_announced': len(tree.findall('s:url', ns)), 'pilot_documents': 2, 'article_content_sha256': hashlib.sha256(post['content'].encode()).hexdigest(), 'original_image_sha256': image['sha256']}))
+40
View File
@@ -0,0 +1,40 @@
<?php
use App\Services\VirtualDesk;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
require_once __DIR__.'/portal-qa-environment.php';
$target = portalQaEnvironment();
require dirname(__DIR__).'/vendor/autoload.php';
$app = require dirname(__DIR__).'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
portalQaAssertTarget($target);
$private = storage_path('app/private/investmoz-v2/portal-audit');
$receipt = json_decode(file_get_contents($private.'/legacy-content-http-qa.json'), true, 512, JSON_THROW_ON_ERROR);
$ids = [$receipt['public']['resource_id'], $receipt['public']['image_resource_id']];
$rows = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
->whereIn('r.id', $ids)->get(['r.id', 'r.source_url', 'r.published', 'r.metadata_version', 'r.rights_status',
'd.id as document_id', 'd.external_id', 'd.media_type', 'd.sha256', 'd.malware_scan_status', 'd.malware_scanned_sha256',
'd.malware_engine', 'd.malware_engine_version', 'd.malware_scanned_at', 'd.data_classification', 'd.storage_key']);
$result = ['target' => $target, 'resources' => [], 'dossiers' => [], 'active_writes' => 0, 'interface_observed' => false];
foreach ($rows as $row) {
$path = Storage::disk('local')->path($row->storage_key);
unset($row->storage_key);
$row->original_hash_verified = hash_equals(trim($row->sha256), hash_file('sha256', $path));
$row->duplicate_document_count = DB::table('documents')->where('sha256', $row->sha256)->whereNull('deleted_at')->count();
$row->audit_actions = DB::table('audit_events')->where('entity_type', 'portal_resources')->whereRaw("after_data->>'record_id' = ?", [(string) $row->id])->pluck('action');
$result['resources'][] = $row;
}
foreach (['id', 'independent_id', 'continue_id'] as $key) {
$case = DB::table('desk_cases')->where('id', $receipt['dossier'][$key])->firstOrFail();
$payload = app(VirtualDesk::class)->decode($case->payload);
$result['dossiers'][$key] = ['id' => $case->id, 'user_id' => $case->user_id, 'version' => $case->version,
'reference_count' => count($payload['investor_path']['content_sources'] ?? []),
'event_count' => DB::table('desk_events')->where('case_id', $case->id)->where('action', 'legacy_content_referenced')->count()];
}
$result['totals'] = ['opportunities' => DB::table('opportunities')->count(),
'documents' => DB::table('documents')->count(), 'library_resources' => DB::table('portal_resources')->count()];
file_put_contents($private.'/legacy-persistence-qa.json', json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo json_encode($result, JSON_UNESCAPED_UNICODE);
+34
View File
@@ -0,0 +1,34 @@
<?php
use App\Models\User;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
// Rehearsal of the existing library importer only. Publication remains an authenticated action.
require_once __DIR__.'/portal-qa-environment.php';
$target = portalQaEnvironment();
require dirname(__DIR__).'/vendor/autoload.php';
$app = require dirname(__DIR__).'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
portalQaAssertTarget($target);
$private = storage_path('app/private/investmoz-v2/portal-audit');
$actors = json_decode(file_get_contents($private.'/resource-http-actors-private.json'), true, 512, JSON_THROW_ON_ERROR);
$actor = User::findOrFail($actors['manager']['id']);
config(['portal.managers' => ['opportunity-manager@example.invalid', 'geo-manager-qa@example.invalid']]);
if (($argv[1] ?? '') === 'apply') {
// Guard above precedes every write. Existing importer previews before any application.
Artisan::call('documents:import-batch', ['manifest' => 'legacy-agronegocio/manifest.json']);
$preview = Artisan::output();
$exit = Artisan::call('documents:import-batch', ['manifest' => 'legacy-agronegocio/manifest.json', '--apply' => true, '--actor' => (string) $actor->id]);
file_put_contents($private.'/legacy-import-preview-qa.txt', $preview.Artisan::output());
if ($exit !== 0) {
throw new RuntimeException('Legacy QA library import failed; inspect private preview.');
}
}
$resources = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
->whereIn('d.external_id', ['legacy-tempo-post-171985-20260420', 'legacy-tempo-agronegocio-image-'.substr(hash_file('sha256', storage_path('app/private/document-import-staging/legacy-agronegocio/Agronegocio.png')), 0, 16)])
->get(['r.id', 'r.document_id', 'r.title', 'r.source_url', 'r.metadata_version', 'r.published', 'd.sha256', 'd.malware_scan_status', 'd.malware_scanned_sha256', 'd.status', 'd.file_name']);
$proof = ['target' => $target, 'resources' => $resources, 'active_writes' => 0, 'interface_observed' => false];
file_put_contents($private.'/legacy-import-qa.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo json_encode($proof, JSON_UNESCAPED_UNICODE);
+185
View File
@@ -0,0 +1,185 @@
"""Reconcile observed sector sources and stage originals privately; never write a database."""
import csv
import hashlib
import html
import json
import re
import shutil
import sys
import urllib.parse
from collections import Counter
from html.parser import HTMLParser
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'
OUT = PRIVATE / 'legacy-sector-reconciliation-20261010'
STAGE = ROOT / 'storage/app/private/document-import-staging/legacy-setores-20261010'
OUT.mkdir(parents=True, exist_ok=True)
STAGE.mkdir(parents=True, exist_ok=True)
csv.field_size_limit(5_000_000)
def load_csv(name):
return list(csv.DictReader((DOC / name).open(encoding='utf-8-sig', newline='')))
class Markup(HTMLParser):
def __init__(self, text):
super().__init__(convert_charrefs=True)
self.tags, self.links, self.text = Counter(), [], []
self.feed(text)
def handle_starttag(self, tag, attrs):
self.tags[tag] += 1
for key, value in attrs:
if key in ['href', 'src'] and value:
self.links.append(value)
def handle_data(self, data):
self.text.append(data)
inventory_file = PRIVATE / 'legacy-discovery/inventory.json'
inventory = json.loads(inventory_file.read_text(encoding='utf-8'))
records = {r['url']: r for r in inventory['records']}
# Inventory every existing captured file before selecting this increment.
for record in records.values():
if record.get('status') == 200:
path = ROOT / record['private_file']
assert path.is_file() and hashlib.sha256(path.read_bytes()).hexdigest() == record['sha256']
home = records['https://investemoz.tempo.co.mz/']['public_props']
sector_cards = home['dynamicOpportunities']
assert len(sector_cards) == 5
remaining = [card for card in sector_cards if card['slug'] != 'agronegocio']
# Use the existing anonymous collector for current sources, without rerunning its crawl.
namespace = {'__file__': str(ROOT / 'tools/legacy-content-discovery.py')}
collector = (ROOT / 'tools/legacy-content-discovery.py').read_text(encoding='utf-8')
exec(collector[:collector.index('\npending = ')], namespace)
fresh = [namespace['fetch'](card['href']) for card in remaining]
(OUT / 'current-public-captures.json').write_text(json.dumps(fresh, ensure_ascii=False, indent=2), encoding='utf-8')
assert all(r.get('status') == 200 and r.get('public_props', {}).get('post') for r in fresh), 'Current sector source unavailable; preserve old captures.'
current = {r['url']: r for r in fresh}
preservation = json.loads((PRIVATE / 'qa-readable-preservation.json').read_text(encoding='utf-8'))
saved_root = Path(preservation['directory'])
saved = json.loads((saved_root / 'manifest.json').read_text(encoding='utf-8'))
def saved_table(name):
proof = saved['tables']['"public"."' + name + '"']
data = (saved_root / proof['file']).read_bytes()
assert hashlib.sha256(data).hexdigest() == proof['sha256']
return [json.loads(line) for line in data.splitlines()]
masters = saved_table('sectors')
library = saved_table('documents')
resources = saved_table('portal_resources')
def normalized(value):
import unicodedata
return re.sub(r'\s+', ' ', ''.join(c for c in unicodedata.normalize('NFD', value.replace('&', ' e ')) if not unicodedata.combining(c))).strip().casefold()
selected, summaries = {}, []
for card in remaining:
record = current[card['href']]
post = record['public_props']['post']
markup = Markup(post['content'])
matches = [s for s in masters if normalized(s['name']) == normalized(post['title']) and s['is_active']]
assert len(matches) == 1, 'Sector master correspondence must be unambiguous.'
selected[record['url']] = record
linked = []
for ref in markup.links:
url = urllib.parse.urljoin(record['url'], ref)
child = records.get(url)
if child and child.get('public_props', {}).get('post'):
selected[url] = child
linked.append({'url': url, 'legacy_id': child['public_props']['post']['id'], 'title': child['public_props']['post']['title']})
summaries.append({'slug': card['slug'], 'title': post['title'], 'url': record['url'], 'legacy_id': post['id'],
'master_sector': {k: matches[0][k] for k in ['id', 'name', 'slug']},
'characters': len(re.sub(r'\s+', ' ', ' '.join(markup.text)).strip()), 'linked_articles': linked,
'tags': dict(markup.tags), 'body_sha256': hashlib.sha256(post['content'].encode()).hexdigest(),
'same_body_as_initial_capture': post['content'] == records[record['url']]['public_props']['post']['content'],
'same_modified_as_initial_capture': record['public_props']['seo'].get('modifiedTime') == records[record['url']]['public_props']['seo'].get('modifiedTime')})
versions, images, manifest, article_details = {}, {}, [], []
for record in selected.values():
post, seo = record['public_props']['post'], record['public_props']['seo']
body_hash = hashlib.sha256(post['content'].encode()).hexdigest()
key = (str(post['id']), seo.get('modifiedTime'), body_hash)
if key in versions:
versions[key]['urls'].append(record['url'])
continue
image_url = urllib.parse.urljoin(record['url'], post['thumbnail'])
image = records[image_url]
assert image['status'] == 200 and image['media_type'] == 'image/png'
images.setdefault(image['sha256'], image)
content = {'format': 'investmoz-legacy-article-v1', 'source_url': record['url'], 'legacy_id': post['id'],
'slug': post['slug'], 'title': post['title'], 'author': post['author'], 'legacy_category': post['category'],
'published_at': seo['publishedTime'], 'modified_at': seo['modifiedTime'], 'content_html': post['content'],
'image_source_url': image_url, 'image_sha256': image['sha256'], 'references': Markup(post['content']).links}
file_name = f"post-{post['id']}-{seo['modifiedTime'][:10].replace('-', '')}.txt"
data = json.dumps(content, ensure_ascii=False, indent=2).encode()
(STAGE / file_name).write_bytes(data)
identity = f"legacy-tempo-post-{post['id']}-{seo['modifiedTime'][:10].replace('-', '')}"
duplicate = next((d['id'] for d in library if d['sha256'].strip() == hashlib.sha256(data).hexdigest()), None)
conflicts = [d['id'] for d in library if d.get('external_id') == identity and d['sha256'].strip() != hashlib.sha256(data).hexdigest()]
topic = next((s['master_sector']['slug'] for s in summaries if s['url'] == record['url']), '')
if post['category'] == 'Turismo':
topic = 'turismo' # Original category, not a territorial geometry association.
if record['url'].rstrip('/').endswith('/energia'):
topic = 'energia-especifica'
if record['url'].rstrip('/').endswith('/mineracao'):
topic = 'mineracao'
manifest.append({'external_id': identity, 'relative_path': STAGE.name + '/' + file_name, 'file_name': file_name,
'title': post['title'], 'issuer': post['author'], 'document_date': seo['publishedTime'][:10],
'version_label': 'Legado modificado ' + seo['modifiedTime'], 'source_url': record['url'],
'source_reference': f"Tempo; post {post['id']}; categoria original {post['category']}; publicado {seo['publishedTime']}; modificado {seo['modifiedTime']}. Conteúdo editorial preservado.",
'category': 'education', 'topic': topic, 'document_type': 'guide' if post['content'].strip() else 'reference_data',
'language': 'pt', 'description': 'Exemplar editorial do legado; divulgação separada. ' + ('Corpo vazio no original; não substituir por texto inventado.' if not post['content'].strip() else 'Não representa uma oportunidade concreta ou aprovação externa.')})
details = {'id': post['id'], 'title': post['title'], 'category': post['category'], 'urls': [record['url']],
'published': seo['publishedTime'], 'modified': seo['modifiedTime'], 'body_sha256': body_hash,
'library_bytes_sha256': hashlib.sha256(data).hexdigest(), 'duplicate_document': duplicate, 'identity_conflicts': conflicts,
'file': file_name, 'tags': dict(Markup(post['content']).tags)}
versions[key] = details
article_details.append(details)
for image in images.values():
name = 'image-' + image['sha256'][:16] + '.png'
shutil.copyfile(ROOT / image['private_file'], STAGE / name)
assert hashlib.sha256((STAGE / name).read_bytes()).hexdigest() == image['sha256']
manifest.append({'external_id': 'legacy-tempo-image-' + image['sha256'][:16], 'relative_path': STAGE.name + '/' + name,
'file_name': name, 'title': 'Imagem original — ' + Path(urllib.parse.urlsplit(image['url']).path).name,
'issuer': 'Tempo (alojamento; autoria individual não indicada)', 'version_label': image['sha256'][:32],
'source_url': image['url'], 'source_reference': 'Imagem explicitamente referenciada no thumbnail legado; criação e autoria individual não indicadas.',
'category': 'media', 'topic': '', 'document_type': 'reference_data', 'language': 'pt',
'description': 'Original preservado. Fundo genérico não identifica um local, parcela ou oportunidade.'})
(STAGE / 'manifest.json').write_text(json.dumps({'documents': manifest}, ensure_ascii=False, indent=2), encoding='utf-8')
inventory_rows = load_csv('legado-inventario.csv')
content_rows = load_csv('legado-conteudos-reconciliados.csv')
for row in inventory_rows:
if row['URL'] in selected or row['SHA256'] in images:
row['Destino proposto'] = 'Biblioteca existente; adapter/componente LegacyContent e portal/legacy; URL original preservado'
row['Situação'] = 'Reconciliado por identidade/versão/hash; lote privado preparado; não importado; scan/divulgação/UI pendentes; C71 bloqueia aplicação'
for row in content_rows:
key = (row['ID legado'], row['Modificado'], row['SHA256 conteúdo'])
if key in versions:
row['Situação'] = 'Reconciliado; original preparado na biblioteca existente; não importado; sem recurso QA novo; visual/scan/divulgação pendentes'
result = {'database_access': False, 'database_writes': 0, 'active_access': False,
'master_snapshot_at': preservation['created_at'], 'master_snapshot_target': preservation['target'],
'initial_inventory_files_verified': len(records), 'initial_inventory_sha256': hashlib.sha256(inventory_file.read_bytes()).hexdigest(),
'sector_summaries': summaries, 'articles': article_details, 'image_count': len(images), 'manifest_count': len(manifest),
'manifest_path': str((STAGE / 'manifest.json').relative_to(ROOT)), 'import_applied': False,
'scan_verified': False, 'public_disclosure_applied': False, 'interface_accepted': False,
'csv_updates': {'legado-inventario.csv': inventory_rows, 'legado-conteudos-reconciliados.csv': content_rows}}
(OUT / 'reconciliation.json').write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({'sectors': len(summaries), 'articles': len(article_details), 'images': len(images), 'manifest_items': len(manifest),
'unchanged_current_sector_bodies': sum(s['same_body_as_initial_capture'] for s in summaries), 'database_writes': 0, 'import_applied': False}))
+45
View File
@@ -0,0 +1,45 @@
"""Verify the untouched origin field independently of the editorial adapter."""
import hashlib
import html
import json
import re
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
root = Path(__file__).resolve().parents[1]
directory = root / 'storage/app/private/investmoz-v2/portal-audit/legacy-financial-origin-20261010'
directory.mkdir(parents=True, exist_ok=True)
url = 'https://investemoz.tempo.co.mz/mercado-financeiro'
agent = 'Mozilla/5.0 InvestMoz-QA-content-inventory'
with urllib.request.urlopen(urllib.request.Request(url, headers={'Accept': 'text/html', 'User-Agent': agent}), timeout=35) as response:
raw = response.read()
status_html = response.status
match = re.search(r'data-page="([^"]+)"', raw.decode('utf-8'))
assert match
page = json.loads(html.unescape(match.group(1)))
post = page['props']['post']
assert post['id'] == 171994
headers = {'Accept': 'application/json', 'User-Agent': agent, 'X-Inertia': 'true', 'X-Requested-With': 'XMLHttpRequest', 'X-Inertia-Version': page['version']}
with urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=35) as response:
raw_json = response.read()
status_json = response.status
data = json.loads(raw_json)
assert data['props']['post']['id'] == post['id']
# Preserve anonymous HTTP snapshots privately; never serve shared session props.
(directory / 'origin-html-private.bin').write_bytes(raw)
(directory / 'origin-inertia-private.bin').write_bytes(raw_json)
proof = {'captured_at': datetime.now(timezone.utc).isoformat(), 'source_url': url,
'legacy_id': post['id'], 'html_http_status': status_html, 'inertia_http_status': status_json,
'origin_html_bytes_sha256': hashlib.sha256(raw).hexdigest(),
'origin_inertia_bytes_sha256': hashlib.sha256(raw_json).hexdigest(),
'initial_origin_content_type': type(post['content']).__name__,
'initial_origin_content_length': len(post['content']),
'inertia_origin_content_length': len(data['props']['post']['content']),
'contents_identical': data['props']['post']['content'] == post['content'],
'origin_content_sha256': hashlib.sha256(post['content'].encode()).hexdigest(),
'extraction_or_sanitization_applied': False, 'publication_applied': False,
'conclusion': 'Origin returns an empty string in both raw server payloads; not an extraction loss.'}
assert proof['initial_origin_content_length'] == 0 and proof['inertia_origin_content_length'] == 0
(directory / 'proof.json').write_text(json.dumps(proof, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps(proof, ensure_ascii=False))
+43
View File
@@ -0,0 +1,43 @@
<?php
// Exercise the existing presentation adapter against staged originals; no app/DB bootstrap.
require dirname(__DIR__).'/vendor/autoload.php';
$root = dirname(__DIR__).'/storage/app/private/investmoz-v2/portal-audit/legacy-sector-reconciliation-20261010';
$reconciliation = json_decode(file_get_contents($root.'/reconciliation.json'), true, 512, JSON_THROW_ON_ERROR);
$stage = dirname(__DIR__).'/storage/app/private/document-import-staging/legacy-setores-20261010';
$adapter = new App\Services\LegacyContent;
$checks = [];
foreach ($reconciliation['articles'] as $article) {
$bytes = file_get_contents($stage.'/'.$article['file']);
if (hash('sha256', $bytes) !== $article['library_bytes_sha256']) {
throw new RuntimeException('Staged source bytes changed.');
}
$source = json_decode($bytes, true, 512, JSON_THROW_ON_ERROR);
$projected = $adapter->sanitize($source['content_html']);
$doc = new DOMDocument;
$old = libxml_use_internal_errors(true);
$doc->loadHTML('<?xml encoding="UTF-8"><html><body>'.$source['content_html'].'</body></html>', LIBXML_NONET);
libxml_clear_errors();
libxml_use_internal_errors($old);
$headings = [];
foreach ($doc->getElementsByTagName('h1') as $heading) {
$headings[] = str_contains(html_entity_decode(strip_tags($projected), ENT_QUOTES | ENT_HTML5, 'UTF-8'), $heading->textContent);
}
$checks[] = ['legacy_id' => $article['id'], 'original_sha256_preserved' => true,
'source_h1_count' => count($headings), 'heading_text_preserved' => ! in_array(false, $headings, true),
'projected_h1_count' => preg_match_all('/<h1\b/i', $projected), 'projection_sha256' => hash('sha256', $projected)];
}
$malicious = $adapter->sanitize('<h1 onmouseover="bad()">Título<script>bad()</script><a href="javascript:bad()">Fonte</a></h1>');
$security = ! preg_match('/<script|javascript:|onmouseover/i', $malicious);
$passed = $security && ! in_array(false, array_column($checks, 'heading_text_preserved'), true);
$phase = $argv[1] ?? 'after';
if (! in_array($phase, ['before', 'after'], true)) {
exit(2);
}
$proof = ['result' => $passed ? 'passed' : 'failed', 'phase' => $phase, 'checks' => $checks,
'active_markup_removed' => $security, 'database_access' => false, 'interface_accepted' => false];
file_put_contents($root.'/content-check-'.$phase.'.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
echo json_encode(['result' => $proof['result'], 'phase' => $phase, 'articles' => count($checks),
'missing_heading_articles' => array_values(array_column(array_filter($checks, fn ($c) => ! $c['heading_text_preserved']), 'legacy_id')),
'active_markup_removed' => $security, 'database_access' => false, 'interface_accepted' => false], JSON_THROW_ON_ERROR).PHP_EOL;
exit($passed ? 0 : 1);
+128
View File
@@ -0,0 +1,128 @@
// Actual desktop Chromium UI on guarded QA. No QA18 credentials, cookies, or MFA values in evidence.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { chromium } = require('C:/Users/macbook/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
const ROOT = path.resolve(__dirname, '..'), PRIVATE = path.join(ROOT, 'storage/app/private/investmoz-v2/portal-audit');
const BASE = 'http://127.0.0.1:18097', TARGET = 'desk_restore_20261009_174726_test:55441';
const actors = JSON.parse(fs.readFileSync(path.join(PRIVATE, 'profiles-v1-actors-private.json'), 'utf8'));
assert.equal(actors.promoter.id, 21);
const receipt = path.join(PRIVATE, 'legacy-sectors-browser-qa.json');
const old = fs.existsSync(receipt) ? JSON.parse(fs.readFileSync(receipt, 'utf8')) : {};
if (old.started_at) fs.copyFileSync(receipt, path.join(PRIVATE, 'legacy-sectors-browser-attempt-' + old.started_at.replaceAll(':', '-') + '.json'));
const stamp = new Date().toISOString().replace(/[:.]/g, '-'), shots = path.join(PRIVATE, 'legacy-sectors-browser-' + stamp);
fs.mkdirSync(shots, { recursive: true });
const proof = { started_at: new Date().toISOString(), base: BASE, target: TARGET, browser: 'Chromium 1243, isolated headless desktop contexts',
interface_observed: true, actor_id: 21, active_writes: 0, dossiers: old.dossiers || {}, checks: {}, screenshots: [], events: [], phase: 'initialization' };
let browser;
function save() { fs.writeFileSync(receipt, JSON.stringify(proof, null, 2)); }
function phase(value) { proof.phase = value; save(); console.log(JSON.stringify({ phase: value })); }
function otp(secret) {
let bits = 0, value = 0; const bytes = [], alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
for (const char of secret.toUpperCase().replace(/=+$/, '')) { const digit = alphabet.indexOf(char); assert(digit >= 0); value = (value << 5) | digit; bits += 5; if (bits >= 8) { bytes.push((value >>> (bits - 8)) & 255); bits -= 8; } }
const counter = Buffer.alloc(8); counter.writeBigUInt64BE(BigInt(Math.floor(Date.now() / 30000)));
const digest = crypto.createHmac('sha1', Buffer.from(bytes)).update(counter).digest(), offset = digest[19] & 15;
return String((digest.readUInt32BE(offset) & 0x7fffffff) % 1000000).padStart(6, '0');
}
async function context() {
const ctx = await browser.newContext({ viewport: { width: 1440, height: 1000 }, locale: 'pt-MZ', reducedMotion: 'reduce' });
ctx.qaConfirmed = false;
await ctx.route('**/*', async (route) => { const request = route.request(), url = new URL(request.url());
if (url.origin !== BASE) return route.abort();
if (!['GET', 'HEAD'].includes(request.method())) assert(ctx.qaConfirmed, 'Actual QA guard precedes writes');
await route.continue(); });
ctx.on('response', (response) => { const url = new URL(response.url()); if (url.origin === BASE && !url.pathname.startsWith('/build-qa-')) {
const headers = response.headers(); proof.events.push({ method: response.request().method(), path: url.pathname, status: response.status(),
target: headers['x-investmoz-qa-target'] || null, redirect_path: headers.location ? new URL(headers.location, BASE).pathname : null }); save(); } });
ctx.on('requestfailed', request => { const url = new URL(request.url()); proof.events.push({ request_failed: url.pathname, reason: request.failure()?.errorText }); save(); });
return ctx;
}
async function open(page, url) { const response = await page.goto(BASE + url, { waitUntil: 'domcontentloaded', timeout: 45000 }); assert.equal(response.headers()['x-investmoz-qa-target'], TARGET); page.context().qaConfirmed = true; return response; }
async function shot(page, name, fullPage = true) { const file = path.join(shots, name + '.png'); await page.screenshot({ path: file, fullPage }); proof.screenshots.push(path.relative(ROOT, file).replaceAll('\\', '/')); }
async function login(page, role) {
const account = actors[role]; if (new URL(page.url()).pathname !== '/login') await open(page, '/login');
await page.locator('#email').fill(account.email); await page.locator('#password').fill(account.password);
await page.locator('[data-test="login-button"]').click(); await page.waitForURL(BASE + '/two-factor-challenge');
await page.locator('input[name="code"]').fill(otp(account.totp_secret)); await page.getByRole('button', { name: 'Continue', exact: true }).click();
await page.waitForURL(url => !['/login', '/two-factor-challenge'].includes(url.pathname)); await page.waitForLoadState('networkidle');
}
async function logout(page) { await open(page, '/dashboard'); const triggers = page.locator('button[aria-haspopup="menu"]');
for (let i = await triggers.count() - 1; i >= 0; i--) { await triggers.nth(i).click(); const action = page.locator('[data-test="logout-button"]');
if (await action.isVisible()) { await action.click(); await page.waitForURL(BASE + '/'); return; } await page.keyboard.press('Escape'); }
throw new Error('Visible logout missing'); }
async function dossier(page, entry) { await page.waitForURL(BASE + '/investimentos/' + entry.id); await page.getByRole('heading', { name: entry.title, exact: true }).waitFor();
assert.equal(await page.getByRole('heading', { name: /^Fase \d+ ·/ }).count(), 5); assert.equal(await page.locator('article[id^="S"]').count(), 19);
const refs = page.getByRole('region', { name: 'Fontes da ideia' }); assert.equal(await refs.locator('li').count(), 1);
assert.equal(new URL(await refs.locator('a').first().getAttribute('href'), BASE).pathname, entry.href);
await page.getByRole('heading', { name: 'Próxima acção', exact: true }).waitFor(); }
(async () => {
browser = await chromium.launch({ headless: true, executablePath: 'C:/Users/macbook/AppData/Local/ms-playwright/chromium-1243/chrome-win64/chrome.exe' });
phase('public sector pages, images and search');
const ctx = await context(), page = await ctx.newPage(); page.setDefaultTimeout(15000);
for (const [group, expected] of [['energia-mineracao', 3], ['turismo', 12], ['infraestruturas', 1]]) {
assert.equal((await open(page, '/explorar/' + group)).status(), 200);
assert.equal(await page.getByRole('link', { name: 'Ler o artigo', exact: true }).count(), expected);
const image = page.locator('main img'); await image.waitFor(); await image.evaluate(img => img.decode()); assert(await image.evaluate(img => img.complete && img.naturalWidth > 0));
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)); await shot(page, 'public-' + group);
const articleLinks = await page.getByRole('link', { name: 'Ler o artigo', exact: true }).evaluateAll(links => links.map(a => a.getAttribute('href')));
for (const href of articleLinks) { await open(page, href); const body = page.getByRole('region', { name: 'Artigo educativo' }); await body.waitFor(); assert((await body.innerText()).trim().length > 0);
await page.locator('main img').evaluate(img => img.decode()); assert(await page.locator('main img').evaluate(img => img.complete && img.naturalWidth > 0)); }
await open(page, '/explorar/' + group); await page.getByRole('searchbox').fill('zzzz-no-content-qa'); await page.getByRole('button', { name: 'Pesquisar', exact: true }).click();
await page.getByText('Nenhum conteúdo corresponde à pesquisa.', { exact: true }).waitFor(); assert.equal(await page.getByRole('link', { name: 'Ler o artigo', exact: true }).count(), 0);
}
await open(page, '/explorar/energia-mineracao'); await page.getByRole('searchbox').fill('Energia'); await page.getByRole('button', { name: 'Pesquisar', exact: true }).click();
await page.locator('h3 a[href="/energia"]').waitFor();
await open(page, '/energia-mineracao'); const body = page.getByRole('region', { name: 'Artigo educativo' });
await body.locator('a[href="/energia"]').click(); await page.waitForURL(BASE + '/energia');
await open(page, '/energia-mineracao'); await body.locator('a[href="/mineracao"]').click(); await page.waitForURL(BASE + '/mineracao');
await open(page, '/infraestruturas'); await page.getByRole('region', { name: 'Artigo educativo' }).getByRole('heading', { name: '2. Sectores Prioritários de Investimento', exact: true }).waitFor();
await shot(page, 'infra-original-heading');
assert.equal((await open(page, '/mercado-financeiro')).status(), 404);
proof.checks.public_read_search_images_distinct_articles = true; save();
phase('public CTA, QA21 normal MFA, UI creation or continuation');
for (const href of ['/energia', '/mineracao', '/turismo', '/infraestruturas']) {
await open(page, href); await page.getByRole('link', { name: 'Iniciar ou continuar uma ideia', exact: true }).click();
await page.waitForURL(url => ['/login', '/investimentos/a-partir-de' + href].includes(url.pathname));
if (new URL(page.url()).pathname === '/login') await login(page, 'promoter');
await page.waitForURL(BASE + '/investimentos/a-partir-de' + href);
const entry = proof.dossiers[href] ||= { title: 'QA visual — ideia ' + href.slice(1) + ' ' + stamp, href };
if (!entry.id) { await page.getByLabel('Título', { exact: true }).fill(entry.title); await page.getByLabel('Autorizo reutilizar o meu perfil neste dossier privado.').check();
await page.getByRole('button', { name: 'Criar e guardar dossier', exact: true }).click(); await page.waitForURL(/\/investimentos\/[0-9a-f-]{36}$/);
entry.id = new URL(page.url()).pathname.split('/').pop(); entry.created_via_interface_at = new Date().toISOString(); save();
} else { const card = page.getByRole('listitem').filter({ has: page.getByRole('link', { name: entry.title, exact: true }) });
await card.getByRole('button', { name: 'Continuar neste dossier com o artigo', exact: true }).click(); }
await dossier(page, entry); await shot(page, 'dossier-' + href.slice(1), false);
await page.getByRole('region', { name: 'Fontes da ideia' }).locator('a').first().click(); await page.waitForURL(url => url.pathname === href && url.searchParams.get('context') === entry.id);
await page.getByRole('link', { name: 'Continuar: ' + entry.title, exact: true }).click(); await dossier(page, entry);
entry.source_return_to_same_dossier = true; save();
}
phase('visible logout, new MFA browser session, recovery and refresh');
await logout(page); await ctx.close();
const freshCtx = await context(), fresh = await freshCtx.newPage(); const first = Object.values(proof.dossiers)[0];
await open(fresh, '/investimentos/' + first.id); await fresh.waitForURL(BASE + '/login'); await login(fresh, 'promoter');
for (const entry of Object.values(proof.dossiers)) {
await open(fresh, '/investimentos/' + entry.id); await dossier(fresh, entry); await fresh.reload({ waitUntil: 'networkidle' }); await dossier(fresh, entry);
await open(fresh, '/investimentos/a-partir-de' + entry.href); const card = fresh.getByRole('listitem').filter({ has: fresh.getByRole('link', { name: entry.title, exact: true }) });
await card.getByRole('button', { name: 'Continuar neste dossier com o artigo', exact: true }).click(); await dossier(fresh, entry);
entry.new_session_recovered_and_refreshed = true; entry.single_reference_after_repetition = true; save();
}
phase('other profile denies dossiers, public context remains private');
const otherCtx = await context(), other = await otherCtx.newPage(); await open(other, '/login'); await login(other, 'outsider');
const publicCtx = await context(), publicPage = await publicCtx.newPage();
for (const entry of Object.values(proof.dossiers)) {
assert.equal((await open(other, '/investimentos/' + entry.id)).status(), 403);
for (const reader of [other, publicPage]) { await open(reader, entry.href + '?context=' + entry.id); await reader.getByRole('region', { name: 'Artigo educativo' }).waitFor();
assert.equal(await reader.getByText(entry.title, { exact: false }).count(), 0); }
entry.other_actor_id = actors.outsider.id; entry.denied_status = 403; entry.private_context_hidden = true;
}
await logout(fresh); await freshCtx.close(); await otherCtx.close(); await publicCtx.close();
proof.result = 'passed'; proof.completed_at = new Date().toISOString(); save();
console.log(JSON.stringify({ result: proof.result, dossiers: Object.keys(proof.dossiers).length, screenshots: proof.screenshots.length, interface_observed: true, active_writes: 0 }));
})().catch(async error => { proof.result = 'failed'; proof.error_type = error.name;
let message = error.message;
for (const actor of Object.values(actors)) for (const key of ['password', 'totp_secret', 'secret']) if (actor[key]) message = message.replaceAll(actor[key], '[REDACTED]');
proof.error = message.replace(/\b\d{6}\b/g, '[MFA OMITIDO]').slice(0, 1600);
proof.page_states = browser ? await Promise.all(browser.contexts().flatMap(ctx => ctx.pages()).map(async page => ({ path: new URL(page.url()).pathname, headings: await page.locator('h1').allTextContents().catch(() => []), alerts: await page.getByRole('alert').count().catch(() => 0) }))) : [];
save(); console.error(JSON.stringify({ result: 'failed', phase: proof.phase, type: error.name })); process.exitCode = 1;
}).finally(async () => { if (browser) await browser.close(); });
+134
View File
@@ -0,0 +1,134 @@
"""Separate authenticated QA disclosure, real originals, and sector dossier continuity."""
import hashlib
import json
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path
from portal_profiles_http_session import BASE, EXPECTED, PRIVATE, ROOT, Session, actors, events
proof = {'started_at': datetime.now(timezone.utc).isoformat(), 'target': EXPECTED, 'base': BASE,
'interface_observed': False, 'active_writes': 0, 'phase': 'initialization'}
proof_path = PRIVATE / 'legacy-sectors-http-qa.json'
previous = json.loads(proof_path.read_text(encoding='utf-8')) if proof_path.exists() else {}
if previous:
archive = PRIVATE / ('legacy-sectors-http-attempt-' + previous['started_at'].replace(':', '-') + '.json')
if not archive.exists():
archive.write_text(json.dumps(previous, ensure_ascii=False, indent=2), encoding='utf-8')
def save():
proof_path.write_text(json.dumps(proof, ensure_ascii=False, indent=2), encoding='utf-8')
def run():
receipt = json.loads((PRIVATE / 'legacy-sectors-verify-qa.json').read_text(encoding='utf-8'))
manifest = json.loads((ROOT / 'storage/app/private/document-import-staging/legacy-setores-20261010/manifest.json').read_text(encoding='utf-8'))
assert receipt['target'] == {'database': 'desk_restore_20261009_174726_test', 'port': 55441}
assert len(receipt['resources']) == 22 and all(c['clean_scan_for_hash'] and c['stored_original_equal'] for c in receipt['checks'])
actors['library_manager'] = json.loads((PRIVATE / 'resource-http-actors-private.json').read_text(encoding='utf-8'))['manager']
anonymous = Session('outsider')
manager = Session('library_manager').login()
rows = receipt['resources']
proof['phase'] = 'separate_authenticated_disclosure'
proof['publication_actions'] = []
for row in rows:
record = next(r for r in manifest['documents'] if r['external_id'] == row['external_id'])
assert hashlib.sha256(manager.call('GET', f"/management/documents/{row['document_id']}/download", binary=True)).hexdigest() == row['sha256'].strip()
if record['external_id'].startswith('legacy-tempo-post-171994-'):
assert not row['published']
anonymous.call('GET', f"/documentos/{row['id']}/view", status=404, binary=True)
proof['empty_finance_private'] = {'resource_id': row['id'], 'external_id': row['external_id'], 'published': False}
continue
if not row['published']:
anonymous.call('GET', f"/documentos/{row['id']}/view", status=404, binary=True)
data = {k: record[k] for k in ['title', 'issuer', 'category', 'topic', 'document_type', 'source_reference', 'source_url', 'language', 'version_label', 'description']}
data.update({'document_id': row['document_id'], 'document_date': record.get('document_date'), 'metadata_version': row['metadata_version'],
'territorial_scope': 'unknown', 'source_confirmation': 'identified', 'legal_status': 'not_applicable',
'rights_status': 'authorized', 'data_classification': 'public', 'independent_review': False,
'legislation': False, 'essential': False,
'source_reference': record['source_reference'] + '; utilização e divulgação do legado exclusivamente em QA autorizadas pelo responsável em 10/10/2026; sem habilitação ativa.'})
if record['external_id'].startswith('legacy-tempo-post-'):
source = json.loads((ROOT / 'storage/app/private/document-import-staging' / record['relative_path']).read_text(encoding='utf-8'))
image = next((r for r in rows if r['sha256'].strip() == source.get('image_sha256')), None)
if image:
data['associations'] = [{'target_type': 'document', 'target_key': str(image['id']), 'relationship': 'related_to',
'applicability': 'support', 'basis_reference': 'Thumbnail identificada no exemplar original do post: ' + image['source_url'], 'review_status': 'confirmed'}]
manager.call('PUT', f"/management/portal-services/resources/{row['id']}", data, status=302, binary=True)
manager.call('POST', f"/management/portal-services/resources/{row['id']}", {'published': True}, binary=True)
proof['publication_actions'].append({'resource_id': row['id'], 'actor_id': actors['library_manager']['id'], 'authenticated': True,
'mfa_challenge_observed': any(e['actor_id'] == actors['library_manager']['id'] and e['path'] == '/two-factor-challenge' and e['status'] == 204 for e in events)})
save()
assert hashlib.sha256(anonymous.call('GET', f"/documentos/{row['id']}/download", binary=True)).hexdigest() == row['sha256'].strip()
proof['phase'] = 'public_consumers'
articles = {}
for group, expected in [('energia-mineracao', 3), ('turismo', 12), ('infraestruturas', 1)]:
landing = anonymous.call('GET', '/explorar/' + group, inertia=True)['props']
assert len(landing['articles']) == expected
for brief in landing['articles']:
article = anonymous.call('GET', brief['href'], inertia=True)['props']['article']
assert article['html'].strip() and article['image'] and not article['calculation_use']
assert article['territorial_references'] == []
assert hashlib.sha256(anonymous.call('GET', article['image']['href'], binary=True)).hexdigest() == article['image']['sha256']
anonymous.call('GET', article['library_href'], inertia=True)
anonymous.call('GET', article['start_href'], status=401, binary=True)
articles[brief['href']] = article
missing = anonymous.call('GET', '/explorar/' + group + '?q=zzzz-no-content-qa', inertia=True)['props']
assert not missing['match'] and missing['articles'] == []
hit = anonymous.call('GET', '/explorar/energia-mineracao?q=Energia', inertia=True)['props']
assert hit['match'] and any(a['href'] == '/energia' for a in hit['articles'])
assert articles['/energia']['id'] != articles['/mineracao']['id']
assert 'href="/energia"' in articles['/energia-mineracao']['html'] and 'href="/mineracao"' in articles['/energia-mineracao']['html']
assert '<h2' in articles['/infraestruturas']['html']
anonymous.call('GET', '/mercado-financeiro', status=404, binary=True)
anonymous.call('GET', '/explorar/mercado-financeiro', status=404, binary=True)
proof['public'] = {'new_nonempty_articles': len(articles), 'distinct_energy_mining': True,
'tourism_editorial_only': True, 'finance_empty_page_unavailable': True,
'image_and_original_hashes': True, 'search_hit_and_miss': True, 'infra_original_heading_preserved': True,
'urls': list(articles)}
save()
proof['phase'] = 'dossier_continuity'
owner = Session('promoter').login()
other = Session('outsider').login()
proof['dossiers'] = previous.get('dossiers', {})
for href in ['/energia', '/mineracao', '/turismo', '/infraestruturas']:
article = articles[href]
choice = owner.call('GET', article['start_href'], inertia=True)['props']
assert choice['contentSource']['id'] == article['id']
entry = proof['dossiers'].setdefault(href, {})
if not entry.get('id'):
entry['id'] = owner.call('POST', '/investimentos', {'title': 'Synthetic QA — ideia ' + article['title'],
'client_key': str(uuid.uuid4()), 'reuse_consent': True, 'content_resource_id': article['id']}, status=201)['id']
save()
case = entry['id']
first = owner.call('GET', '/investimentos/' + case)
assert len(first['phases']) == 5 and len(first['steps']) == 19
assert len(first['content_sources']) == 1 and first['content_sources'][0]['resource_id'] == article['id']
owner.call('POST', f'/investimentos/{case}/conteudos', {'version': first['case']['version'], 'resource_id': article['id']})
repeated = owner.call('GET', '/investimentos/' + case)
assert repeated['case']['version'] == first['case']['version'] and repeated['content_sources'] == first['content_sources']
other.call('GET', '/investimentos/' + case, status=403, binary=True)
other.call('POST', f'/investimentos/{case}/conteudos', {'version': first['case']['version'], 'resource_id': article['id']}, status=403, binary=True)
assert other.call('GET', href + '?context=' + case, inertia=True)['props']['contentContext'] is None
assert anonymous.call('GET', href + '?context=' + case, inertia=True)['props']['contentContext'] is None
entry.update(resource_id=article['id'], return_href=href, other_account_denied=True, reference_replay_idempotent=True)
save()
owner.call('POST', '/logout', {}, status=204)
recovered = Session('promoter').login()
for href, entry in proof['dossiers'].items():
view = recovered.call('GET', '/investimentos/' + entry['id'])
assert len(view['content_sources']) == 1 and view['content_sources'][0]['return_href'] == href
assert recovered.call('GET', href + '?context=' + entry['id'], inertia=True)['props']['contentContext']['id'] == entry['id']
entry['new_mfa_session_recovered'] = True
proof.update(result='passed', completed_at=datetime.now(timezone.utc).isoformat(), events=events, actor_id=21)
save()
print(json.dumps({'result': 'passed', 'new_articles': len(articles), 'publication_actions': len(proof['publication_actions']),
'dossiers': len(proof['dossiers']), 'events': len(events), 'finance_private': True, 'active_writes': 0}))
try:
run()
except Exception as error:
proof.update(result='failed', error_type=type(error).__name__, events=events)
save()
print(json.dumps({'result': 'failed', 'phase': proof['phase'], 'error_type': type(error).__name__, 'details': 'private sanitized receipt'}))
sys.exit(1)
+83
View File
@@ -0,0 +1,83 @@
<?php
// Existing private-library importer, isolated QA only; disclosure is a separate HTTP action.
require dirname(__DIR__).'/vendor/autoload.php';
require __DIR__.'/portal-qa-environment.php';
$target = portalQaEnvironment();
$app = require dirname(__DIR__).'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$identity = portalQaAssertTarget($target);
config(['portal.managers' => ['opportunity-manager@example.invalid', 'geo-manager-qa@example.invalid']]);
$directory = storage_path('app/private/investmoz-v2/portal-audit');
$stamp = gmdate('Ymd_His');
set_exception_handler(function (Throwable $error) use ($directory, $stamp): void {
file_put_contents($directory.'/legacy-sector-import-error-private-'.$stamp.'.json', json_encode(['type' => get_class($error), 'message' => $error->getMessage()], JSON_THROW_ON_ERROR));
fwrite(STDERR, "QA sector operation failed; inspect private diagnostic.\n");
exit(1);
});
$mode = $argv[1] ?? 'preview';
throw_unless(in_array($mode, ['preview', 'apply', 'verify', 'replay'], true), RuntimeException::class, 'Unknown operation');
$manifestName = 'legacy-setores-20261010/manifest.json';
$staging = storage_path('app/private/document-import-staging');
$manifest = json_decode(file_get_contents($staging.'/'.$manifestName), true, 512, JSON_THROW_ON_ERROR);
throw_unless(count($manifest['documents']) === 22, RuntimeException::class, 'Expected original 22-item sector lot');
$actorFile = json_decode(file_get_contents($directory.'/resource-http-actors-private.json'), true, 512, JSON_THROW_ON_ERROR);
$actor = App\Models\User::findOrFail($actorFile['manager']['id']);
throw_unless($actor->can('manage-portal'), RuntimeException::class, 'Authorized existing library manager required');
function sectorCounts(): array {
$result = [];
foreach (['documents', 'portal_resources', 'audit_events', 'document_malware_scans', 'opportunities', 'desk_cases', 'users', 'jobs'] as $table) {
if (Illuminate\Support\Facades\Schema::hasTable($table)) { $result[$table] = Illuminate\Support\Facades\DB::table($table)->count(); }
}
return $result;
}
$before = sectorCounts();
$exit = null;
if (in_array($mode, ['preview', 'apply', 'replay'], true)) {
$args = ['manifest' => $manifestName];
if ($mode !== 'preview') {
$health = app(App\Services\Malware\MalwareScanner::class)->health();
throw_unless($health['available'] ?? false, RuntimeException::class, 'Real ClamD not healthy; import refused');
// A fresh read-only preview is mandatory before each application, including repetition.
$previewExit = Illuminate\Support\Facades\Artisan::call('documents:import-batch', $args);
$preview = Illuminate\Support\Facades\Artisan::output();
file_put_contents($directory.'/legacy-sectors-pre-apply-'.$stamp.'.txt', $preview);
throw_unless($previewExit === 0 && ! str_contains($preview, 'review required') && ! str_contains($preview, 'not imported'), RuntimeException::class, 'Preview has unresolved conflicts');
$args += ['--apply' => true, '--actor' => (string) $actor->id];
config(['queue.default' => 'sync']); // Actual configured ClamD scanner; no fake scan result.
}
$exit = Illuminate\Support\Facades\Artisan::call('documents:import-batch', $args);
$output = Illuminate\Support\Facades\Artisan::output();
file_put_contents($directory.'/legacy-sectors-import-'.$mode.'-'.$stamp.'.txt', $output);
throw_unless($exit === 0 && ! str_contains($output, 'review required') && ! str_contains($output, 'not imported'), RuntimeException::class, 'Import/preview failed');
}
$rows = Illuminate\Support\Facades\DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
->whereIn('d.external_id', array_column($manifest['documents'], 'external_id'))
->orderBy('r.id')->get(['r.id', 'r.document_id', 'r.title', 'r.source_url', 'r.metadata_version', 'r.published', 'd.external_id', 'd.sha256',
'd.malware_scan_status', 'd.malware_scanned_sha256', 'd.status', 'd.file_name', 'd.storage_key', 'd.storage_encryption', 'd.data_classification']);
$checks = [];
foreach ($rows as $row) {
$record = collect($manifest['documents'])->firstWhere('external_id', $row->external_id);
$path = $staging.'/'.$record['relative_path'];
$bytes = Illuminate\Support\Facades\Storage::disk('local')->get($row->storage_key);
if ($row->storage_encryption === 'laravel') { $bytes = Illuminate\Support\Facades\Crypt::decryptString($bytes); }
$checks[] = ['resource_id' => $row->id, 'source_hash_equal' => hash_equals(trim($row->sha256), hash_file('sha256', $path)),
'stored_original_equal' => hash_equals(trim($row->sha256), hash('sha256', $bytes)), 'clean_scan_for_hash' => $row->malware_scan_status === 'clean'
&& hash_equals(trim($row->sha256), (string) $row->malware_scanned_sha256)];
unset($bytes);
}
$after = sectorCounts();
if ($mode === 'preview') { throw_unless($before === $after, RuntimeException::class, 'Preview unexpectedly changed database'); }
if ($mode !== 'preview') {
throw_unless($rows->count() === 22 && collect($checks)->every(fn ($c) => $c['source_hash_equal'] && $c['stored_original_equal'] && $c['clean_scan_for_hash']), RuntimeException::class, 'Original/hash/scan checks incomplete');
if ($mode === 'apply') { throw_unless($rows->every(fn ($r) => ! $r->published && $r->data_classification === 'internal'), RuntimeException::class, 'Importer must not publish'); }
if ($mode === 'replay') { throw_unless($before === $after, RuntimeException::class, 'Repeat import is not idempotent'); }
}
$proof = ['mode' => $mode, 'captured_at' => gmdate('c'), 'target' => $identity, 'manifest_sha256' => hash_file('sha256', $staging.'/'.$manifestName),
'expected_items' => 22, 'resources' => $rows, 'checks' => $checks, 'before' => $before, 'after' => $after, 'command_exit' => $exit,
'scanner_class' => get_class(app(App\Services\Malware\MalwareScanner::class)), 'active_writes' => 0, 'publication_is_separate' => true];
$path = $directory.'/legacy-sectors-'.$mode.'-'.$stamp.'.json';
file_put_contents($path, json_encode($proof, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
file_put_contents($directory.'/legacy-sectors-'.$mode.'-qa.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
echo json_encode(['mode' => $mode, 'expected_items' => 22, 'resources_found' => $rows->count(), 'before' => $before, 'after' => $after, 'command_exit' => $exit,
'target' => $identity, 'active_writes' => 0], JSON_THROW_ON_ERROR).PHP_EOL;
+55
View File
@@ -0,0 +1,55 @@
<?php
// Independent read-only persistence proof after real HTTP and Chromium journeys.
require dirname(__DIR__).'/vendor/autoload.php';
require __DIR__.'/portal-qa-environment.php';
$target = portalQaEnvironment();
$app = require dirname(__DIR__).'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$identity = portalQaAssertTarget($target);
$dir = storage_path('app/private/investmoz-v2/portal-audit');
set_exception_handler(function (Throwable $error) use ($dir): void {
file_put_contents($dir.'/legacy-sector-persistence-error-private.json', json_encode(['type' => get_class($error), 'message' => $error->getMessage()]));
fwrite(STDERR, "QA persistence check failed; inspect private diagnostic.\n"); exit(1);
});
$http = json_decode(file_get_contents($dir.'/legacy-sectors-http-qa.json'), true, 512, JSON_THROW_ON_ERROR);
$browser = json_decode(file_get_contents($dir.'/legacy-sectors-browser-qa.json'), true, 512, JSON_THROW_ON_ERROR);
throw_unless(($http['result'] ?? null) === 'passed' && ($browser['result'] ?? null) === 'passed'
&& $http['target'] === $target['database'].':55441' && $browser['target'] === $http['target'], RuntimeException::class, 'Passed same-target evidence required');
Illuminate\Support\Facades\DB::statement('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
try {
$cases = [];
foreach (['HTTP' => $http, 'Chromium' => $browser] as $consumer => $receipt) {
foreach ($receipt['dossiers'] as $href => $entry) {
$case = Illuminate\Support\Facades\DB::table('desk_cases')->where('id', $entry['id'])->firstOrFail();
$payload = app(App\Services\VirtualDesk::class)->decode($case->payload);
$refs = $payload['investor_path']['content_sources'] ?? [];
$events = Illuminate\Support\Facades\DB::table('desk_events')->where('case_id', $case->id)->where('action', 'legacy_content_referenced')->count();
throw_unless((int) $case->user_id === 21 && count($refs) === 1 && $refs[0]['return_href'] === $href && $events === 1,
RuntimeException::class, 'Dossier/source isolation or idempotence failed');
$cases[] = ['consumer' => $consumer, 'id' => $case->id, 'resource_id' => $refs[0]['resource_id'], 'return_href' => $href,
'reference_count' => 1, 'reference_event_count' => 1, 'version' => $case->version];
}
}
$manifest = json_decode(file_get_contents(storage_path('app/private/document-import-staging/legacy-setores-20261010/manifest.json')), true, 512, JSON_THROW_ON_ERROR);
$resources = Illuminate\Support\Facades\DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
->whereIn('d.external_id', array_column($manifest['documents'], 'external_id'))->get(['r.id', 'r.published', 'r.metadata_version', 'd.external_id', 'd.sha256', 'd.malware_scan_status', 'd.malware_scanned_sha256']);
$originals = [];
foreach ($resources as $row) {
$count = Illuminate\Support\Facades\DB::table('documents')->where('sha256', $row->sha256)->whereNull('deleted_at')->count();
throw_unless($count === 1 && $row->malware_scan_status === 'clean' && hash_equals(trim($row->sha256), trim($row->malware_scanned_sha256)), RuntimeException::class, 'Original duplication or missing scan');
$originals[] = ['resource_id' => $row->id, 'documents_for_hash' => $count, 'metadata_version' => $row->metadata_version, 'published' => $row->published];
}
$finance = $resources->first(fn ($row) => str_starts_with($row->external_id, 'legacy-tempo-post-171994-'));
$territorialLinks = Illuminate\Support\Facades\DB::table('portal_resource_links')->whereIn('resource_id', $resources->pluck('id'))->where('target_type', 'territory')->count();
throw_unless($resources->count() === 22 && $resources->where('published', true)->count() === 21 && $finance && ! $finance->published && $territorialLinks === 0,
RuntimeException::class, 'Publication or territorial scope differs');
$scans = Illuminate\Support\Facades\DB::table('document_malware_scans')->whereIn('document_id', Illuminate\Support\Facades\DB::table('documents')->whereIn('external_id', array_column($manifest['documents'], 'external_id'))->pluck('id'))
->get(['engine', 'engine_version', 'result', 'file_sha256']);
throw_unless($scans->count() === 22 && $scans->every(fn ($s) => $s->engine === 'ClamAV' && $s->engine_version !== 'fixture' && $s->result === 'clean'), RuntimeException::class, 'Real ClamD evidence required');
$proof = ['verified_at' => gmdate('c'), 'target' => $identity, 'transaction_read_only' => true, 'cases' => $cases, 'originals' => $originals,
'real_scans' => $scans, 'automatic_territory_links' => $territorialLinks, 'finance_private' => true, 'database_writes' => 0, 'active_writes' => 0];
file_put_contents($dir.'/legacy-sectors-persistence-qa.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
echo json_encode(['dossiers_verified' => count($cases), 'originals' => count($originals), 'real_scans' => $scans->count(), 'published' => 21,
'finance_private' => true, 'automatic_territory_links' => 0, 'database_writes' => 0, 'active_writes' => 0]).PHP_EOL;
} finally { Illuminate\Support\Facades\DB::statement('ROLLBACK'); }
+181
View File
@@ -0,0 +1,181 @@
"""Accepted modular QA contracts on the actual isolated server; no auth material in receipts."""
import copy
import hashlib
import json
import uuid
import sys
from datetime import datetime, timezone
from portal_profiles_http_session import Session, PRIVATE, BASE, EXPECTED, REASON, events
receipt_path = PRIVATE / 'opportunity-modular-receipts-qa.json'
receipts = json.loads(receipt_path.read_text(encoding='utf-8-sig')) if receipt_path.exists() else {}
proof = {'url': BASE, 'target': EXPECTED, 'started_at': datetime.now(timezone.utc).isoformat(),
'interface_observed': False, 'active_writes': 0, 'cases': []}
def page(session, ident):
return session.call('GET', f'/minhas-oportunidades/{ident}/view', inertia=True)['props']
def save(session, ident, payload):
return session.call('PUT', f'/minhas-oportunidades/{ident}/draft', {'etag': page(session, ident)['etag'], 'canonical': payload, 'reason': REASON})
def action(session, ident, name, status=200, **extra):
current = page(session, ident)
if name == 'publish':
extra.update({'confirm_publication': True, 'preview_hash': current['profileContext']['approval']['hash']})
return session.call('POST', f'/minhas-oportunidades/{ident}/workflow', {'etag': current['etag'], 'action': name, 'reason': REASON, **extra}, status=status)
def prepare(session, category, mode):
source = page(session, 33)
original = source['values']['canonical']
exclude = ['images', 'review_due_date', 'module_profile_version'] + ([] if category == 'business_sale' else ['business_transaction'])
payload = {k: copy.deepcopy(v) for k, v in original.items() if k not in exclude}
payload.update({'opportunity_category': category, 'visibility': 'internal'})
key = category + ':' + mode
if key not in receipts:
created = session.call('POST', '/minhas-oportunidades/drafts', {'creation_key': str(uuid.uuid4()), 'organization_id': source['profileOrganization'],
'opportunity_category': category, 'canonical': {'opportunity_category': category}, 'reason': REASON}, status=201)
receipts[key] = created['id']
receipt_path.write_text(json.dumps(receipts, indent=2), encoding='utf-8')
ident = receipts[key]
current = page(session, ident)
mapping = {}
for kind in ['locations', 'documents']:
for source_row in source['canonicalOptions']['resources'][kind]:
if source_row['id'] not in original[kind]:
continue
title = 'Synthetic QA modular copy ' + source_row['id']
row = next((r for r in current['canonicalOptions']['resources'][kind] if r['payload'].get('title') == title), None)
if row is None:
meta = {**source_row['payload'], 'title': title}
body = {'etag': page(session, ident)['etag'], 'creation_key': str(uuid.uuid4()), 'reason': REASON, 'payload': meta}
url = f'/minhas-oportunidades/{ident}/resources/{kind}'
if kind == 'documents':
raw = session.call('GET', f'/minhas-oportunidades/33/resources/documents/{source_row["id"]}/download', binary=True)
body['payload'] = json.dumps(meta)
session.call('POST', url, body, multipart=('qa-modular-original.png', 'image/png', raw), status=201)
else:
session.call('POST', url, body, status=201)
current = page(session, ident)
row = next(r for r in current['canonicalOptions']['resources'][kind] if r['payload'].get('title') == title)
mapping[source_row['id']] = row['id']
def replace(value):
if isinstance(value, dict): return {k: replace(v) for k, v in value.items()}
if isinstance(value, list): return [replace(v) for v in value]
return mapping.get(value, value) if isinstance(value, str) else value
payload = replace(payload)
document = payload['documents'][0]
payload['official_title'] = 'Synthetic QA modular ' + str(ident)
if category == 'business_sale':
payload['module_profile_version'] = current['canonicalOptions']['historyVersion']
payload['business_transaction'].update({'history_status': 'no_completed_years', 'available_financial_years': 0, 'financial_periods': [],
'history_declaration': 'Entidade sintética QA recém-criada sem exercícios completos.', 'history_declaration_document_id': document,
'projections': {'scenario': 'Projeção sintética QA', 'source': 'Plano de teste QA', 'date': '2026-10-10', 'document_id': document}})
return ident, payload, document
payload['module_profile_version'] = current['canonicalOptions']['modularProfiles'][category]
m = {'mode': mode, 'object': 'Objeto sintético QA, sem compromisso real', 'object_document_id': document, 'representation_document_id': document}
if category == 'concession':
m.update({'granting_entity': 'Entidade sintética QA, não autoridade real', 'procedure_regime': 'Procedimento de teste QA',
'scope': 'Distrito de localização QA', 'term_value': 12, 'term_unit': 'months', 'remuneration_obligations': 'Obrigações sintéticas QA',
'participation_conditions': 'Condições sintéticas QA', 'land_required': False, 'construction_required': False,
'disclosure_authorization_document_id': document})
if mode == 'awarded_right': m['external_decision_document_id'] = document
if category in ['financing', 'supply']:
m.update({'conditions_status': 'proposed', 'terms': 'Condições indicativas QA, sem compromisso', 'scenario': 'Cenário QA', 'source': 'Fonte sintética QA', 'terms_date': '2026-10-10'})
if category == 'financing':
m.update({'instrument': 'debt', 'term_value': 12, 'term_unit': 'months', 'repayment_periodicity': 'mensal', 'disbursement_calendar': 'Calendário indicativo QA'})
if mode == 'seeking_funding':
payload.update({'total_investment': 1000, 'investment_breakdown': [{'label': 'QA', 'amount': 1000, 'currency': 'MZN'}], 'secured_financing': [], 'funding_sought': 500})
m.update({'use_of_funds': 'Preparação sintética QA', 'financial_plan_document_id': document})
else:
m.update({'offered_capacity': 100000, 'eligibility_criteria': 'Critérios indicativos QA', 'capacity_document_id': document})
if category == 'supply':
m.update({'quantity': 10, 'unit': 'unidade', 'destination': 'Distrito QA', 'start_date': '2026-10-11', 'end_date': '2026-11-11',
'acceptance_criteria': 'Critérios sintéticos QA', 'unit_price': 10, 'total_price': 100, 'advance_or_guarantee': False})
if mode == 'supply_offer': m['capacity_document_id'] = document
if category == 'other':
m.update({'subtype': 'qa-test-subtype', 'purpose': 'Teste QA', 'recipients': 'Destinatários sintéticos QA', 'expected_output': 'Resultado QA', 'risks': 'Riscos declarados QA'})
payload['module_details'] = m
return ident, payload, document
try:
owner = Session('promoter').login()
reviewer = Session('validator').login()
publisher = Session('publisher').login()
outsider = Session('outsider').login()
cases = [('concession', 'intention_call'), ('concession', 'awarded_right'), ('financing', 'seeking_funding'), ('financing', 'funding_offer'),
('supply', 'purchase_request'), ('supply', 'supply_offer'), ('other', 'classified_other')]
if '--history-only' in sys.argv:
cases = [('business_sale', 'no_completed_years')]
for category, mode in cases:
# Every case uses normal fresh MFA; long suites must not outlive the 15-minute security window.
owner = Session('promoter').login()
reviewer = Session('validator').login()
publisher = Session('publisher').login()
ident, payload, document = prepare(owner, category, mode)
current = page(owner, ident)
initial_state = current['workflowState']
if current['workflowState'] == 'draft':
invalid = copy.deepcopy(payload)
field = None
if mode == 'seeking_funding': invalid['funding_sought'] = 2000; field = 'canonical.funding_sought'
if mode == 'funding_offer': invalid['total_investment'] = 100; field = 'canonical.total_investment'
if mode == 'awarded_right': invalid['module_details']['external_decision_document_id'] = None; field = 'canonical.module_details.external_decision_document_id'
if mode == 'supply_offer': invalid['module_details']['total_price'] = 99; field = 'canonical.module_details.total_price'
if category == 'other': invalid['module_details']['subtype'] = 'unclassified-idea'; field = 'canonical.module_details.subtype'
if field:
save(owner, ident, invalid)
assert page(Session('promoter').login(), ident)['values']['canonical']['module_details'] == invalid['module_details']
failure = action(owner, ident, 'submit', status=422)
assert field in failure['errors']
# Clear fields left by a failed offer test explicitly; the merge never silently erases values.
if mode == 'funding_offer': payload['total_investment'] = None
save(owner, ident, payload)
action(owner, ident, 'submit')
current = page(owner, ident)
if current['workflowState'] == 'submitted' and not current['profileContext'].get('correction_cycles'):
action(reviewer, ident, 'request_correction', correction_items=['canonical.executive_summary'])
current = page(owner, ident)
if current['workflowState'] == 'needs_information':
payload = current['values']['canonical']
payload['executive_summary'] += ' Correção QA preservada.'
save(owner, ident, payload)
action(owner, ident, 'submit')
current = page(owner, ident)
if current['workflowState'] in ['submitted', 'in_review']:
for gate in current['canonicalOptions']['gates']:
action(reviewer, ident, 'review_gate', gate=gate, gate_decision='approved', evidence_document_ids=[document])
action(owner, ident, 'approve', status=403)
action(reviewer, ident, 'approve')
if page(owner, ident)['workflowState'] == 'approved': action(publisher, ident, 'publish')
recovered = Session('promoter').login().call('GET', f'/minhas-oportunidades/{ident}/response')
if category == 'business_sale':
assert recovered['data']['business_transaction']['history_status'] == mode
assert recovered['data']['business_transaction']['financial_periods'] == []
else:
assert recovered['data']['module_details']['mode'] == mode
assert recovered['data']['status'] == 'published' and recovered['publication']['scope'] == 'restricted'
outsider.call('GET', f'/minhas-oportunidades/{ident}/response', status=403)
Session('outsider').call('GET', f'/minhas-oportunidades/{ident}/response', status=401)
raw = owner.call('GET', f'/minhas-oportunidades/{ident}/resources/documents/{document}/download', binary=True)
resource = next(d for d in recovered['data']['documents'] if d['id'] == document)
assert resource['scan_recorded_status'] == 'clean' and resource['sha256'].strip() == hashlib.sha256(raw).hexdigest()
proof['cases'].append({'id': ident, 'category': category, 'mode': mode, 'profile_version': recovered['data']['module_profile_version'],
'workflow': 'draft → submit → correction → resubmit → independent gates → approval → restricted release',
'new_mfa_recovery': True, 'same_opportunity_verified_original': True, 'other_organization': 403, 'anonymous': 401,
'public_catalog': False, 'initial_state_in_this_attempt': initial_state,
'subtype_scope': 'synthetic QA acceptance only' if category == 'other' else None})
print(json.dumps({'case': ident, 'category': category, 'mode': mode, 'result': 'passed', 'interface_observed': False}), flush=True)
proof['result'] = 'passed'
except Exception as error:
proof.update({'result': 'failed', 'failure': str(error)})
raise
finally:
proof['events'] = events
proof['completed_at'] = datetime.now(timezone.utc).isoformat()
dest = PRIVATE / ('opportunity-history-http-qa.json' if '--history-only' in sys.argv else 'opportunity-modular-http-qa.json')
if dest.exists():
previous = json.loads(dest.read_text(encoding='utf-8-sig'))
archive = PRIVATE / ('opportunity-modular-http-qa-' + previous['completed_at'].replace(':', '').replace('+', '_') + '.json')
if not archive.exists(): archive.write_bytes(dest.read_bytes())
dest.write_text(json.dumps(proof, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({'result': proof['result'], 'cases': len(proof['cases']), 'events': len(events), 'interface_observed': False, 'active_writes': 0}))
+49 -13
View File
@@ -1,13 +1,21 @@
<?php
use App\Http\Middleware\EnsurePortalProfiles;
use App\Models\User;
use App\Services\OpportunityResourceInstallation;
use App\Services\PortalProfileAccess;
use App\Services\PortalSuperusers;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Http\Request;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Vite;
use Inertia\Inertia;
use Laravel\Fortify\Features;
use Laravel\Fortify\Fortify;
$root = dirname(__DIR__);
require_once $root.'/tools/portal-qa-environment.php';
@@ -23,19 +31,28 @@ $app = require $root.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
config(['investor-path.enabled' => true]);
config(['geometry-import.enabled' => true]);
config(['portal.interface_language' => 'pt-MZ']);
$identity = portalQaAssertTarget($target);
if (PHP_SAPI !== 'cli' && is_file($dir.'/portal-audit/qa-recovery-maintenance.json')) {
http_response_code(503);
header('Retry-After: 60');
header('Cache-Control: no-store');
header('X-Investmoz-QA-Target: '.$identity->database.':'.$identity->port);
echo 'QA temporariamente em recuperação técnica. Nenhuma alteração no ambiente ativo.';
return;
}
config(['portal.qa_synthetic_accounts_only' => true]);
require_once $root.'/tools/portal-qa-account-selection.php';
// Only expose the non-secret identity; passwords, TOTP secrets and recovery codes remain private.
\Laravel\Fortify\Fortify::loginView(fn (Request $request) => \Inertia\Inertia::render('auth/login', [
'canResetPassword' => \Laravel\Fortify\Features::enabled(\Laravel\Fortify\Features::resetPasswords()),
Fortify::loginView(fn (Request $request) => Inertia::render('auth/login', [
'canResetPassword' => Features::enabled(Features::resetPasswords()),
'status' => $request->session()->get('status'),
'qaAccount' => ['id' => 18, 'email' => User::findOrFail(18)->email],
]));
\Laravel\Fortify\Fortify::twoFactorChallengeView(function (Request $request) {
Fortify::twoFactorChallengeView(function (Request $request) {
$actor = User::findOrFail($request->session()->get('login.id'));
return \Inertia\Inertia::render('auth/two-factor-challenge', ['qaAccount' => ['id' => $actor->id, 'email' => $actor->email]]);
return Inertia::render('auth/two-factor-challenge', ['qaAccount' => ['id' => $actor->id, 'email' => $actor->email]]);
});
config(['portal.managers' => ['opportunity-manager@example.invalid', 'geo-manager-qa@example.invalid'], 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]);
$sessions = $dir.'/qa-opportunity-resources-sessions';
@@ -45,33 +62,52 @@ if (! is_dir($sessions)) {
config(['session.files' => $sessions]);
config(['portal.opportunity_dashboard_access_enabled' => true, 'portal.qa_build_directory' => 'build-qa-opportunities']);
config(['portal.opportunity_specialized_drafts_enabled' => true]);
config(['portal.opportunity_modular_profiles_qa_enabled' => true]);
// Explicit synthetic acceptance subtype, never an approval/classification of a real opportunity.
config(['portal.opportunity_other_qa_subtypes.qa-test-subtype' => ['approved' => true, 'version' => 'synthetic-acceptance-v1',
'decision_reference' => 'Synthetic QA acceptance fixture only; real subtypes require functional approval',
'gates' => ['technical_review', 'legal_territorial_review']]]);
config(['portal.profile_access_enabled' => true]);
config(['portal.legacy_content_qa_enabled' => true]);
// QA-only bootstrap ceremony: fixed synthetic identities, normal authenticated MFA.
Illuminate\Support\Facades\Route::middleware(['web', 'auth', 'verified', \App\Http\Middleware\EnsurePortalProfiles::class])->post('/qa-auditoria/perfis/inicializar', function (Request $request) use ($dir) {
Route::middleware(['web', 'auth', 'verified', EnsurePortalProfiles::class])->post('/qa-auditoria/perfis/inicializar', function (Request $request) use ($dir) {
$actors = json_decode(file_get_contents($dir.'/portal-audit/profiles-v1-actors-private.json'), true, 512, JSON_THROW_ON_ERROR);
abort_unless($request->user()->id === $actors['bootstrap']['id'], 403);
config(['portal_profiles.bootstrap_authorized' => true]);
try {
app(\App\Services\PortalSuperusers::class)->initialize(User::findOrFail($actors['super_first']['id']), User::findOrFail($actors['super_second']['id']), $request->user());
app(PortalSuperusers::class)->initialize(User::findOrFail($actors['super_first']['id']), User::findOrFail($actors['super_second']['id']), $request->user());
} finally {
config(['portal_profiles.bootstrap_authorized' => false]);
}
return response()->json(['initialized' => true, 'holders' => [$actors['super_first']['id'], $actors['super_second']['id']]]);
});
// Read-only report derived from the sole audit matrix, accessible only on QA.
function authorizeQaAuditReader(Request $request): void
{
$access = app(\App\Services\PortalProfileAccess::class);
abort_unless($request->user()->can('manage-portal') || $access->has($request->user(), 'auditor') || ($access->superuser($request->user()) && $access->mfa($request->user())), 403);
$access = app(PortalProfileAccess::class);
$allowed = $request->user()->can('manage-portal') || $access->has($request->user(), 'auditor') || ($access->superuser($request->user()) && $access->mfa($request->user()));
if (! $allowed && ! $request->expectsJson()) {
if ($request->headers->has('X-Inertia')) {
// Render the refused destination as a full page instead of trapping login in an error modal.
abort(Inertia::location($request->url()));
}
// Refuse the administrative report without discarding an authenticated investor session.
// Keep a usable destination when Fortify returns to a previously requested report.
abort(response('<!doctype html><html lang="pt-MZ"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Acesso reservado — InvestMoz QA</title></head><body style="font:16px/1.6 system-ui;max-width:44rem;margin:auto;padding:24px"><main><h1>Relatório reservado à gestão e auditoria</h1><p>A sua sessão está autenticada. Este perfil não tem autorização para consultar o relatório administrativo.</p><p><a href="/dashboard">Abrir o painel</a></p><p><a href="/investimentos">Continuar os meus investimentos</a></p></main></body></html>', 403)->header('Cache-Control', 'private, no-store'));
}
abort_unless($allowed, 403);
}
Illuminate\Support\Facades\Route::middleware(['web', 'auth', 'verified'])->get('/qa-auditoria/pedidos-abertos', function (Request $request) use ($dir) {
Route::middleware(['web', 'auth', 'verified'])->get('/qa-auditoria/pedidos-abertos', function (Request $request) use ($dir) {
authorizeQaAuditReader($request);
return response()->file($dir.'/portal-audit/pedidos-abertos.html', ['Cache-Control' => 'private, no-store']);
});
foreach (['relatorio-executivo', 'backlog', 'perfis-v1-reconciliacao', 'roteiro-manual-perfis-v1'] as $reportName) {
Illuminate\Support\Facades\Route::middleware(['web', 'auth', 'verified'])->get('/qa-auditoria/'.$reportName, function (Request $request) use ($root, $reportName) {
foreach (['relatorio-executivo', 'backlog', 'perfis-v1-reconciliacao', 'roteiro-manual-perfis-v1', 'legado-piloto-qa'] as $reportName) {
Route::middleware(['web', 'auth', 'verified'])->get('/qa-auditoria/'.$reportName, function (Request $request) use ($root, $reportName) {
authorizeQaAuditReader($request);
$content = file_get_contents($root.'/docs/investmoz/v2/auditoria-portal/'.$reportName.'.md');
return response('<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>InvestMoz — '.$reportName.'</title><style>body{font:16px/1.55 system-ui;max-width:1100px;margin:auto;padding:24px}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit}</style><pre>'.htmlspecialchars($content, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8').'</pre></html>')->header('Cache-Control', 'private, no-store');
});
}
@@ -145,10 +181,10 @@ header('X-Investmoz-QA-Target: '.$identity->database.':'.$identity->port);
header('X-Investmoz-QA-Key-Fingerprint: '.hash('sha256', app('encrypter')->getKey()));
header('X-Investmoz-QA-Config-Cache: bypassed');
require_once $root.'/tools/portal-qa-auth-trace.php';
$httpKernel = $app->make(\Illuminate\Contracts\Http\Kernel::class);
$httpKernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$httpKernel->prependMiddleware(PortalQaAuthTrace::class);
$httpKernel->appendMiddlewareToGroup('web', PortalQaSessionTrace::class);
$httpKernel->addToMiddlewarePriorityAfter(\Illuminate\Session\Middleware\StartSession::class, PortalQaSessionTrace::class);
$httpKernel->addToMiddlewarePriorityAfter(StartSession::class, PortalQaSessionTrace::class);
$httpKernel->appendMiddlewareToGroup('web', PortalQaAccountSelection::class);
$httpKernel->addToMiddlewarePriorityAfter(PortalQaSessionTrace::class, PortalQaAccountSelection::class);
$app->handleRequest(Request::capture());
+139
View File
@@ -0,0 +1,139 @@
"""Read adapter and §27 semantics on QA18097. No credentials, cookies or MFA output."""
import copy
import json
import uuid
import sys
from datetime import datetime, timezone
from portal_profiles_http_session import Session, PRIVATE, EXPECTED, REASON, events
receipt_path = PRIVATE / 'opportunity-response-receipts-qa.json'
receipts = json.loads(receipt_path.read_text(encoding='utf-8-sig')) if receipt_path.exists() else {}
proof = {'target': EXPECTED, 'started_at': datetime.now(timezone.utc).isoformat(), 'active_writes': 0, 'interface_observed': False, 'responses': [], 'semantics': []}
resume_semantics = '--semantics-only' in sys.argv
if resume_semantics:
previous = json.loads((PRIVATE / 'opportunity-response-http-qa.json').read_text(encoding='utf-8-sig'))
assert len(previous['responses']) == 10 and previous['active_writes'] == 0
proof['responses'] = previous['responses']
proof['response_read_scope_from_attempt_at'] = previous['completed_at']
proof['response_scope_note'] = 'Ten read/recovery criteria passed in the preserved preceding attempt; its subsequent harness correction failed. New semantic receipts below are separate.'
def save_receipts():
receipt_path.write_text(json.dumps(receipts, indent=2), encoding='utf-8')
def page(session, ident):
return session.call('GET', f'/minhas-oportunidades/{ident}/view', inertia=True)['props']
def persist(session, ident, payload):
current = page(session, ident)
return session.call('PUT', f'/minhas-oportunidades/{ident}/draft', {'etag': current['etag'], 'canonical': payload, 'reason': REASON})
def submit(session, ident, status=200):
current = page(session, ident)
return session.call('POST', f'/minhas-oportunidades/{ident}/workflow', {'etag': current['etag'], 'action': 'submit', 'reason': REASON}, status=status)
def clone(session, source_id, key):
source = page(session, source_id)
payload = copy.deepcopy(source['values']['canonical'])
if key not in receipts:
row = session.call('POST', '/minhas-oportunidades/drafts', {'creation_key': str(uuid.uuid4()), 'organization_id': source['profileOrganization'],
'opportunity_category': payload['opportunity_category'], 'reason': REASON}, status=201)
receipts[key] = row['id']; save_receipts()
ident = receipts[key]
existing = page(session, ident)
mapping = {}
for kind in ['locations', 'documents']:
for source_row in source['canonicalOptions']['resources'][kind]:
if source_row['id'] not in payload[kind]:
continue
row = next((r for r in existing['canonicalOptions']['resources'][kind] if r['payload'].get('title') == 'QA response copy ' + source_row['id']), None)
if row is None:
resource_payload = {**source_row['payload'], 'title': 'QA response copy ' + source_row['id']}
body = {'etag': page(session, ident)['etag'], 'creation_key': str(uuid.uuid4()), 'reason': REASON, 'payload': resource_payload}
url = f'/minhas-oportunidades/{ident}/resources/{kind}'
if kind == 'documents':
raw = session.call('GET', f'/minhas-oportunidades/{source_id}/resources/documents/{source_row["id"]}/download', binary=True)
body['payload'] = json.dumps(resource_payload)
session.call('POST', url, body, multipart=('qa-response-original.png', 'image/png', raw), status=201)
else:
session.call('POST', url, body, status=201)
existing = page(session, ident)
row = next(r for r in existing['canonicalOptions']['resources'][kind] if r['payload'].get('title') == resource_payload['title'])
mapping[source_row['id']] = row['id']
def replace(value):
if isinstance(value, dict):
return {k: replace(v) for k, v in value.items()}
if isinstance(value, list):
return [replace(v) for v in value]
return mapping.get(value, value) if isinstance(value, str) else value
payload = replace(payload)
payload['official_title'] = 'Synthetic QA response semantics ' + str(ident)
if 'land_rights' in payload:
payload['land_rights']['duat_number'] = 'QA-RESPONSE-' + str(ident)
return ident, payload
try:
owner = Session('promoter').login()
reviewer = Session('validator').login()
outsider = Session('outsider').login()
fresh = Session('promoter').login()
for ident in ([] if resume_semantics else range(30, 40)):
current = page(owner, ident)
original = current['values']['canonical']
response = owner.call('GET', f'/minhas-oportunidades/{ident}/response')
assert response == current['canonicalResponse']
assert response['data']['id'] == ident
assert not response['compatibility']['normative_response_complete']
assert not response['validation_result']['revalidated_on_read']
assert 'completion_percentage' not in response['data']
assert response['resource_references'] == {k: original.get(k, []) for k in ['documents', 'images', 'locations']}
assert response == fresh.call('GET', f'/minhas-oportunidades/{ident}/response')
for kind in ['documents', 'images', 'locations']:
assert [r['id'] for r in response['data'][kind]] == original.get(kind, [])
proof['responses'].append({'id': ident, 'adapter_version': response['adapter_version'], 'ids_preserved': True, 'new_mfa_recovery': True,
'inertia_consumer_matches_endpoint': True, 'validation_not_invented': True, 'normative_response_complete': False})
reviewer.call('GET', '/minhas-oportunidades/33/response')
outsider.call('GET', '/minhas-oportunidades/33/response', status=403)
Session('outsider').call('GET', '/minhas-oportunidades/33/response', status=401)
land_id, land = clone(owner, 32, 'land_duplicate')
current = page(owner, land_id)
if current['workflowState'] == 'draft':
land['land_rights']['parcels'] = [copy.deepcopy(land['land_rights']['parcels'][0]) for _ in range(2)]
for parcel in land['land_rights']['parcels']:
parcel['cadastral_number'] = 'QA-DUPLICATE-' + str(land_id)
persist(owner, land_id, land)
failure = submit(owner, land_id, status=422)
assert 'canonical.land_rights.parcels.1.cadastral_number' in failure['errors']
recovered = page(Session('promoter').login(), land_id)
assert recovered['values']['canonical']['land_rights']['parcels'] == land['land_rights']['parcels']
land['land_rights']['parcels'] = copy.deepcopy(land['land_rights']['parcels'])
land['land_rights']['parcels'][1]['cadastral_number'] += '-SECOND'
persist(owner, land_id, land)
submit(owner, land_id)
proof['semantics'].append({'id': land_id, 'rule': '§27 cadastral number unique within opportunity', 'invalid_submission': 422,
'field': 'canonical.land_rights.parcels.1.cadastral_number', 'invalid_draft_recovered': True, 'corrected_submission': 'submitted'})
business_id, business = clone(owner, 33, 'succession')
if page(owner, business_id)['workflowState'] == 'draft':
business['visibility'] = 'internal'
business['images'] = []
business['business_transaction']['transaction_type'] = 'succession'
persist(owner, business_id, business)
submit(owner, business_id)
response = Session('promoter').login().call('GET', f'/minhas-oportunidades/{business_id}/response')
assert response['data']['business_transaction']['transaction_type'] == 'succession'
assert response['data']['status'] == 'submitted'
proof['semantics'].append({'id': business_id, 'rule': 'original business_sale succession modality', 'saved': True, 'submitted': True, 'new_mfa_recovery': True})
proof['result'] = 'passed'
except Exception as error:
proof.update({'result': 'failed', 'failure': str(error)})
raise
finally:
proof['events'] = events
proof['receipts'] = receipts
proof['completed_at'] = datetime.now(timezone.utc).isoformat()
dest = PRIVATE / 'opportunity-response-http-qa.json'
if dest.exists():
dest.rename(PRIVATE / ('opportunity-response-http-qa-' + datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S') + '.json'))
dest.write_text(json.dumps(proof, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({'result': proof['result'], 'responses': len(proof['responses']), 'semantics': len(proof['semantics']), 'events': len(events), 'active_writes': 0}))
+15
View File
@@ -0,0 +1,15 @@
"""Run only the affected guarded dossier/Balcao regression; write sanitized test-reporter output."""
import json
import subprocess
import sys
from pathlib import Path
root = Path(__file__).resolve().parent.parent
out = root / 'storage/app/private/investmoz-v2/portal-audit'
result = subprocess.run(['C:/Users/macbook/.config/investmoz/php-8.5.11/php.exe', 'vendor/bin/phpunit',
'tests/Feature/InvestorPathTest.php', '--no-progress'], cwd=root, capture_output=True)
receipt = json.loads(result.stdout.decode('utf-8-sig'))
(out / 'balcao-prefill-after-fix-qa.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({'exit': result.returncode, 'tests': receipt['tests'], 'assertions': receipt['assertions'],
'failures': receipt.get('failures', [])}, ensure_ascii=True))
sys.exit(result.returncode)
@@ -0,0 +1,73 @@
<?php
// Guarded read-only trace of the existing, explicitly synthetic A/B dossiers. No credentials or ciphertexts in evidence.
use App\Models\SimulationFormDraft;
use App\Services\VirtualDesk;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
require dirname(__DIR__).'/vendor/autoload.php';
require __DIR__.'/portal-qa-environment.php';
$target = portalQaEnvironment();
$app = require dirname(__DIR__).'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
portalQaAssertTarget($target);
DB::beginTransaction();
DB::statement('SET TRANSACTION READ ONLY');
$dir = storage_path('app/private/investmoz-v2/portal-audit');
$prior = json_decode(file_get_contents($dir.'/investor-ab-chromium-20261010.json'), true, 512, JSON_THROW_ON_ERROR);
$proof = ['captured_at' => gmdate('c'), 'target' => $target, 'read_only' => true, 'active_writes' => 0, 'scenarios' => []];
foreach ($prior['scenarios'] as $name => $entry) {
$case = DB::table('desk_cases')->where('id', $entry['id'])->firstOrFail();
abort_unless((int) $case->user_id === 18 && $case->simulation_id === $entry['draft_id'], 403);
$desk = app(VirtualDesk::class);
$payload = $desk->decode($case->payload);
$path = $payload['investor_path'];
$result = collect($path['results'])->firstWhere('id', $entry['result_id']);
abort_unless($result && $result['draft_id'] === $case->simulation_id, 422);
$draft = SimulationFormDraft::findOrFail($entry['draft_id']);
abort_unless((int) $draft->owner_user_id === 18, 403);
$economic = SimulationFormDraft::decryptSnapshot($draft->economic_snapshot);
$request = DB::table('desk_requests')->where('id', $entry['request_id'])->where('case_id', $case->id)->firstOrFail();
$requestPayload = $desk->decode($request->payload);
$task = DB::table('tasks')->where('id', $entry['task_id'])->where('desk_case_id', $case->id)->firstOrFail();
abort_unless($task->guidance_key === 'path:S02' && $task->status === 'done', 422);
$steps = DB::table('journey_steps')->where('journey_id', $case->journey_id)->whereIn('step_code', ['S02', 'S10'])->get();
$stepProof = [];
foreach ($steps as $step) {
$detail = $desk->decode($step->path_detail);
$stepProof[$step->step_code] = ['state' => $detail['state'], 'recorded_at' => $detail['recorded_at'] ?? null, 'scope' => $detail['scope'] ?? null];
}
$timeline = [];
foreach (DB::table('desk_events')->where('case_id', $case->id)->orderBy('event_order')->get() as $event) {
$data = $desk->decode($event->payload);
if (($event->action === 'path_stage_recorded' && in_array($data['code'] ?? '', ['S02', 'S10'], true))
|| (in_array($event->action, ['path_task_accepted', 'guidance_task_updated'], true) && ($data['task_id'] ?? '') === $task->id)) {
$timeline[] = ['order' => $event->event_order, 'action' => $event->action, 'actor_id' => $event->actor_id,
'created_at' => $event->created_at, 'code' => $data['code'] ?? null, 'state' => $data['state'] ?? null,
'task_status' => $data['status'] ?? null, 'task_id' => $data['task_id'] ?? null];
}
}
if ($name === 'B') {
$wait = collect($timeline)->first(fn ($e) => $e['code'] === 'S10' && $e['state'] === 'waiting_external');
$done = collect($timeline)->first(fn ($e) => $e['task_status'] === 'done');
abort_unless($wait && $done && $wait['order'] < $done['order'] && $stepProof['S10']['state'] === 'waiting_external', 422);
}
$proof['scenarios'][$name] = ['case_id' => $case->id, 'case_version' => $case->version, 'owner_id' => $case->user_id,
'facts' => $path['facts'], 'steps' => $stepProof, 'payload_sha256' => hash('sha256', $case->payload),
'draft_id' => $draft->id, 'draft_version' => $draft->version_no, 'draft_snapshot_sha256' => $draft->snapshot_sha256,
'project_title' => $economic['project_title'] ?? null, 'result_id' => $result['id'], 'result_draft_id' => $result['draft_id'],
'tool_summary_length' => strlen($economic['project_summary'] ?? ''), 'request_description_length' => strlen($requestPayload['description'] ?? ''),
'request_payload_fields' => array_keys($requestPayload), 'request_created_at' => $request->created_at,
'result_draft_version' => $result['draft_version'], 'result_source_key' => $result['source_key'],
'request_id' => $request->id, 'request_status' => $request->status, 'request_version' => $request->version,
'request_country' => $requestPayload['country'] ?? null, 'request_origin' => $requestPayload['declared_origin_country_code'] ?? null,
'task_id' => $task->id, 'task_status' => $task->status, 'task_guidance_key' => $task->guidance_key,
'task_version' => $task->version_no, 'timeline' => $timeline];
}
DB::rollBack();
$proof['result'] = 'passed_read_only_existing_path';
$file = $dir.'/investor-ab-remaining-persistence-qa.json';
if (is_file($file)) copy($file, $dir.'/investor-ab-remaining-persistence-qa-'.gmdate('Ymd_His').'.json');
file_put_contents($file, json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
echo json_encode(['result' => $proof['result'], 'target' => $target, 'read_only' => true, 'scenarios' => array_keys($proof['scenarios'])]).PHP_EOL;
+175
View File
@@ -0,0 +1,175 @@
// Remaining A/B acceptance only: reuse the demonstrated dossiers, do not repeat C37 or create fixtures.
const {start, BASE, DIR, fs, path, assert, sanitized} = require('./portal-visual-qa-lib.cjs');
(async () => {
const q = await start('investor-ab-remaining-chromium-20261010');
const previous = JSON.parse(fs.readFileSync(path.join(DIR, 'investor-ab-chromium-20261010.json'), 'utf8'));
assert.equal(previous.result, 'passed');
q.proof.previous_full_path_receipt = 'investor-ab-chromium-20261010.json';
q.proof.scope = 'Remaining visual A/B criteria, existing dossiers. Authentication is a prerequisite; C37 acceptance is not repeated.';
q.proof.scenarios = {};
q.proof.accessibility = [];
let page, ctx;
async function regionShot(locator, name) {
await locator.scrollIntoViewIfNeeded();
const file = path.join(DIR, 'investor-ab-remaining-' + q.stamp + '-' + name + '.png');
await locator.screenshot({path:file});
q.proof.screenshots.push(path.relative(path.resolve(__dirname, '..'), file).replaceAll('\\', '/'));
q.save();
}
async function audit(route, width) {
await page.addScriptTag({path:path.join(DIR, 'qa-a11y-toolkit/package/axe.min.js')});
const result = await page.evaluate(async () => {
const r = await axe.run(document, {runOnly:{type:'tag', values:['wcag2a','wcag2aa','wcag21a','wcag21aa','best-practice']}});
return {violations:r.violations.map(v => ({id:v.id, impact:v.impact, targets:v.nodes.map(n => n.target)})),
incomplete:r.incomplete.map(v => v.id), viewport:innerWidth, scrollWidth:document.documentElement.scrollWidth};
});
q.proof.accessibility.push({route, width, ...result});
q.save();
assert(result.scrollWidth <= width + 1, 'Horizontal overflow on the demonstrated A/B page');
assert.equal(result.violations.length, 0, 'Accessibility findings need separate correction');
}
try {
q.phase('Remaining A/B criteria in fresh Chromium session, 390px');
({page, ctx} = await q.context('investor18-remaining-ab', 390));
await q.login(page, 'investor18');
for (const name of ['A', 'B']) {
const entry = previous.scenarios[name];
q.phase('Scenario ' + name + ': applicability, correct financial result, existing Balcao preparation');
await q.open(page, '/investimentos/' + entry.id);
await page.getByRole('heading', {name:entry.title, exact:true}).waitFor();
const data = (await q.json(page, '/investimentos/' + entry.id)).data;
assert.equal(data.case.id, entry.id);
assert.equal(await page.locator('article[id^="S"]').count(), 19);
assert.equal(await page.getByRole('heading', {name:/^Fase \d+ ·/}).count(), 5);
const decisions = page.locator('form').filter({has:page.getByRole('heading', {name:'Decisões do promotor e aplicabilidade', exact:true})});
const expected = name === 'A' ? 'no' : 'yes';
assert.equal(await decisions.getByLabel(/^Precisa de terra/).inputValue(), expected);
assert.equal(await decisions.getByLabel(/^Precisa de financiamento externo/).inputValue(), expected);
const land = data.steps.find(s => s.code === 'S10');
assert.equal(land.state, name === 'A' ? 'dispensed' : 'waiting_external');
assert.equal(land.preparation_allowed, name === 'B');
await page.locator('#S10').getByText(name === 'A' ? /Dispensa apenas de preparação deste ramo/ : /Aguarda informação do terreno QA/).waitFor();
await regionShot(decisions, name + '-applicability-390');
await regionShot(page.locator('#S10'), name + '-land-390');
const financial = page.locator('section').filter({has:page.getByRole('heading', {name:'Resultados devolvidos ao dossier', exact:true})});
const result = data.results.find(r => r.id === entry.result_id);
assert(result && result.draft_id === entry.draft_id);
const other = previous.scenarios[name === 'A' ? 'B' : 'A'];
assert(!data.results.some(r => r.id === other.result_id || r.draft_id === other.draft_id), 'No financial result from the other dossier');
// Preserve older result versions; assert the exact previously demonstrated evidence, not an arbitrary first row.
const financialEntry = financial.locator('li').filter({hasText:entry.result_id});
assert.equal(await financialEntry.count(), 1);
await financialEntry.getByText('Ponto de equilíbrio: 20 unidades', {exact:true}).waitFor();
await financialEntry.getByText(new RegExp('^Necessidade de financiamento: ' + entry.expected_funding_gap + '(?:\\.00)?$')).waitFor();
await regionShot(financial, name + '-financial-390');
const tasks = page.locator('section').filter({has:page.getByRole('heading', {name:'Acompanhamento interno', exact:true})});
const savedTask = data.tasks.find(t => t.id === entry.task_id);
assert.equal(savedTask.status, 'done');
const item = tasks.locator('li').filter({hasText:savedTask.title});
await item.getByText(/done/).waitFor();
if (name === 'B') {
const independent = data.steps.find(s => s.code === 'S02');
assert.equal(independent.saved_state, 'preparing');
assert.equal(independent.preparation_allowed, true);
const stage = page.locator('#S02');
await stage.locator('summary').click();
assert.equal(await stage.getByRole('button', {name:'Guardar registo', exact:true}).isEnabled(), true);
assert.equal(await tasks.getByLabel(/^Etapa/).locator('option[value="S02"]').count(), 1);
await regionShot(stage, 'B-independent-stage-enabled-390');
}
await regionShot(item, name + '-internal-completion-390');
await audit('/investimentos/' + entry.id, 390);
const support = page.locator('section').filter({has:page.getByRole('heading', {name:'Apoio no Balcão', exact:true})});
const request = data.requests.find(r => r.id === entry.request_id);
assert.equal(request.status, 'draft');
assert.equal(request.protocol, null);
await support.locator('a[href="/balcao-virtual/pedidos/' + entry.request_id + '"]').click();
await page.waitForURL(BASE + '/balcao-virtual/pedidos/' + entry.request_id);
await page.getByRole('heading', {name:'Rascunho — ainda não submetido', exact:true}).waitFor();
assert.equal(await page.locator('#request-country').inputValue(), 'PT');
assert.equal(await page.locator('#declared-origin-country').inputValue(), 'ZA');
await page.getByRole('link', {name:'Voltar ao dossier ' + entry.title, exact:true}).waitFor();
const prepared = (await q.json(page, '/balcao-virtual/pedidos/' + entry.request_id)).data;
if (!prepared.request.payload.description && name === 'A') {
const source = prepared.toolPrefill;
assert(source && source.draft_id === entry.draft_id);
const reuse = page.getByRole('region', {name:'Descrição guardada no projeto'});
await reuse.getByText(source.description, {exact:true}).waitFor();
await regionShot(reuse, 'A-C88-explicit-reuse-preview');
const saved = page.waitForResponse(r => r.request().method() === 'PUT' && new URL(r.url()).pathname === '/balcao-virtual/pedidos/' + entry.request_id);
await reuse.getByRole('button', {name:'Reutilizar descrição guardada e guardar rascunho', exact:true}).click();
assert([200,302,303].includes((await saved).status()));
await page.reload({waitUntil:'domcontentloaded'});
const recovered = (await q.json(page, '/balcao-virtual/pedidos/' + entry.request_id)).data;
assert.equal(recovered.request.payload.description, source.description);
assert.equal(recovered.request.version, prepared.request.version + 1);
assert.equal(recovered.request.status, 'draft');
assert.equal(recovered.request.payload.country, 'PT');
assert.equal(recovered.request.payload.declared_origin_country_code, 'ZA');
assert(recovered.revisions.some(r => Number(r.version) === Number(prepared.request.version) && r.snapshot.payload.description === ''));
assert(recovered.events.some(e => e.action === 'request_prefill_reused' && e.payload.draft_id === entry.draft_id));
q.proof.C88 = {request_id:entry.request_id, draft_id:source.draft_id, draft_version:source.draft_version,
request_version_before:prepared.request.version, request_version_after:recovered.request.version,
old_empty_description_preserved_in_revision:true, explicit_preview_and_ui_save:true, source_recorded_in_audit:true};
q.proof.business_data_writes = 1;
q.save();
} else if (name === 'A') {
const reused = prepared.events.find(e => e.action === 'request_prefill_reused' && e.payload.draft_id === entry.draft_id);
assert(reused, 'Recovered reuse must have provenance in the existing audit trail');
assert(prepared.revisions.some(r => r.snapshot.payload.description === ''), 'Empty original preserved in a previous revision');
q.proof.C88 = {request_id:entry.request_id, draft_id:reused.payload.draft_id, draft_version:reused.payload.draft_version,
recovered_request_version:prepared.request.version, old_empty_description_preserved_in_revision:true,
source_recorded_in_audit:true, explicit_preview_and_ui_save_receipt:'investor-ab-remaining-chromium-20261010-attempt-' + q.stamp + '.json',
mutation_attempt_started_at:q.prior.started_at, mutation_attempt_result:q.prior.result,
mutation_attempt_harness_limit:'revision version is serialized as text; corrected comparison uses Number and does not repeat the save'};
q.save();
}
const populated = await page.locator('textarea, input:not([type="hidden"]):not([type="checkbox"])').evaluateAll(nodes => nodes.filter(n => n.value).map(n => ({label:n.labels?.[0]?.textContent?.trim(), value:n.value})).filter(n => /nome.*proje|título|resumo|descrição/i.test(n.label || '')));
q.proof.prefill_probe = {scenario:name, fields:populated}; q.save();
assert(populated.some(n => n.value === entry.title), 'Balcao title must remain the correct dossier title');
assert(populated.some(n => n.value === 'Descrição sintética ' + name + ' QA para recuperar no Balcão.'), 'Balcao summary must come from the contextual saved tool');
await q.shot(page, name + '-balcao-prepared-390');
await audit('/balcao-virtual/pedidos/' + entry.request_id, 390);
q.proof.scenarios[name] = {case_id:entry.id, draft_id:entry.draft_id, result_id:entry.result_id,
facts:data.facts, land:{state:land.state, saved_state:land.saved_state, preparation_allowed:land.preparation_allowed},
break_even_units:20, funding_gap:entry.expected_funding_gap, currency:result.currency,
result_only_in_correct_dossier:true, preserved_result_history_count:data.results.length, request_id:entry.request_id, request_status:request.status,
request_country:'PT', request_origin:'ZA', prefilled_fields:populated,
task_id:savedTask.id, task_status:savedTask.status, task_title:savedTask.title,
independent_stage_enabled:name === 'B', case_version_before:data.case.version};
q.save();
}
q.phase('320px A/B dossier reflow, same data and no repeated mutation');
await page.setViewportSize({width:320, height:844});
for (const name of ['A', 'B']) {
const entry = previous.scenarios[name];
await q.open(page, '/investimentos/' + entry.id);
await page.getByRole('heading', {name:entry.title, exact:true}).waitFor();
await audit('/investimentos/' + entry.id, 320);
const data = (await q.json(page, '/investimentos/' + entry.id)).data;
assert.equal(data.case.version, q.proof.scenarios[name].case_version_before);
q.proof.scenarios[name].case_version_after = data.case.version;
await regionShot(page.locator('section').filter({has:page.getByRole('heading', {name:'Resultados devolvidos ao dossier', exact:true})}), name + '-financial-320');
}
q.proof.checks = {applicability_A_no_land_no_funding:true, applicability_B_land_and_funding:true,
financial_results_in_correct_dossier:true, prepared_balcao_drafts_recovered:true,
independent_task_completed_while_land_remains_waiting:true, independent_preparation_enabled:true,
mobile_390_320_no_overflow_or_axe_violations:true, preserved_existing_dossiers_and_versions:true};
q.proof.business_data_writes ||= 0;
q.proof.scope_limit = 'Internal A/B preparation and follow-up only; no Balcao submission or external land/finance decision. Real devices and screen-reader acceptance remain separate.';
q.proof.result = 'passed';
} catch (error) {
q.proof.result = 'failed';
q.proof.error = sanitized(error);
if (page && !['/login','/two-factor-challenge'].includes(new URL(page.url()).pathname)) {
q.proof.failure_state = {path:new URL(page.url()).pathname, labels:await page.locator('label').allTextContents(), alerts:await page.getByRole('alert').allTextContents()};
await q.shot(page, 'failure').catch(() => {});
}
} finally {
q.proof.finished_at = new Date().toISOString();
q.save();
await q.browser.close();
console.log(JSON.stringify({result:q.proof.result, phase:q.proof.phase, checks:q.proof.checks, receipt:q.receipt}));
if (q.proof.result !== 'passed') process.exitCode = 1;
}
})();
+35
View File
@@ -0,0 +1,35 @@
const {start,BASE,assert,sanitized}=require('./portal-visual-qa-lib.cjs');
(async()=>{const q=await start('investor-ab-chromium-20261010');q.proof.scenarios=q.prior.scenarios||{};q.proof.checks={...(q.prior.checks||{})};q.proof.prior_attempt_started_at=q.prior.started_at||null;let page,ctx;
async function view(id){const r=await q.json(page,'/investimentos/'+id);assert.equal(r.status,200);return r.data;}
async function mutate(path,action){const response=page.waitForResponse(r=>new URL(r.url()).pathname===path&&r.request().method()==='POST');await action();const r=await response;assert([200,201,302,303].includes(r.status()),'Expected successful UI mutation');await page.waitForLoadState('domcontentloaded');}
async function choice(label,text){const escaped=label.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');const control=page.getByRole('combobox',{name:new RegExp('^'+escaped+'(?: \\(obrigatório\\))?$')});await control.fill(text);await page.getByRole('option').filter({hasText:text}).first().click();}
async function state(e,code,value,reason){const box=page.locator('#'+code);await box.locator('summary').click();await box.getByLabel(/^Estado/).selectOption(value);await box.getByLabel(/^Acção/).selectOption('prepare');await box.getByLabel('Fundamento',{exact:true}).fill(reason);await mutate('/investimentos/'+e.id+'/etapas/'+code,()=>box.getByRole('button',{name:'Guardar registo',exact:true}).click());await page.reload({waitUntil:'domcontentloaded'});assert.equal((await view(e.id)).steps.find(s=>s.code===code).saved_state,value);}
try{
if(!q.proof.checks.new_session_applicability_parallel){q.phase('A/B creation through interface');({page,ctx}=await q.context('investor18'));await q.login(page,'investor18');
for(const name of ['A','B']){const e=q.proof.scenarios[name]||={title:'QA visual '+name+' — '+q.stamp};await q.open(page,'/investimentos');if(!e.id){await page.getByLabel('Título',{exact:true}).fill(e.title);await page.getByLabel('Autorizo reutilizar o meu perfil neste dossier privado.').check();await page.getByRole('button',{name:'Criar e guardar dossier',exact:true}).click();await page.waitForURL(/\/investimentos\/[0-9a-f-]{36}$/);e.id=new URL(page.url()).pathname.split('/').pop();e.created_via_ui=true;q.save();}await q.open(page,'/investimentos/'+e.id);await page.getByRole('heading',{name:e.title,exact:true}).waitFor();assert.equal(await page.locator('article[id^="S"]').count(),19);assert.equal(await page.getByRole('heading',{name:/^Fase \d+ ·/}).count(),5);}
assert.notEqual(q.proof.scenarios.A.id,q.proof.scenarios.B.id);q.proof.checks.two_independent_dossiers_5_phases_19_steps=true;q.save();await q.logout(page);await ctx.close();
q.phase('Fresh authenticated session, applicability and parallel preparation');({page,ctx}=await q.context('investor18-new-session'));await q.login(page,'investor18');
for(const name of ['A','B']){const e=q.proof.scenarios[name];await q.open(page,'/investimentos');await page.getByRole('link',{name:e.title,exact:true}).click();await page.waitForURL(BASE+'/investimentos/'+e.id);e.recovered_new_session=true;
const form=page.locator('form').filter({has:page.getByRole('heading',{name:'Decisões do promotor e aplicabilidade',exact:true})});await form.getByLabel(/^Precisa de terra/).selectOption(name==='A'?'no':'yes');await form.getByLabel(/^Precisa de financiamento externo/).selectOption(name==='A'?'no':'yes');await form.getByLabel(/^Decisão sobre viabilidade/).selectOption('proceed');await form.getByLabel('Fundamento',{exact:true}).fill('Declaração sintética '+name+' QA; sem decisão externa.');await mutate('/investimentos/'+e.id+'/aplicabilidade',()=>form.getByRole('button',{name:'Guardar decisões',exact:true}).click());await page.reload({waitUntil:'domcontentloaded'});
const v=await view(e.id);assert.equal(v.facts.land_needed,name==='A'?'no':'yes');assert(name==='A'?v.steps.find(s=>s.code==='S10').state==='dispensed':['not_started','waiting_external'].includes(v.steps.find(s=>s.code==='S10').state));e.applicability_recovered=true;
if(name==='B')await state(e,'S10','waiting_external','Aguarda informação do terreno QA; mercado independente.');await state(e,'S02','preparing','Preparação independente de mercado QA.');await q.shot(page,'dossier-'+name+'-applicability',false);q.save();}
q.proof.checks.new_session_applicability_parallel=true;
}else{q.phase('Resume demonstrated A/B, preserving prior evidence and persisted states');({page,ctx}=await q.context('investor18'));await q.login(page,'investor18');}
q.phase('Contextual tool, saved financial result and return to dossier');
for(const name of ['A','B']){const e=q.proof.scenarios[name];await q.open(page,'/investimentos/'+e.id);await page.getByRole('link',{name:'Plano e simulador',exact:true}).click();await page.waitForURL(u=>u.searchParams.get('context')===e.id);await page.getByText('Dossier privado recuperado da sua conta.',{exact:true}).waitFor();await choice('O que pretende calcular ou planear? Escolha o percurso','Avaliar o meu investimento');await choice('Nível de detalhe','Simples');await choice('O que pretende fazer?','Apenas simular');await choice('Idioma preferido','Português (Moçambique)');
await page.getByRole('navigation',{name:'Etapas do formulário'}).getByRole('button',{name:/Projecto ou negócio/}).click();await page.getByLabel('Título do projecto',{exact:true}).fill('Plano financeiro '+name+' QA');await page.getByLabel('Resumo executivo',{exact:true}).fill('Descrição sintética '+name+' QA para recuperar no Balcão.');
await page.getByRole('navigation',{name:'Etapas do formulário'}).getByRole('button',{name:/Estrutura financeira/}).click();
// UI probes contain field names only, no entered credentials or OTP.
q.proof.tool_probe={scenario:name,path:new URL(page.url()).pathname,labels:await page.locator('label').allTextContents(),alerts:await page.getByRole('alert').allTextContents()};q.save();
const financial=page.getByRole('region',{name:'Ferramentas de planeamento financeiro'});await choice('Moeda','MZN');assert.equal(await financial.getByLabel('Moeda dos valores (código ISO)').inputValue(),'MZN');await financial.getByLabel(/^Custos fixos/).fill('1000');await financial.getByLabel('Preço por unidade',{exact:true}).fill('100');await financial.getByLabel('Custo variável por unidade',{exact:true}).fill('50');await financial.getByLabel('Investimento total',{exact:true}).fill('100000');await financial.getByLabel('Montante já mobilizado',{exact:true}).fill(name==='A'?'100000':'30000');await financial.getByLabel(/^Valor da receita/).first().fill('100');await financial.getByLabel(/^Valor da despesa/).first().fill('50');await financial.getByText(/Unidades: 20;/).waitFor();assert.equal(await financial.getByLabel('Moeda dos valores (código ISO)').inputValue(),'MZN');
await page.getByRole('button',{name:'Guardar e continuar mais tarde',exact:true}).click();await page.getByText('Dossier guardado no espaço privado da sua conta. Pode continuar mais tarde ao iniciar sessão.',{exact:true}).waitFor();await page.getByRole('button',{name:'Guardar resultado financeiro e voltar ao dossier',exact:true}).click();await page.waitForURL(BASE+'/investimentos/'+e.id);const v=await view(e.id);assert(v.results.length>0);e.result_id=v.results.at(-1).id;e.draft_id=v.results.at(-1).draft_id;e.tool_returned_via_ui=true;await page.getByText('Ponto de equilíbrio: 20 unidades',{exact:true}).first().waitFor();await page.getByText(new RegExp('^Necessidade de financiamento: '+(name==='A'?'0':'70000')+'(?:\\.00)?$')).first().waitFor();e.expected_break_even=20;e.expected_funding_gap=name==='A'?0:70000;await q.shot(page,'dossier-'+name+'-result');q.save();
const support=page.locator('section').filter({has:page.getByRole('heading',{name:'Apoio no Balcão',exact:true})});const requests=(await view(e.id)).requests;if(!requests.length){await support.getByLabel(/^Serviço/).selectOption({index:0});await support.getByRole('button',{name:'Preparar pedido com dados do dossier',exact:true}).click();await page.waitForURL(/\/balcao-virtual\/pedidos\/[0-9a-f-]{36}$/);e.request_id=new URL(page.url()).pathname.split('/').pop();}else{e.request_id=requests[0].id;await support.locator('a[href="/balcao-virtual/pedidos/'+e.request_id+'"]').click();await page.waitForURL(BASE+'/balcao-virtual/pedidos/'+e.request_id);}q.save();await page.locator('#request-country').selectOption('PT');await page.locator('#declared-origin-country').selectOption('ZA');const supportSave=page.waitForResponse(r=>r.request().method()==='PUT'&&new URL(r.url()).pathname==='/balcao-virtual/pedidos/'+e.request_id);await page.getByRole('button',{name:'Guardar rascunho',exact:true}).click();assert([200,302,303].includes((await supportSave).status()));await page.reload({waitUntil:'domcontentloaded'});assert.equal(await page.locator('#request-country').inputValue(),'PT');assert.equal(await page.locator('#declared-origin-country').inputValue(),'ZA');e.support_ui_saved=true;await q.shot(page,'balcao-'+name);
await q.open(page,'/investimentos/'+e.id);const task=page.locator('section').filter({has:page.getByRole('heading',{name:'Acompanhamento interno',exact:true})});if(!(await view(e.id)).tasks.length){await task.getByLabel(/^Etapa/).selectOption('S02');await task.getByLabel('Tarefa',{exact:true}).fill('Mercado paralelo '+name+' QA');await task.getByLabel('Prazo',{exact:true}).fill('2026-10-20T12:00');await task.getByLabel('Assumo esta tarefa no acompanhamento interno.').check();await mutate('/investimentos/'+e.id+'/tarefas',()=>task.getByRole('button',{name:'Guardar tarefa',exact:true}).click());await page.reload({waitUntil:'domcontentloaded'});}e.task_id=(await view(e.id)).tasks[0].id;q.save();}
q.proof.checks.contextual_tool_results_support_tasks=true;await q.logout(page);await ctx.close();
q.phase('Management follow-up with private financial data protected');({page,ctx}=await q.context('manager'));await q.login(page,'manager');
for(const name of ['A','B']){const e=q.proof.scenarios[name];await q.open(page,'/investimentos/'+e.id);await page.getByText('Nenhum resultado financeiro disponível para esta conta neste dossier.',{exact:true}).waitFor();assert.equal((await view(e.id)).results.length,0);if((await view(e.id)).tasks[0].status!=='done'){await page.getByLabel('Fundamento do resultado ou atribuição').fill('Acompanhamento interno sintético '+name+' QA.');for(const action of ['Assumir acompanhamento','Iniciar tarefa','Registar conclusão']){await mutate('/balcao-virtual/dossiers/'+e.id+'/tarefas/'+e.task_id,()=>page.getByRole('button',{name:action,exact:true}).click());await page.reload({waitUntil:'domcontentloaded'});await page.getByLabel('Fundamento do resultado ou atribuição').fill('Acompanhamento interno sintético '+name+' QA.');}}assert.equal((await view(e.id)).tasks[0].status,'done');e.management_ui_completed=true;await q.shot(page,'management-'+name);q.save();}await q.logout(page);await ctx.close();
q.phase('Investor recovery, no project mixing, outsider denial');({page,ctx}=await q.context('investor18-recovery'));await q.login(page,'investor18');
for(const name of ['A','B']){const e=q.proof.scenarios[name];await q.open(page,'/investimentos/'+e.id);const v=await view(e.id);assert(v.results.some(r=>r.id===e.result_id&&r.draft_id===e.draft_id));assert.equal(v.tasks[0].status,'done');assert.equal(v.requests[0].id,e.request_id);await page.getByRole('heading',{name:'Próxima acção',exact:true}).waitFor();assert.equal(v.steps.find(s=>s.code==='S10').state,name==='A'?'dispensed':'waiting_external');e.final_ui_recovery=true;await q.shot(page,'final-'+name);}
for(const [name,e] of Object.entries(q.proof.scenarios)){await q.open(page,'/simulador-fiscal?context='+e.id);await page.getByText('Dossier privado recuperado da sua conta.',{exact:true}).waitFor();await page.getByRole('navigation',{name:'Etapas do formulário'}).getByRole('button',{name:/Projecto ou negócio/}).click();assert.equal(await page.getByLabel('Título do projecto',{exact:true}).inputValue(),'Plano financeiro '+name+' QA');await page.getByRole('navigation',{name:'Etapas do formulário'}).getByRole('button',{name:/Estrutura financeira/}).click();const tool=page.getByRole('region',{name:'Ferramentas de planeamento financeiro'});assert.equal(await tool.getByLabel(/^Custos fixos/).inputValue(),'1000');assert.equal(await tool.getByLabel('Moeda dos valores (código ISO)').inputValue(),'MZN');assert.equal(await tool.getByLabel('Montante já mobilizado',{exact:true}).inputValue(),name==='A'?'100000':'30000');e.tool_inputs_recovered_new_session=true;await q.shot(page,'tool-recovery-'+name);}q.proof.checks.tool_inputs_recovered_new_session=true;
assert.notEqual(q.proof.scenarios.A.draft_id,q.proof.scenarios.B.draft_id);await q.logout(page);await ctx.close();({page,ctx}=await q.context('outsider'));await q.login(page,'outsider');for(const e of Object.values(q.proof.scenarios)){assert.equal((await q.open(page,'/investimentos/'+e.id)).status(),403);assert.equal((await q.json(page,'/api/v1/simulation-drafts/'+e.draft_id)).status,404);}await ctx.close();q.proof.checks.final_recovery_financial_privacy_unauthorized_denied=true;q.proof.result='passed';
}catch(e){q.proof.result='failed';q.proof.error=sanitized(e);if(page&&!['/login','/two-factor-challenge'].includes(new URL(page.url()).pathname)){q.proof.failure_state={path:new URL(page.url()).pathname,labels:await page.locator('label').allTextContents(),alerts:await page.getByRole('alert').allTextContents()};await q.shot(page,'failure').catch(()=>{});}}finally{q.proof.finished_at=new Date().toISOString();q.save();await q.browser.close();console.log(JSON.stringify({result:q.proof.result,phase:q.proof.phase,checks:q.proof.checks,receipt:q.receipt}));if(q.proof.result!=='passed')process.exitCode=1;}})();
+82
View File
@@ -0,0 +1,82 @@
"""Record the responsible user's scoped C37 acceptance, without repeating tests."""
import csv
import hashlib
import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
csv.field_size_limit(16000000)
ROOT = Path(__file__).resolve().parent.parent
DOC = ROOT / 'docs/investmoz/v2/auditoria-portal'
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
OUT = PRIVATE / 'c37-manual-close-20261010'
OUT.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc)
references = ['c37-qa18-chromium-20261010.json', 'investor-ab-chromium-20261010.json',
'portal-logout-chromium-20261010.json', 'logout-finalize-20261010/manual-qa18-logout.json']
proofs = []
for name in references:
data = (PRIVATE / name).read_bytes()
proof = json.loads(data)
if name.endswith('manual-qa18-logout.json'):
assert proof['button_and_logout_confirmed_by_user'] and not proof['session_authenticated_after_logout']
else:
assert proof['result'] == 'passed' and proof['active_writes'] == 0
proofs.append({'file': name, 'sha256': hashlib.sha256(data).hexdigest()})
evidence = {'recorded_at_utc': stamp.isoformat(), 'case': 'C37', 'state': 'closed_scoped_qa18_authentication',
'manual_confirmation': 'Registar a confirmação manual e encerrar C37 no âmbito da autenticação QA18, ligando-a às provas anteriores de logout e recuperação. Não repetir os ensaios já concluídos.',
'accepted_by': 'Responsável, mensagem desta conversa em 10/10/2026', 'actor_id': 18,
'scope': 'Login, MFA, redirecionamento, sessão autenticada, atualização, logout e recuperação/nova sessão QA18',
'references': proofs, 'tests_repeated': False, 'active_writes': 0,
'does_not_close': ['A/B acceptance', 'entire audit', 'mobile and full accessibility', 'other profiles', 'active activation']}
(OUT / 'acceptance.json').write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding='utf-8')
tag = '[C37 fechado QA18 10/10/2026] '
text = ('Confirmação manual expressa do responsável: C37 encerrado exclusivamente no âmbito da autenticação QA18. '
'Liga login/MFA/redirecionamento/sessão/F5/nova sessão à prova Chromium QA18 anterior, '
'ao logout manual18 com invalidação de sessão e à recuperação previamente demonstrada; sem repetir ensaios. '
'Aceitação sanitizada e hashes: c37-manual-close-20261010/acceptance.json. C85 permanece corrigido QA. '
'Não encerra A/B, auditoria integral, outros perfis ou ativação ativa.')
ids = {'AUD-UI-01', 'AUD-GUIDE-01', 'AUD-DASH-01', 'AUD-PROFILE-24', 'AUD-PROFILE-25'}
files = []
for filename in ['matriz-requisitos.csv', 'backlog-priorizado.csv']:
source = DOC / filename
rows = list(csv.reader(source.open(encoding='utf-8-sig', newline='')))
h = rows[0]
patches = []
for index, row in enumerate(rows[1:], 1):
old = dict(zip(h, row))
if old['ID'] not in ids:
continue
if filename.startswith('matriz'):
values = {'Evidência funcional': old['Evidência funcional'] + ' | ' + tag + text,
'Lacuna': tag + 'C37 não é mais pendência de autenticação QA18; permanecem os demais critérios do requisito. Histórico abaixo preservado e superado apenas neste âmbito. | ' + old['Lacuna'],
'Interface': old['Interface'] + ' | ' + tag + 'Aceitação manual QA18 encerrada, ligada a logout e recuperação anteriores.',
'Recuperação': old['Recuperação'] + ' | ' + tag + 'Recuperação/nova sessão aceite pelo responsável; provas anteriores preservadas, não repetidas.',
'Consolidação': old['Consolidação'] + ' | ' + tag + 'Fecho por caso e âmbito; estado global parcial e referências/versões preservados.'}
variants = json.loads(old['Critérios e versões por origem (JSON)'])
for variant in variants:
variant['aceitacao_c37_qa18_20261010'] = {'case': 'C37', 'estado': 'encerrado no âmbito da autenticação QA18',
'fonte': 'Confirmação manual expressa do responsável', 'prova': 'c37-manual-close-20261010/acceptance.json',
'registado_em': stamp.isoformat(), 'ensaios_repetidos': False, 'ativacao': False}
values['Critérios e versões por origem (JSON)'] = json.dumps(variants, ensure_ascii=False, separators=(',', ':'))
else:
values = {'Pendência': tag + 'C37 autenticação QA18 encerrado; não agendar repetição. Demais pendências e histórico: ' + old['Pendência'],
'Aceitação': old['Aceitação'] + ' | ' + tag + text}
patches.append({'row': index, 'values': values})
assert len(rows) == 3394 and len(patches) == len(ids)
files.append({'filename': filename, 'input_sha256': hashlib.sha256(source.read_bytes()).hexdigest(), 'rows': rows,
'patches': patches, 'allowed_columns': sorted({field for patch in patches for field in patch['values']}), 'append': []})
(OUT / 'csv-edits.json').write_text(json.dumps({'files': files}, ensure_ascii=False), encoding='utf-8')
section = f'''## C37 encerrado — autenticação QA18 — {stamp.astimezone(timezone(timedelta(hours=2))).isoformat()}
**Confirmação manual expressa do responsável registada. C37 está encerrado exclusivamente no âmbito da autenticação QA18.** Login → MFA → redirecionamento → sessão autenticada → atualização → logout → nova sessão/recuperação estão ligados às provas anteriores, sem repetir ensaios nem alterar conta, chave ou MFA. O logout manual de 20:17:15 foi confirmado pelo responsável e pelo rastreio: POST302→/, autenticação e estado persistido vazios.
Aceitação e hashes dos recibos: `c37-manual-close-20261010/acceptance.json`. Referências: `c37-qa18-chromium-20261010.json`, `logout-finalize-20261010/manual-qa18-logout.json`, `portal-logout-chromium-20261010.json` e recuperação do dossier em `investor-ab-chromium-20261010.json`. A prova QA21 do botão é complementar; a conta18 tem a sua própria confirmação. Horário local UTC+02; datas técnicas dos recibos conservadas em UTC.
O fecho **não encerra A/B, auditoria integral, móvel/acessibilidade integral, outros perfis ou ativação ativa**. Os cinco requisitos relacionados continuam parciais pelos seus outros critérios; contagens preservadas: 3393 requisitos/3393 linhas/6845 ocorrências. Menções históricas abaixo a C37/autenticação18 pendente são superadas somente por este fecho. C85 permanece corrigido em QA. Próximo trabalho independente: gravação/recuperação territorial e consistência país–hierarquia por consumidor, países/origem/estatísticas, documentos e workflow, preservando decisões próprias de perfis modulares/SLA.
'''
for name in ['relatorio-executivo.md', 'backlog.md', 'casos-reproducao.md', 'plano-ativacao.md']:
file = DOC / name
old = file.read_text(encoding='utf-8-sig')
file.write_text('<!-- c37-manual-close-20261010:begin -->\n' + section + '\n<!-- c37-manual-close-20261010:end -->\n\n' + old, encoding='utf-8')
print(json.dumps({'case': 'C37', 'state': 'closed_scoped_qa18_authentication', 'requirements_touched': len(ids), 'tests_repeated': False, 'active_writes': 0}))
+184
View File
@@ -0,0 +1,184 @@
"""Reconcile browser evidence into the sole existing audit, preserving source references."""
import collections
import csv
import hashlib
import json
import re
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(5_000_000)
browser = json.loads((PRIVATE / 'legacy-content-browser-qa.json').read_text(encoding='utf-8'))
persist = json.loads((PRIVATE / 'legacy-content-browser-persistence-qa.json').read_text(encoding='utf-8'))
assert browser['result'] == 'passed' and browser['interface_observed'] and browser['checks']['qa_browser_title']
assert persist['dossier_id'] == browser['dossier_id'] and persist['reference_count'] == persist['reference_event_count'] == 1
assert browser['creation_evidence']['receipt'] and browser['checks']['created_from_ui']
assert persist['transaction_read_only'] and not persist['active_access'] and browser['active_writes'] == 0
matrix = DOC / 'matriz-requisitos.csv'
rows = list(csv.DictReader(matrix.open(encoding='utf-8-sig', newline='')))
headers = list(rows[0])
protected = ['ID', 'Pedido/trecho', 'Fonte/ocorrências', 'Critério de aceitação', 'Versões divergentes',
'IDs das linhas de origem', 'Quantidade de linhas de origem']
before = {r['ID']: {k: r[k] for k in protected} for r in rows}
tag = '[Interface legado QA 10/10/2026] '
evidence = ('Chromium instalado, contextos isolados, desktop 1440 px / móvel 390 px: fotografia/fontes/paleta, pesquisa hit/miss, '
'Enter/foco de 3 px, links carteira/zonas/biblioteca/oportunidade. Promotor QA21: login→MFA→dossier→fonte→retorno→'
'logout→novo contexto/MFA→recuperação/F5→associação repetida. Conta23:403 e artigo público sem título privado; '
'anónimo sem contexto privado. UI observada; referência/evento únicos e originais/hash/scan confirmados '
'em leitura QA. legacy-content-browser-qa.json (creation_evidence aponta a criação inicial); legacy-content-browser-persistence-qa.json; legado-piloto-qa.md.')
gaps = {
'AUD-LEG-01': 'Cobertura histórica, paginação, destinos externos e restantes exemplares/versões não importados. C68 continua identificado.',
'AUD-LEG-02': 'Piloto desktop/móvel e foco por teclado demonstrados. Falta cobertura sistemática dos restantes controlos/tecnologias assistivas e expansão das adaptações ao restante legado; não declarar conformidade integral de acessibilidade.',
'AUD-LEG-03': 'Percurso do piloto demonstrado na interface. Relações cartográficas concretas dos seis corredores ainda não fundamentadas; link para mapas gerais não comprova delimitação/integração territorial do artigo. A/B globais permanecem independentes.',
'AUD-LEG-04': 'Nenhum critério pendente na consulta e proteção do piloto QA demonstrado. C37/QA18 no navegador do utilizador e disponibilização ativa são requisitos/ações independentes, não fechados por este ensaio.',
'AUD-LEG-05': 'Relações territoriais concretas e ampliação por etapa/setor ainda por fundamentar. Fonte única no piloto, sem criar oportunidades ou geometrias pelo artigo.',
'AUD-LEG-06': 'Contexto/retorno do piloto observados; mapa de migração dos restantes URLs ainda pendente. C37/QA18 e cenários A/B completos não foram executados neste ensaio.',
'AUD-LEG-07': 'Imagens/links/pesquisa/permissões/deduplicação do piloto demonstrados. Anexos/referências dos restantes conteúdos e imagem histórica404 por resolver; cobertura global permanece parcial.',
}
for row in rows:
if row['ID'] not in gaps:
continue
# Old findings remain as dated evidence; source requests and acceptance wording are immutable.
for key in ['Interface', 'Evidência funcional', 'Resultado apresentado', 'Recuperação']:
if tag not in row[key]:
value = evidence if key == 'Evidência funcional' else (
'Render/cliques observados no Chromium QA, com recuperação noutra sessão/F5 e negação à conta23. '
'Não é o navegador pessoal do utilizador nem o ensaio C37/QA18.')
row[key] = tag + value + ' | Evidência anterior preservada: ' + row[key]
row['Lacuna'] = gaps[row['ID']]
row['Dependência'] = 'Biblioteca/documentos existentes; Balcão+09/190000 para dossier. Expansão cartográfica exige correspondências fundamentadas. C37/A-B/SLA/perfis e ativação mantêm âmbito próprio.'
if row['ID'] == 'AUD-LEG-04':
old_state = row['Estado']
row['Estado'] = 'concluído e demonstrado'
variants = json.loads(row['Critérios e versões por origem (JSON)'])
for variant in variants:
if variant.get('estado') != row['Estado']:
variant.setdefault('historico_estado', []).append({'estado': variant.get('estado'), 'motivo': 'Sem prova visual no ensaio inicial'})
variant['estado'] = row['Estado']
variant['ambito_demonstrado'] = 'Piloto agronegócio exclusivamente QA; não ativação, C37 ou cobertura histórica'
variant['evidencia'] = 'legacy-content-browser-qa.json; legacy-content-browser-persistence-qa.json'
row['Critérios e versões por origem (JSON)'] = json.dumps(variants, ensure_ascii=False)
if tag not in row['Consolidação']:
row['Consolidação'] += ' | ' + tag + 'Estado anterior ' + old_state + '; critérios do piloto QA agora demonstrados; ativo indisponível e requisitos globais preservados.'
else:
if tag not in row['Consolidação']:
row['Consolidação'] += ' | ' + tag + 'Critérios visuais do piloto comprovados sem encerrar as lacunas globais enumeradas.'
# A later backup probe discovered a separate QA infrastructure fault. Keep the UI proof,
# but do not allow a historical readiness receipt to imply a current verified backup.
failure = json.loads((PRIVATE / 'profile-installation-backup-failure-qa.json').read_text(encoding='utf-8'))
preservation = json.loads((PRIVATE / 'qa-readable-preservation.json').read_text(encoding='utf-8'))
assert not failure['restore_verified'] and f"{failure['target']['database']}:{failure['target']['port']}" == browser['target']
assert preservation['transaction_read_only'] and not preservation['restore_verified'] and preservation['database_writes'] == 0
fault_tag = '[C71 QA 10/10/2026] '
for row in rows:
if row['ID'] not in ['AUD-OPS-01', 'AUD-PROFILE-31']:
continue
if fault_tag not in row['Evidência funcional']:
row['Evidência funcional'] += ' | ' + fault_tag + 'Novo pg_dump falha: global/2964 (pg_db_role_setting) ausente no cluster QA55441. Prova anterior13:35:39UTC conservada, não é renovação. 295 tabelas/34902 linhas/31 sequências preservadas por leitura;26 ficheiros de catálogos partilhados ausentes. Preservação não é restauro verificado. qa-readable-preservation.json; profile-installation-backup-failure-qa.json.'
row['Lacuna'] = fault_tag + 'Restauro atual bloqueado pela integridade do cluster QA; origem da remoção não identificada. Recuperar em cluster isolado preservando todos os dados, conferir schema/seq/ficheiros/MFA e repetir backup/restauro e percursos afetados. Readiness QA invalidada; candidata não fixada. | Lacunas anteriores preservadas: ' + row['Lacuna']
row['Dependência'] = fault_tag + 'Restabelecer backup/restauro QA antes de novas escritas de ensaio ou candidata; nenhum catálogo copiado/recriado às cegas e nenhuma ação no PostgreSQL ativo5432. | ' + row['Dependência']
if row['ID'] == 'AUD-OPS-01':
row['Estado'] = 'com falha'
row['Prioridade'] = 'P0'
assert before == {r['ID']: {k: r[k] for k in protected} for r in rows}
assert len({r['ID'] for r in rows}) == len(rows)
with matrix.open('w', encoding='utf-8-sig', newline='') as f:
writer = csv.DictWriter(f, fieldnames=headers); writer.writeheader(); writer.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)), 'legacy_states': {r['ID']: r['Estado'] for r in rows if r['ID'] in gaps}}
(PRIVATE / 'legacy-browser-audit-update-qa.json').write_text(json.dumps(counts, ensure_ascii=False, indent=2), encoding='utf-8')
marker = 'qa-legacy-browser-20261010'
def prepend(filename, content):
file = DOC / filename
text = file.read_text(encoding='utf-8')
text = re.sub(r'\n*<!-- ' + marker + r':begin -->.*?<!-- ' + marker + r':end -->\n*', '\n\n', text, flags=re.S)
first, rest = text.split('\n', 1)
file.write_text(first + '\n\n<!-- ' + marker + ':begin -->\n' + content + '\n<!-- ' + marker + ':end -->\n\n' + rest.lstrip(), encoding='utf-8')
summary = '''## Verificação do piloto na interface — 10/10/2026, exclusivamente QA
**O piloto existente foi demonstrado em Chromium instalado**, com contextos separados, sem usar o perfil pessoal do utilizador. O controlo nativo continua indisponível (`native pipe`, os error 2, duas sondagens); esse problema não impediu renderização, cliques e capturas por Playwright. C37/QA18 permanece pendente; este ensaio usou Promotor QA21 e outra conta sintética QA23.
Página pública→pesquisa com/sem resultado→artigo→carteira/mapas/biblioteca/oportunidade publicada→CTA→login/MFA da conta QA21→dossier guardado→fonte/retorno→logout→novo contexto/MFA→recuperação e F5→continuação/repetição passaram. Leitura anónima e QA23 não recebem o título/ID do contexto privado; dossier negado 403 à QA23. A criação na interface consta da tentativa inicial preservada; as repetições recuperam o mesmo dossier sem criar outro. Cinco fases / 19 etapas observadas. Persistência em transação READ ONLY: uma referência, um evento, um documento por hash; originais e scan preservados; projeto independente com zero referências.
Desktop 1440 px / móvel 390 px: fotografia 900×506, Noto Sans / Noto Serif carregadas, cores reais adaptadas, texto integral de 4.702 carateres, foco de 3 px por teclado e nenhum deslocamento horizontal. Sete capturas privadas. **C70 corrigido:** título de separador “Laravel” substituído por “InvesteMoz QA” apenas no build QA; bundle ativo preservado. Isto não certifica acessibilidade integral nem demonstra todas as geometrias/consumidores do portal.
**AUD-LEG-04 concluído e demonstrado em QA**, por consulta pública/proteção privada. AUD-LEG-01/02/03/05/06/07 continuam parciais pelas lacunas explicitadas na matriz: cobertura histórica, controlo assistivo sistemático, relações territoriais concretas, restante mapa de URLs e anexos. Nenhum I0–I3/A-B/workflow/C37 é encerrado por esta prova.
Recibos: `legacy-content-browser-qa.json`, `legacy-content-browser-persistence-qa.json`; testes `tools/legacy-content-browser-qa.cjs` e `tools/legacy-content-browser-persistence-qa.php`. Capturas e credenciais permanecem privadas; não foram registados Cookie, X-XSRF-TOKEN ou corpo com o código MFA. O ZIP anteriormente entregue é um snapshot anterior à verificação, não prova de execução remota.'''
backup_note = """
**C71 / P0 — novo backup QA bloqueado:** `pg_dump` falhou às14:48–14:49UTC: `global/2964` ausente, catálogo `pg_db_role_setting`; diagnóstico identifica26 ficheiros de catálogos partilhados ausentes no cluster55441, situado em Temp. Não atribuir a remoção à limpeza automática sem evidência. O backup/restauro válido de13:35:39UTC (295 tabelas/81 ficheiros) permanece histórico. Preservação posterior READ ONLY:295 tabelas/34.902 linhas/31 sequências; não equivale a backup/restauro verificado. Readiness QA foi invalidada e o CLI passa a devolver exit1 controlado. Próxima ação: recuperação em ambiente isolado, preservação/conferência integral e nova prova de restauro antes de retomar escritas. Nenhum catálogo recriado às cegas nem acesso/escrita na base ativa. Ver `qa-readable-preservation.json`, `profile-installation-backup-failure-qa.json` e C71 em `casos-reproducao.md`."""
summary += backup_note
prepend('legado-piloto-qa.md', summary)
prepend('relatorio-executivo.md', summary + f'''
**Contagens da mesma matriz:** {counts['requirements']} requisitos/{counts['matrix_rows']} linhas; {counts['source_occurrences']} ocorrências de origem. Estados: ''' + '; '.join(f"{v} {k}" for k, v in counts['states'].items()) + '.')
prepend('backlog.md', '''## Próximo lote após a demonstração do piloto
| Requisito | Demonstrado | Falta / próxima ação |
| --- | --- | --- |
| AUD-OPS-01 / C71 — P0 | Diagnóstico e preservação READ ONLY; backup anterior intacto, readiness atual inválida. | Recuperar cluster QA e renovar backup/restauro antes de novas escritas; não fixar candidata. |
| AUD-LEG-04 | Consulta pública e login apenas para dossier; MFA da conta QA21/nova sessão/F5; anónimo/QA23 sem contexto; QA23 negado 403. Concluído exclusivamente QA. | Ativação separada; não assumir C37/QA18 resolvido. |
| AUD-LEG-02 | Desktop/móvel/fotografia/fontes/paleta, pesquisa, teclado; C70 título QA corrigido. | Verificação sistemática de controlos/tecnologias assistivas e adaptação dos outros setores. |
| AUD-LEG-03/05 | Artigo→consumidores→dossier/fonte/retorno; referência/evento únicos; sem mistura. | Fundamentar correspondências dos seis corredores; mapas gerais não equivalem a geometrias relacionadas com o artigo. |
| AUD-LEG-01/06/07 | Piloto visual e fontes preservadas. | Reconciliar os demais 50 exemplares observados, sitemap misto/paginação, outros URLs/anexos; C68 imagem 404 continua aberto. |
| C37; A/B; dashboard QA21 | Ensaio QA21 do piloto não os substitui. | C37 no contexto problemático QA18; A/B completos com ferramentas/Balcão/acompanhamento; CRUD de oportunidades pelo dashboard. |
Continuar países/origem/estatísticas, 173 lacunas cartográficas (Mandie/Guro ambíguo), documentação/derivadas, Response/materialidade e workflow na mesma matriz. Nenhum responsável real ou compromisso SLA foi designado. Base ativa fora do âmbito; sem repetir instalações QA030/040/09-190. C71 bloqueia novas escritas QA até recuperar a integridade e o restauro, sem revogar as provas já obtidas.''')
prepend('casos-reproducao.md', '''## C70 — identificação errada no título do navegador
**Requisito:** AUD-LEG-02; P2. **Reprodução:** abrir anónimamente `/explorar/agronegocio` na QA 18097 e consultar o título do separador. **Esperado:** identidade do portal e distinção QA. **Observado:** “Explorar agronegócio - Laravel”. **Causa:** `resources/js/app.tsx` usa o nome padrão da compilação; a QA herdava `VITE_APP_NAME` do ambiente local. **Correção:** `vite.config.ts` define “InvesteMoz QA” exclusivamente sob `INVESTMOZ_QA_BUILD=1`; não altera .env nem build ativo. **Evidência:** título verificado no Chromium, percurso renderizado após recompilação; manifesto ativo inalterado. Código preparado eQA demonstrada; não ativado.
As primeiras tentativas do ensaio de browser terminaram por falta de espera da navegação Inertia no próprio teste, antes de inspecionar o URL atualizado. O runner foi corrigido para aguardar o destino; recibos das tentativas preservados privadamente. Não classificar essas falhas de instrumentação como falhas do portal. C37 não foi repetido; C68 permanece aberto.
## C71 — falha do novo backup, cluster PostgreSQL QA
**Requisito:** AUD-OPS-01, dependência de AUD-PROFILE-31; P0, aberto. **Reprodução:** guarda identifica `desk_restore_20261009_174726_test:55441`; executar `tools/portal-profile-installation-backup-qa.php`. **Esperado:** dump atual, restauro noutra base QA e comparação integral. **Observado:** `pg_dump` exit1 ao consultar `pg_roles`, pois não consegue abrir `global/2964`; nenhum novo restauro criado. **Causa identificada:** ficheiro de `pg_db_role_setting` ausente; inspeção READ ONLY identifica26 ficheiros partilhados ausentes no clusterQA em Temp. O processo que os removeu não foi identificado. **Mitigação:** originais/backups anteriores preservados;295 tabelas/34.902 linhas/31 sequências extraídas privadamente por leitura; readiness atual invalidada e erro do CLI controlado. **Correção pendente:** recuperar clusterQA preservando schema, IDs, relações, sequências e chave estável, conferir todos os dados/ficheiros e repetir backup/restauro antes de ensaios com escrita. Não declarar preservação NDJSON ou backup13:35 como restauro renovado. Nenhum `key:generate`, catálogo inventado, reinício do PostgreSQL ativo ou escrita ativa.
Referência técnica: [PostgreSQL17 — pg_db_role_setting](https://www.postgresql.org/docs/17/catalog-pg-db-role-setting.html), catálogo partilhado por todas as bases de um cluster. Diagnósticos completos e extrações permanecem privados.''')
prepend('plano-ativacao.md', '''## Atualização após a prova visual do piloto
O piloto legado foi demonstrado no Chromium QA com Promotor QA21; C37/QA18, CRUD de oportunidades no dashboard e A/B completos continuam pendentes. Não fixar uma revisão candidata global apenas por esta prova. A identificação “InvesteMoz QA” é compilada somente no build QA; não transferir esse rótulo automaticamente para produção.
Preparação preservada: corrigir critérios globais→fixar revisão/hashes/configuração→conferir base ativa por leitura e renovar backup/restauro correspondente→código/bundle compatíveis com flags desligadas→030000→040000→09/190000 apenas se ausentes→importações privadas por prévia/hash/ClamD→divulgação por recurso/camada→disponibilização do percurso. Fonte editorial pública usa biblioteca; guardar contexto requer Balcão/09-190. Cartografia continua lote distinto.
Contagens do legado inalteradas: um artigo + uma imagem, três associações fundamentadas, zero oportunidades/geometrias criadas pelo artigo e zero contas/dossiers QA transferidos. Reversão: ocultar piloto/flag e usar bundle compatível; retirar divulgação em ação auditada preservando originais, versões, relações e histórico. Ensaio/restauro QA não substitui backup da base ativa/revisão candidata; nenhuma ativação autorizada.
C71 é agora pré-condição técnica: recuperar o cluster QA e renovar backup/restauro, retomar as escritas e repetir os percursos afetados; depois fixar candidata e preparar a prova específica do alvo ativo. O restauroQA anterior13:35 permanece histórico; readiness invalidada. A falha nova não autoriza alterar ou reiniciar a base ativa.''')
pilot = DOC / 'legado-piloto-qa.md'
text = pilot.read_text(encoding='utf-8')
text = text.replace('**Aceitação visual pendente:** `cua.getState()`', '**Histórico do ensaio inicial, anterior ao Chromium:** `cua.getState()`')
text = text.replace('Promotor QA21 visual, A/B, workflow normativo integral', 'CRUD do dashboard com Promotor QA21, A/B, workflow normativo integral')
pilot.write_text(text, encoding='utf-8')
# Preserve older versions as history, with explicit precedence for the latest evidence.
for filename in ['legado-piloto-qa.md', 'relatorio-executivo.md', 'backlog.md', 'casos-reproducao.md', 'plano-ativacao.md']:
file = DOC / filename
content = file.read_text(encoding='utf-8')
content = content.replace('## Frente atual — legado agronegócio, exclusivamente em QA', '## Histórico — legado agronegócio antes do ensaio Chromium')
content = content.replace('## Próximo lote — expansão do legado mantendo I0–I3/C37', '## Histórico do lote planeado antes do ensaio Chromium')
content = content.replace('## Complemento legado — preparaçãoQA, sem autorização ativa', '## Histórico — complemento legado anterior ao ensaio Chromium')
content = content.replace('**Navegador integrado indisponível; aceitação visual pendente**', '**Estado histórico, antes do ensaio Chromium: navegador integrado indisponível; aceitação visual então pendente**')
# The remainder of these old blocks retains its original findings and counts.
for a, b in {'às14:48': 'às 14:48', 'identifica26': 'identifica 26', 'cluster55441': 'cluster 55441', 'de13:35': 'de 13:35', 'ONLY:295': 'ONLY: 295', 'exit1': 'exit 1', 'identifica26': 'identifica 26', 'clusterQA': 'cluster QA', 'privadamente por leitura;295': 'privadamente por leitura; 295', 'leitura;295': 'leitura; 295', 'preservados;295': 'preservados; 295', 'leitura;26': 'leitura; 26', 'de catálogos partilhados ausentes no cluster55441': 'de catálogos partilhados ausentes no cluster 55441'}.items():
content = content.replace(a, b)
file.write_text(content, encoding='utf-8')
sources = DOC / 'inventario-fontes.csv'
records = list(csv.DictReader(sources.open(encoding='utf-8-sig', newline='')))
for filename in ['legado-piloto-qa.md']:
file = DOC / filename; data = file.read_bytes()
record = next(r for r in records if r['Exemplar/caminho'].replace('\\', '/').endswith('/' + filename))
old = record['SHA256']; new = hashlib.sha256(data).hexdigest()
if old != new: record['Edição/versão identificada'] += ' | exemplar anterior SHA256=' + old
edition = record['Edição/versão identificada']
if ' | interface QA10/10/2026' not in edition:
edition += ' | interface QA10/10/2026'
record.update({'SHA256': new, 'Bytes': str(len(data)), 'Nota de leitura': 'Prova real Chromium QA; limites históricos/cartográficos/C37/A-B preservados, sem ativação.',
'Edição/versão identificada': edition})
with sources.open('w', encoding='utf-8-sig', newline='') as f:
writer = csv.DictWriter(f, fieldnames=list(records[0])); writer.writeheader(); writer.writerows(records)
print(json.dumps(counts, ensure_ascii=True))
+114
View File
@@ -0,0 +1,114 @@
// Edit only the reported manual criteria in the sole matrix; preserve every other cell.
import fs from 'node:fs/promises';
import path from 'node:path';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { pathToFileURL, fileURLToPath } from 'node:url';
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const DOC = path.join(ROOT, 'docs/investmoz/v2/auditoria-portal');
const RUNTIME = path.join(ROOT, 'storage/app/private/investmoz-v2/portal-audit/legacy-sector-reconciliation-20261010');
const requireRuntime = createRequire(path.join(RUNTIME, 'package.json'));
const { Workbook } = await import(pathToFileURL(requireRuntime.resolve('@oai/artifact-tool')).href);
const sourcesOnly = process.argv.includes('--sources-only');
const baseline = JSON.parse(await fs.readFile(path.join(RUNTIME, sourcesOnly ? 'csv-source-baseline.json' : 'csv-baselines.json'), 'utf8'));
const reconciliation = JSON.parse(await fs.readFile(path.join(RUNTIME, 'reconciliation.json'), 'utf8'));
const csvUpdates = sourcesOnly ? { 'inventario-fontes.csv': JSON.parse(await fs.readFile(path.join(RUNTIME, 'source-inventory-updates.json'), 'utf8')) }
: reconciliation.csv_updates;
const manualTag = '[Confirmação manual recebida 10/10/2026] ';
const scope = {
'AUD-LEG-03': ['leitura_publica', 'autenticacao', 'criar_ou_escolher_dossier', 'guardar_referencia', 'nova_sessao', 'recuperar_dossier', 'abrir_artigo'],
'AUD-LEG-04': ['leitura_publica', 'autenticacao'],
'AUD-LEG-06': ['criar_ou_escolher_dossier', 'guardar_referencia', 'nova_sessao', 'recuperar_dossier', 'abrir_artigo'],
'AUD-LEG-07': ['guardar_referencia', 'recuperar_dossier', 'abrir_artigo'],
};
const evidence = 'Responsável confirmou leitura pública → autenticação → criar/escolher dossier → guardar referência → nova sessão → recuperar dossier e abrir artigo. Confirmação declarada, sem conta/perfil/dispositivo/data de execução identificados. Não comprova MFA específico, ensaios móveis, acessibilidade, outros perfis, deduplicação, C37/QA18 ou A/B. Ver legado-piloto-qa.md, confirmação manual.';
const allowedColumns = ['Evidência funcional', 'Interface', 'Gravação', 'Recuperação', 'Resultado apresentado', 'Consolidação', 'Critérios e versões por origem (JSON)'];
const outputs = [];
function append(original, value) {
return original.includes(manualTag) ? original : original + ' | ' + manualTag + value;
}
function encodeCSV(rows) {
const quote = value => /[",\r\n]/.test(value) ? '"' + value.replaceAll('"', '""') + '"' : value;
return '\ufeff' + rows.map(row => row.map(quote).join(',')).join('\r\n') + '\r\n';
}
function matchesSource(value, literal) {
// Imported ISO dates are typed by this runtime. Compare their instant for validation,
// but serialize untouched fields from the original CSV so its exact offset survives.
if (value instanceof Date) return value.getTime() === Date.parse(literal);
return (value == null ? '' : String(value)) === literal;
}
for (const filename of sourcesOnly ? ['inventario-fontes.csv'] : ['matriz-requisitos.csv', 'legado-inventario.csv', 'legado-conteudos-reconciliados.csv']) {
const input = await fs.readFile(path.join(DOC, filename), 'utf8');
const sourceRows = baseline[filename];
const workbook = await Workbook.fromCSV(input.replace(/^\ufeff/, ''), { sheetName: 'Registos' });
const sheet = workbook.worksheets.getItem('Registos');
const fullRange = sheet.getRangeByIndexes(0, 0, sourceRows.length, sourceRows[0].length);
const rows = fullRange.values;
assert(rows.every((row, i) => row.every((value, c) => matchesSource(value, sourceRows[i][c]))), 'Imported data must match source values');
const strings = sourceRows.map(row => [...row]);
const headers = strings[0];
const changes = [];
for (let i = 1; i < strings.length; i++) {
const original = strings[i];
const updated = [...original];
const id = filename === 'matriz-requisitos.csv' ? original[headers.indexOf('ID')] : null;
if (id && scope[id]) {
for (const column of allowedColumns.filter(c => c !== 'Critérios e versões por origem (JSON)')) {
const index = headers.indexOf(column);
updated[index] = append(original[index], evidence);
}
const index = headers.indexOf('Critérios e versões por origem (JSON)');
const variants = JSON.parse(original[index]);
for (const variant of variants) {
variant.confirmacao_manual = { recebida_em: '2026-10-10', fonte: 'Mensagem do responsável nesta conversa',
conta: 'não indicada', perfil: 'não indicado', dispositivo: 'não indicado',
criterios: scope[id].map(criterio => ({ criterio, estado: 'confirmado manualmente', ambito: 'piloto agronegócio QA' })),
exclusoes: ['móvel', 'acessibilidade', 'outros perfis', 'C37 conta18', 'cenários A/B', 'cobertura integral'] };
}
updated[index] = JSON.stringify(variants);
} else if (filename !== 'matriz-requisitos.csv') {
const prepared = csvUpdates[filename][i - 1];
assert(prepared);
for (let c = 0; c < headers.length; c++) {
if (prepared[headers[c]] !== original[c]) {
const allowed = filename === 'legado-inventario.csv' ? ['Destino proposto', 'Situação']
: filename === 'inventario-fontes.csv' ? ['SHA256', 'Bytes', 'Nota de leitura', 'Edição/versão identificada'] : ['Situação'];
assert(allowed.includes(headers[c]), 'Source identity, authors, dates, hashes and URL aliases are immutable');
updated[c] = prepared[headers[c]];
}
}
}
if (JSON.stringify(original) !== JSON.stringify(updated)) {
sheet.getRangeByIndexes(i, 0, 1, headers.length).values = [updated];
changes.push({ row: i + 1, id, columns: headers.filter((_, c) => original[c] !== updated[c]) });
strings[i] = updated;
}
}
workbook.recalculate();
const observed = sheet.getRangeByIndexes(0, 0, sourceRows.length, headers.length).values;
assert(observed.every((row, i) => row.every((value, c) => matchesSource(value, strings[i][c]))), 'Authored fields must match verified values');
const final = strings;
for (let i = 0; i < sourceRows.length; i++) {
for (let c = 0; c < headers.length; c++) {
if (sourceRows[i][c] === final[i][c]) continue;
const id = filename === 'matriz-requisitos.csv' ? sourceRows[i][0] : null;
assert(changes.some(change => change.row === i + 1 && change.columns.includes(headers[c])));
if (id) assert(scope[id] && allowedColumns.includes(headers[c]));
}
}
// CSV has no workbook layout, formulas or styles; serialize the verified cell strings
// in the repository's established BOM/CRLF CSV format rather than adding an XLSX matrix.
const output = encodeCSV(final);
const temp = path.join(RUNTIME, filename + '.verified');
await fs.writeFile(temp, output, 'utf8');
outputs.push({ filename, temp, changes });
}
// Apply only after ALL three imported tables have passed exact preservation checks.
for (const output of outputs) await fs.copyFile(output.temp, path.join(DOC, output.filename));
await fs.writeFile(path.join(RUNTIME, sourcesOnly ? 'csv-source-update-proof.json' : 'csv-update-proof.json'), JSON.stringify({ database_access: false, changed: outputs.map(({ filename, changes }) => ({ filename, changes })) }, null, 2));
console.log(JSON.stringify(sourcesOnly ? { source_metadata_rows_updated: outputs[0].changes.length, database_writes: 0 }
: { matrix_rows_changed: outputs[0].changes.map(c => c.id), statuses_changed: 0,
inventory_rows_reconciled: outputs[1].changes.length, article_versions_reconciled: outputs[2].changes.length, database_writes: 0 }));
+51
View File
@@ -0,0 +1,51 @@
"""Polish the new report block only; never rewrite historical sources or the matrix."""
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'
fixes = {'sem substituirI0': 'mantendo I0', 'mesmosIDs': 'mesmos IDs', 'os50': 'os 50', 'nãoimportados': 'não importados',
'GOVERNADOR-in.gif404': 'GOVERNADOR-in.gif (404)', 'outroslinks': 'outros links', 'comfundamentoou': 'com fundamento ou',
'módulosQA': 'módulos QA', 'nohistóricoQA49': 'ausência de histórico QA49', 'provaHTTP': 'prova HTTP',
'decisões/external/publicação': 'decisões externas/aprovação/publicação', 'compromissosSLA': 'compromissos SLA',
'Plano global030000': 'Plano global 030000', '1artigo+1imagem': '1 artigo +1 imagem', '3associações': '3 associações',
'0oportunidades ou geometrias criadaspeloartigo': '0 oportunidades ou geometrias criadas pelo artigo',
'0contas/dossiersQAtransferidos': '0 contas/dossiers QA transferidos', 'noopssehash/ID': 'reutilização se hash/ID',
'Reversão:ocultar': 'Reversão: ocultar', 'BackupQA': 'Backup QA', 'biblioteca existente; associação': 'biblioteca existente; associação',
'sóaplicaQA': 'só aplica QA', 'mesmo comflagtrue': 'mesmo com flag true', 'publicadas/recupe­radas': 'publicadas/recuperadas'}
for name in ['backlog.md', 'relatorio-executivo.md', 'plano-ativacao.md', 'casos-reproducao.md']:
path = DOC / name
text = path.read_text(encoding='utf-8')
begin = text.index('<!-- qa-legacy-20261010:begin -->')
end = text.index('<!-- qa-legacy-20261010:end -->')
block = text[begin:end]
for source, replacement in fixes.items(): block = block.replace(source, replacement)
text = text[:begin] + block + text[end:]
path.write_text(text, encoding='utf-8')
path = DOC / 'legado-piloto-qa.md'
text = path.read_text(encoding='utf-8')
if '## Verificação final deste lote' not in text:
text += '''
## Verificação final deste lote
Leitura ativa renovada em10/10/2026, com transação READ ONLY: zero documentos do legado, piloto indisponível e bundle ativo inalterado. Ausentes exatamente as extensões `2026_10_09_030000_extend_opportunity_resources`, `2026_10_10_040000_create_portal_profile_authorization` e `2026_10_09_190000_prepare_investor_path`. Os190000 fiscal(03/10) eBalcão-base(07/10) instalados não substituem09/190000. Recibo `legacy-active-readonly.json`.
Persistência: um documento por hash; publicação e revisões documentais auditadas; ClamAV1.5.4 registado nos dois originais. Dossier criado e dossier continuado têm uma referência e um evento cada; projeto independente conserva zero referências/eventos. `legacy-persistence-qa.json`.
Restauro QA renovado em10/10/2026: **295 tabelas e81 ficheiros, zero ausentes**, base de restauro distinta `profile_restore_20261010_133520_test:55441`. Ensaio QA, sem substituir backup da futura base ativa/candidata. Recibo `profile-installation-backup-qa.json`. Build QA, TypeScript e análise estática passaram; oito testes específicos da biblioteca/percurso passaram, além dos ensaios HTTP por critério. Candidata não fixada nem ativação autorizada.
'''
path.write_text(text, encoding='utf-8')
sources = DOC / 'inventario-fontes.csv'
rows = list(csv.DictReader(sources.open(encoding='utf-8-sig')))
for row in rows:
if row['Exemplar/caminho'].replace('\\', '/').endswith('/legado-piloto-qa.md'):
data = path.read_bytes(); sha = hashlib.sha256(data).hexdigest()
if row['SHA256'] != sha:
row['Edição/versão identificada'] += ' | exemplar anterior SHA256=' + row['SHA256']
row['SHA256'] = sha; row['Bytes'] = str(len(data))
with sources.open('w', encoding='utf-8-sig', newline='') as f:
w = csv.DictWriter(f, fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)
print(json.dumps({'reports_updated': 5, 'matrix_preserved': True}))
@@ -0,0 +1,117 @@
"""Document a narrowly scoped user confirmation and sector preparation in the existing audit."""
import collections
import csv
import hashlib
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOC = ROOT / 'docs/investmoz/v2/auditoria-portal'
OUT = ROOT / 'storage/app/private/investmoz-v2/portal-audit/legacy-sector-reconciliation-20261010'
csv.field_size_limit(5_000_000)
r = json.loads((OUT / 'reconciliation.json').read_text(encoding='utf-8'))
preview = json.loads((OUT / 'import-preview.json').read_text(encoding='utf-8'))
content = json.loads((OUT / 'content-check-after.json').read_text(encoding='utf-8'))
assert preview['exit_code'] == 0 and preview['transaction_read_only'] and preview['preview_items'] == 22
assert content['result'] == 'passed' and not content['database_access'] and not content['interface_accepted']
def section(filename, marker, value):
file = DOC / filename
text = file.read_text(encoding='utf-8')
text = re.sub(r'\n*<!-- ' + marker + r':begin -->.*?<!-- ' + marker + r':end -->\n*', '\n\n', text, flags=re.S)
first, rest = text.split('\n', 1)
file.write_text(first + '\n\n<!-- ' + marker + ':begin -->\n' + value.strip() + '\n<!-- ' + marker + ':end -->\n\n' + rest.lstrip(), encoding='utf-8')
manual = '''## Confirmação manual recebida do responsável — 10/10/2026
Confirmado nesta conversa: **leitura pública → autenticação → criar/escolher dossier → guardar referência → nova sessão → recuperar dossier e abrir artigo**. Conta, perfil, dispositivo e hora de execução não foram indicados; não os presumir. “Criar/escolher” conserva o relato do responsável, sem declarar que ambas as alternativas foram ensaiadas separadamente.
| Critério confirmado manualmente | Correspondência na matriz existente |
| --- | --- |
| Leitura pública e autenticação ao entrar no percurso privado | AUD-LEG-03 e AUD-LEG-04 |
| Criar/escolher dossier e guardar referência | AUD-LEG-03, AUD-LEG-06; gravação/consumidor em AUD-LEG-07 |
| Nova sessão, recuperação do dossier e abertura do artigo | AUD-LEG-03, AUD-LEG-06 e AUD-LEG-07 |
Só estes subcritérios receberam a confirmação manual. Nenhum estado global foi alterado, nenhuma API/persistência recebeu prova técnica adicional por inferência e nenhuma linha foi acrescentada à matriz. O relato manual fica separado das provas Chromium/API/READ ONLY anteriores.
**Continuam pendentes:** ensaios manuais móveis, acessibilidade, outros perfis e cobertura integral do legado. As observações automáticas anteriores de viewport/teclado/QA23 ficam preservadas como provas limitadas; não constituem essas aceitações manuais. C37 da conta18 e os cenários A/B completos não foram encerrados. A demonstração não renova backup/restauro nem resolve C71.
'''
rows = []
for s in r['sector_summaries']:
desc = {
'energia-mineracao': 'Índice com ligações explícitas a Energia172679 e Mineração172674; preservar as três identidades e os setores existentes2/8/7.',
'turismo': 'Artigo setorial e 11 artigos territoriais explicitamente ligados; categoria Turismo preservada, sem inferir geometrias.',
'infraestruturas': 'Artigo integral; C72 removia um título h1. Normalização h1→h2 preparada e conferida sobre o original.',
'mercado-financeiro': 'Corpo vazio no exemplar atual. Arquivar metadados/original; não inventar texto nem substituir por Fintech.'
}[s['slug']]
rows.append(f"| [{s['title']}]({s['url']}) | {s['legacy_id']} / setor QA{s['master_sector']['id']} (`{s['master_sector']['slug']}`) | {desc} |")
sectors = '''## Reconciliação dos quatro restantes setores — preparação, sem importação
As 92 entradas do inventário existente foram conferidas antes da seleção. Os quatro artigos de entrada foram novamente recolhidos por leitura anónima: HTTP200 e corpo/data de modificação iguais ao exemplar inicial. Os artigos descendentes e imagens usam os snapshots já inventariados, com hash conferido. Sitemap misto, paginação e restante acervo continuam abertos.
| Fonte legada | Identidade e cadastro existente | Conteúdo e lacuna concreta |
| --- | --- | --- |
''' + '\n'.join(rows) + '''
**Lote privado preparado: 17 versões de artigos + 5 imagens = 22 documentos.** Inclui quatro entradas setoriais, dois artigos de Energia/Mineração e 11 artigos territoriais de Turismo. A imagem de fundo genérica é um único exemplar por bytes, referenciado por vários artigos; não representa a fotografia de cada território. URLs com/sem barra são conservados como aliases do mesmo ID/versão/hash. Autoria, publicação, alteração, classificação original e HTML ficam preservados.
Os setores foram confrontados com a preservação READ ONLY da QA de10/10/2026 às14:54:19UTC, não com a base ativa. Correspondência2/3/4/6 para as entradas setoriais;8/7 para Energia/Mineração. Nenhum setor, identidade, artigo ou oportunidade foi criado na base. Alterações do corpo com a mesma identidade/versão deverão gerar conflito, não substituição silenciosa.
**Prévia real do importador existente:** 22 candidatos, zero duplicados por bytes e zero conflitos de identidade encontrados; transação READ ONLY no alvo `desk_restore_20261009_174726_test:55441`. CLI guardado, sem `--apply`, sem contas ou privilégios escolhidos. Não é aplicação/publicação autenticada. Nenhum scan novo foi executado ou declarado limpo; os scans do piloto não se estendem a este lote. C71 impede a aplicação enquanto faltar integridade/backup/restauro QA.
Reutilização concreta: biblioteca `documents`/`portal_resources`, importador `documents:import-batch`, verificador de binários, `portal_resource_links`, `LegacyContent`, controlador e componente `portal/legacy`, referências cifradas/eventos do dossier. Rotas de leitura/entrada, identidade do artigo e rótulos do componente ainda estão fixos em agronegócio: parametrizar esses pontos antes de disponibilizar outras páginas. As URLs legadas não são rotas QA disponíveis por estarem nesta tabela. Não criar um cadastro de conteúdo paralelo.
**C72 — código preparado:** o sanitizador removia o texto do h1 interno de Infraestruturas. A correção transforma-o em h2 e aplica a limpeza normal dos seus filhos/atributos. Os 17 originais conservaram hash; transformação e remoção de markup ativo verificadas sem bootstrap de aplicação ou acesso à base. Resultado não equivale a apresentação ou percurso dos novos setores demonstrado em QA.
Próximo incremento, após recuperar C71: prévia renovada no alvo e aplicação como rascunhos privados → ClamD por ficheiro → metadados/relações por citação e divulgação separada → parametrizar adaptador/componente e URLs preservadas → ensaios públicos e de dossier por setor. Mercado Financeiro mantém corpo vazio visível/identificado até existir conteúdo real; a consulta/cadastro do exemplar não exige uma relação a módulo/regra. Nenhuma importação ativa ou cálculo fiscal é habilitado.
Provas privadas: `legacy-sector-reconciliation-20261010/reconciliation.json`, `import-preview.json`, `content-check-before.json`, `content-check-after.json`; manifesto de staging `legacy-setores-20261010/manifest.json`. Inventários únicos atualizados: `legado-inventario.csv` e `legado-conteudos-reconciliados.csv`. Permanecem92 URLs/50 versões de artigos;22 entradas reconciliadas e17 versões preparadas neste incremento, sem omitir as restantes.
'''
section('legado-piloto-qa.md', 'legacy-manual-sectors-20261010', manual + '\n' + sectors)
short = '''## Confirmação manual e reconciliação setorial — 10/10/2026
Responsável confirmou leitura pública → autenticação → criar/escolher dossier → guardar referência → nova sessão → recuperar dossier e abrir artigo. AUD-LEG-03/04/06/07 receberam apenas os subcritérios correspondentes; estados e contagens da matriz inalterados. Conta/perfil/dispositivo não indicados. Móvel, acessibilidade, outros perfis, cobertura integral, C37/QA18 e A/B continuam pendentes.
Quatro setores reconciliados: Energia & Mineração (índice + dois artigos), Turismo (artigo +11 referências territoriais), Infraestruturas (título corrigido em código) e Mercado Financeiro (corpo vazio preservado). **17 artigos +5 imagens** preparados; prévia CLI em transação READ ONLY:22 candidatos,0 duplicados,0 conflitos; nenhuma aplicação/scan/publicação nova. C72 corrigido na transformação dos originais, sem prova visual dos novos setores. C71 continua P0: recuperar integridade/backup/restauro antes de aplicação QA. Biblioteca/entidades/componentes existentes reutilizados; rotas dos restantes setores ainda por parametrizar. Ver `legado-piloto-qa.md` e inventários existentes.
'''
section('relatorio-executivo.md', 'legacy-manual-sectors-20261010', short)
section('backlog.md', 'legacy-manual-sectors-20261010', short + '''
| Prioridade | Próxima ação | Aceitação exigida |
| --- | --- | --- |
| P0 / C71 | Recuperar cluster QA preservando dados e renovar backup/restauro. | Conteúdos/schema/sequências/ficheiros conferidos; guarda, chave e percursos recuperados. |
| P1 / LEG01/05/06/07 | Renovar prévia dos22 exemplares, importar como rascunhos privados após C71, scan e parametrizar leitura/contexto por identidade. | Sem duplicações; fontes/URLs preservadas; publicação separada; origem/referência recuperadas por setor. |
| P1 / LEG02/07 / C72 | Verificar o título preservado de Infraestruturas na página real após importação. | Conteúdo integral legível; original/hash imutáveis; markup ativo removido. Verificação de transformação já passada. |
| P1 / Mercado Financeiro | Manter corpo vazio identificado e procurar no acervo um exemplar/conteúdo real. | Sem conteúdo inventado ou Fintech usado como substituto silencioso. |
| P1 / aceitação pendente | Ensaios móveis, acessibilidade e outros perfis, C37/QA18, A/B e restante acervo. | Evidência própria por percurso/perfil; relato manual do piloto não os encerra. |
''')
section('casos-reproducao.md', 'legacy-sector-heading-20261010', '''## C72 — título original removido na projeção de Infraestruturas
**Requisitos:** AUD-LEG-02/07; P1 na expansão. **Reprodução:** aplicar `LegacyContent::sanitize()` ao HTML original do post171992, através de `tools/legacy-sector-content-check.php before`. **Esperado:** título “2. Sectores Prioritários de Investimento” preservado sem introduzir segundo h1 de página. **Observado:** título ausente; os restantes16 exemplares do lote não apresentavam essa perda. **Causa:** lista de apresentação não admitia h1 e removia o elemento com todos os filhos. **Correção preparada:** normalizar h1 interno para h2 e manter limpeza de atributos/filhos. **Prova:** verificação after sobre17 originais, hashes preservados, título presente e markup ativo removido; nenhum acesso à base. **Pendente:** importar o exemplar privado depois de C71, divulgar separadamente e demonstrar a página real. Não atualizar outros critérios da matriz com esta prova de transformação, nem declarar o setor publicado. C37/QA18 continua independente.
''')
section('plano-ativacao.md', 'legacy-manual-sectors-20261010', '''## Complemento após confirmação manual — preparação QA
O relato manual do piloto confirma somente a sequência registada em AUD-LEG-03/04/06/07. C37/QA18, A/B, outros perfis, móvel/acessibilidade e cobertura integral permanecem pendentes. C71 continua a impedir novas aplicações QA e a fixação de candidata.
O lote seguinte tem previsão de **17 artigos +5 imagens**, biblioteca existente e zero oportunidades/territórios/contas criados. A prévia READ ONLY encontrou22 candidatos novos; reconfirmar contagens/identidades no alvo após recuperação. Arquivo do exemplar vazio de Mercado Financeiro não significa artigo educativo completo. Relações do Turismo são textuais/editoriais; não importam geometrias.
Sequência global030000 →040000 →09/190000 preservada, sem repetir instalações. Após recuperar QA: aplicação privada/scan, parametrização dos consumidores e divulgação separada; depois ensaios específicos. Ativação futura requer revisão candidata, prova da base ativa por leitura e backup/restauro próprio, além da autorização separada. Reversão conserva originais, versões, referências e eventos; retirar divulgação/desabilitar disponibilização não elimina fontes dos dossiers. Nenhuma aplicação ativa nesta frente.
''')
# Prepare source-inventory metadata; its CSV is authored by the same JS pipeline.
sources_path = DOC / 'inventario-fontes.csv'
sources = list(csv.DictReader(sources_path.open(encoding='utf-8-sig', newline='')))
for name in ['legado-piloto-qa.md', 'legado-inventario.csv', 'legado-conteudos-reconciliados.csv']:
file = DOC / name
data = file.read_bytes()
row = next(row for row in sources if row['Exemplar/caminho'].replace('\\', '/').endswith('/' + name))
old, new = row['SHA256'], hashlib.sha256(data).hexdigest()
if old != new:
row['Edição/versão identificada'] += ' | exemplar anterior SHA256=' + old
row.update({'SHA256': new, 'Bytes': str(len(data)), 'Nota de leitura': 'Confirmação manual limitada e reconciliação dos quatro setores;22 candidatos privados, sem importação/publicação; C71/C37/A-B/móvel/acessibilidade/outros perfis preservados.'})
(OUT / 'source-inventory-updates.json').write_text(json.dumps(sources, ensure_ascii=False), encoding='utf-8')
(OUT / 'csv-source-baseline.json').write_text(json.dumps({'inventario-fontes.csv': list(csv.reader(sources_path.open(encoding='utf-8-sig', newline='')))}, ensure_ascii=False), encoding='utf-8')
print(json.dumps({'manual_confirmed_requirements': ['AUD-LEG-03', 'AUD-LEG-04', 'AUD-LEG-06', 'AUD-LEG-07'], 'statuses_changed': 0, 'database_writes': 0, 'sectors_reconciled': 4, 'new_imports': 0}))
+151
View File
@@ -0,0 +1,151 @@
"""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))
@@ -0,0 +1,154 @@
"""Reconcile C85 and the confirmed QA18 logout into the existing audit only.
CSV cells are prepared as JSON and authored by the existing Artifact Tool writer.
"""
import collections
import csv
import hashlib
import json
import re
import time
from datetime import datetime, timezone
from pathlib import Path
csv.field_size_limit(16000000)
ROOT = Path(__file__).resolve().parent.parent
DOC = ROOT / 'docs/investmoz/v2/auditoria-portal'
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
OUT = PRIVATE / 'logout-finalize-20261010'
OUT.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc).isoformat()
tag = '[C85/logout QA 10/10/2026] '
proof = json.loads((PRIVATE / 'portal-logout-chromium-20261010.json').read_text())
assert proof['result'] == 'passed' and all(proof['checks'].values())
assert proof['active_writes'] == 0 and proof['checks']['no_qa18_mfa_consumed']
assert len(proof['presentation']) == 6 and len(proof['logout']) == 2
# Retain only non-secret trace fields for the manually confirmed QA18 logout.
traces = []
for file in PRIVATE.glob('auth-session-trace-qa*.jsonl'):
for attempt in range(10):
try:
lines = file.read_text(encoding='utf-8').splitlines()
break
except PermissionError:
time.sleep(.1)
for line in lines:
try:
row = json.loads(line)
except json.JSONDecodeError:
continue
if row.get('at_utc', '') < '2026-10-10T18:09:00' or row.get('path') not in ['/logout', '/login', '/two-factor-challenge', '/investimentos']:
continue
phases = {name: {key: (row.get(name) or {}).get(key) for key in ['authenticated_user', 'pending_mfa_user', 'stored_web_user']} for name in ['loaded', 'after', 'persisted']}
if not any(18 in state.values() for state in phases.values()):
continue
traces.append({key: row.get(key) for key in ['at_utc', 'method', 'path', 'status', 'redirect']} | phases)
traces.sort(key=lambda row: row['at_utc'])
logout = next(row for row in reversed(traces) if row['method'] == 'POST' and row['path'] == '/logout' and row['loaded']['authenticated_user'] == 18)
assert logout['status'] == 302 and logout['redirect']['path'] == '/'
assert logout['after']['authenticated_user'] is None and logout['persisted']['stored_web_user'] is None
post_logout_mfa = any(row['at_utc'] > logout['at_utc'] and row['path'] == '/two-factor-challenge' and row['method'] == 'POST' and row['after']['authenticated_user'] == 18 for row in traces)
manual = {'recorded_at': stamp, 'source': 'Responsável: «já tem o botão e fiz o logout»; rastreio sanitizado QA18097',
'target': proof['target'], 'actor_id': 18, 'button_and_logout_confirmed_by_user': True,
'logout_at_utc': logout['at_utc'], 'session_authenticated_after_logout': False,
'manual_post_logout_mfa_observed': post_logout_mfa, 'manual_f5_confirmed': False,
'events': traces, 'active_writes': 0, 'secrets_recorded': False}
(OUT / 'manual-qa18-logout.json').write_text(json.dumps(manual, ensure_ascii=False, indent=2), encoding='utf-8')
readiness_path = ROOT / 'storage/app/private/investmoz-v2/portal-profile-installation/readiness.json'
backup = json.loads(readiness_path.read_text())
checks = json.loads((PRIVATE / 'profile-installation-checks-qa.json').read_text())
assert backup['code_hash'] == checks['code_hash'] and backup['restore_verified'] and backup['files_restore_verified']
alias = PRIVATE / 'profile-installation-backup-qa.json'
if alias.exists():
previous = OUT / 'previous-profile-installation-backup-qa.json'
if not previous.exists():
previous.write_bytes(alias.read_bytes())
alias.write_bytes(readiness_path.read_bytes())
evidence = ('C85: ação ausente na navegação do percurso, apesar de existir no menu do dashboard. '
'Navegação comum passa a mostrar conta e Terminar sessão, usando POST /logout do Fortify e limpeza do cache Inertia. '
'Responsável confirmou botão e logout QA18; rastreio ' + logout['at_utc'] + ': auth18 → null, persistência null, 302 → /. '
'Chromium QA21 demonstrou logout direto do dossier desktop/móvel, API privada401, página privada→login, novo contexto com MFA normal e dossier inalterado; seis verificações de apresentação/teclado/reflow 320/390/1440px sem violações automáticas no cabeçalho. '
'Provas: portal-logout-chromium-20261010.json e logout-finalize-20261010/manual-qa18-logout.json. '
'MFA, chaves e QA18 preservados; bundle ativo sem alteração. Não substitui A/B ou outros perfis.')
gap = ('C37: logout manual QA18 demonstrado; F5 explícito e novo login/MFA QA18 após este logout ainda por confirmar. '
'Nova sessão Chromium QA21 demonstrada separadamente. Restantes consumidores, perfis, critérios A/B, móvel/acessibilidade integral e ativação não encerrados por este ensaio.')
ids = {'AUD-UI-01', 'AUD-GUIDE-01', 'AUD-DASH-01', 'AUD-PROFILE-24', 'AUD-PROFILE-25'}
files = []
for filename in ['matriz-requisitos.csv', 'backlog-priorizado.csv']:
source = DOC / filename
rows = list(csv.reader(source.open(encoding='utf-8-sig', newline='')))
headers = rows[0]
found = set()
patches = []
for index, row in enumerate(rows[1:], 1):
old = dict(zip(headers, row))
rid = old['ID']
if rid not in ids:
continue
found.add(rid)
if filename.startswith('matriz'):
values = {'Evidência funcional': evidence, 'Lacuna': gap,
'Interface': 'Terminar sessão visível no topo de investimentos/dossier/artigo/simulador; confirmação manual QA18 e Chromium QA21 desktop/móvel.',
'API': 'POST logout302→/; API privada401 e página privada→login após saída. Novo login+MFA21 normal numa sessão distinta.',
'Gravação': 'Sessão invalidada; documentos, dossier, resultados, etapas e tarefas guardados não modificados.',
'Recuperação': 'Dossier QA21 recuperado noutra sessão com o mesmo hash do snapshot de case/results/steps/tasks; F5 manteve acesso.',
'Resultado apresentado': 'Conta atual identificada, botão acessível por teclado; após logout consulta pública sem botão e acesso privado exige login.',
'Consolidação': 'Subcritérios C85 e logout manual C37 atualizados; estado global, fontes, versões e decisões preservados.'}
variants = json.loads(old['Critérios e versões por origem (JSON)'])
for variant in variants:
variant['incremento_logout_qa_20261010'] = {'atualizado_em': stamp, 'evidencia': evidence, 'lacuna_atual': gap, 'ativacao': False}
values = {field: old[field] + ' | ' + tag + value for field, value in values.items()}
values['Critérios e versões por origem (JSON)'] = json.dumps(variants, ensure_ascii=False, separators=(',', ':'))
else:
values = {'Pendência': gap, 'Correção/componentes': 'C85 corrigido no PortalGroupedMenu/UserMenuContent; Fortify e proteção de sessão reutilizados. Não contornar MFA nem permissões.',
'Aceitação': 'Logout manual18 demonstrado. Confirmar F5 e novo login/MFA18 após saída; provas QA21 independentes preservadas.'}
values = {field: old[field] + ' | ' + tag + value for field, value in values.items()}
patches.append({'row': index, 'values': values})
assert found == ids and len(rows) == 3394
files.append({'filename': filename, 'input_sha256': hashlib.sha256(source.read_bytes()).hexdigest(), 'rows': rows,
'patches': patches, 'allowed_columns': sorted({field for patch in patches for field in patch['values']}), 'append': []})
(OUT / 'csv-edits.json').write_text(json.dumps({'files': files}, ensure_ascii=False), encoding='utf-8')
section = f'''## Logout disponível e confirmação manual — {stamp}
**C85 corrigido em QA; nenhuma alteração no ativo.** A ação existia no menu de utilizador do dashboard, mas não na navegação do investidor. O cabeçalho comum mostra agora a conta atual e **Terminar sessão**, sem obrigar a regressar ao dashboard. Reutiliza POST `/logout` do Fortify e limpa o cache Inertia; chaves, MFA, perfis e dados guardados foram preservados.
O responsável confirmou o botão e o logout. Rastreio QA18 às **{logout['at_utc']}** (20:17:15 em Moçambique): POST logout302→/, conta18→sem autenticação e estado persistido vazio. C37 recebe apenas esse critério; F5 explícito e novo login/MFA QA18 após a saída continuam pendentes. O GET/login anterior com conta autenticada não era um novo login. A declaração geral de testes efetuados não substitui evidências dos restantes critérios.
Chromium QA21, contextos isolados: ação visível em investimentos, dossier, artigo e simulador; logout direto do dossier em desktop e 320px; API privada401 e página privada→login; nova sessão com MFA normal recuperou case/results/steps/tasks com hash igual e F5 manteve acesso. Seis verificações de cabeçalho em 320/390/1440px sem violações automáticas ou overflow; foco por teclado demonstrado. Não consumiu códigos QA18. Não encerra A/B, leitor de ecrã ou acessibilidade integral.
Provas sanitizadas: `portal-logout-chromium-20261010.json` e `logout-finalize-20261010/manual-qa18-logout.json`. Cinco requisitos receberam subcritérios; permanecem **3393 requisitos/3393 linhas/6845 ocorrências**, com estados globais inalterados.
Regressão de instalador/publicação: 9 testes/93 asserções e TypeScript passaram. Backup/restauro QA renovado **{backup['verified_at']}**, 295 tabelas/103 ficheiros, em `{backup['restored_target']['database']}:55441`, associado ao novo fingerprint que inclui os dois menus. O backup ativo READ ONLY de 17:48:52 UTC permanece um retrato anterior; candidata global, compatibilidade/reversão no clone do ativo e ativação continuam pendentes. Preservar **030000 → 040000 → 09/190000**, sem repetir instalações QA nem copiar contas/dossiers de teste.
As afirmações históricas abaixo sobre logout manual ainda por testar são superadas somente por esta confirmação; nenhuma aceitação adicional inferida.
'''
def prepend(filename, key, content):
file = DOC / filename
old = file.read_text(encoding='utf-8-sig')
pattern = re.compile(r'<!-- ' + re.escape(key) + r':begin -->.*?<!-- ' + re.escape(key) + r':end -->\s*', re.S)
old = pattern.sub('', old)
file.write_text(f'<!-- {key}:begin -->\n{content}\n<!-- {key}:end -->\n\n' + old, encoding='utf-8')
prepend('relatorio-executivo.md', 'qa-logout-results-20261010', section)
prepend('backlog.md', 'qa-logout-results-20261010', section)
prepend('casos-reproducao.md', 'qa-logout-c85-20261010', '''## C85 — logout ausente no percurso do investidor
- **Reprodução:** autenticar QA18, abrir `/investimentos` ou um dossier; procurar logout no cabeçalho. Não havia botão; era preciso conhecer o menu do dashboard.
- **Esperado:** terminar sessão diretamente no percurso, em desktop/móvel e por teclado; dados guardados preservados e acesso privado negado após saída.
- **Causa:** PortalNavigation usa PortalGroupedMenu sem ação de sessão; o logout de UserMenuContent só era apresentado pelo AppLayout do dashboard.
- **Correção QA:** conta e botão Terminar sessão no cabeçalho comum; POST Fortify/CSRF existentes, limpeza do cache Inertia, sem alteração de guard, MFA ou privilégios.
- **Evidência:** responsável confirmou botão/logout QA18; trace18 POST/logout302→/ com auth e persistência vazias. Chromium21 desktop/móvel, API401, página privada→login, nova sessão MFA e snapshot do dossier iguais. Cabeçalho acessível por teclado, sem overflow ou violações automáticas nos seis ensaios.
- **Estado:** causa corrigida e logout manual demonstrado. C37 continua limitado pelos critérios F5 explícito e novo login/MFA18 após saída; provas21 não substituem conta18. Provas sanitizadas e 0 escritas ativas no relatório atual.
''')
prepend('plano-ativacao.md', 'qa-logout-activation-20261010', f'''## Incremento C85 — revisão QA, sem ativação
Menus de sessão adicionados ao fingerprint; preparação anterior ligada ao hash antigo preservada. Checagens e backup/restauro QA renovados em {backup['verified_at']} para o código atual: 295 tabelas/103 ficheiros em `{backup['restored_target']['database']}:55441`; nenhum novo recurso, migração, titular ou grant necessário para o botão. Bundle ativo permaneceu com SHA256 `{proof['active_build_sha256']}`.
Correção futura: disponibilizar componente/build compatíveis, após revisão candidata e ensaios correspondentes; preservar a ordem 030000→040000→09/190000 e executar somente instalações ausentes. Não repetir instalações QA por causa do botão. Reversão do incremento UI: regressar ao bundle/componentes anteriores, preservando dados e MFA; não desfazer schema nem recuperar uma chave distinta. Backup ativo READ ONLY de 17:48:52 UTC não é aceitação da nova candidata; verificar compatibilidade e reversão no clone e renovar o retrato antes de qualquer corte autorizado. C37 manual parcialmente confirmado; sem autorização de ativação ativa.
''')
print(json.dumps({'prepared_tables': len(files), 'updated_requirements': len(ids), 'manual_qa18_logout': True, 'manual_post_logout_mfa_observed': post_logout_mfa, 'active_writes': 0}))
+20 -6
View File
@@ -17,8 +17,12 @@ 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'}
access_failure = {'AUD-DASH-01', 'AUD-UI-01'}
# 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'})
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']
@@ -34,16 +38,16 @@ def group(row):
next_actions = {
'AUD-DASH-01': 'Identificar URL/conta da sessão reportada; no servidorQA, gestor17: dashboard→Gestão de Oportunidades→Nova oportunidade→guardar→nova sessão→editar→arquivar→restaurar. Incorporar prova visualC43 antes de futura disponibilização dos atalhos.',
'AUD-UI-01': 'ManterC37 pendente; executar separadamente login/reload/logout/nova sessão investidor18 e os cenáriosA/B completos, depois as demais rotas/consumidores. Sem novo pedido de autorizaçãoQA.',
'AUD-OP-01': 'Executar roteiro visualC43 a partir do dashboard com gestorQA17/MFA; conferir mensagens, filtro archived,ID/histórico e público intacto.',
'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': 'Resolver188nomes(31distritos/157postos) por pai+edição/correspondência inequívoca; aplicar privadamenteQA, replaysem duplicação; percorrer cada seletor e limpar filhos ao mudar pai.',
'AUD-GEO-03': 'Verificar mapa por consumidor com393limites privados e permissões; distinguir referência/limite/perímetro; resolver31/157e disponibilidade de polígonos de localidades. Publicação separada.',
'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.',
@@ -62,6 +66,12 @@ next_actions = {
'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; completar consumidores e A/B com ferramenta/Balcão/acompanhamento, sem usar esta prova para encerrar C37.',
'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.',
@@ -83,6 +93,8 @@ def action(row):
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')])
@@ -92,6 +104,8 @@ page = '''<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewp
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')
+75
View File
@@ -0,0 +1,75 @@
"""Append a scoped response/§27 receipt to the existing matrix; no new inventory."""
import csv
import hashlib
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DOCS = ROOT / 'docs/investmoz/v2/auditoria-portal'
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
csv.field_size_limit(16 * 1024 * 1024)
proof = json.loads((PRIVATE / 'opportunity-response-http-qa.json').read_text(encoding='utf-8-sig'))
checks = json.loads((PRIVATE / 'profile-installation-checks-qa.json').read_text(encoding='utf-8-sig'))
assert proof['result'] == checks['result'] == 'passed' and proof['active_writes'] == 0 and not proof['interface_observed']
assert len(proof['responses']) == 10 and len(proof['semantics']) == 2
schema_sha = hashlib.sha256((ROOT / 'resources/data/investmoz-opportunity-modular.schema.json').read_bytes()).hexdigest()
assert schema_sha == '9ce0acfa5df1f81a3357dee9e5a9a1c4e77e4eefc8bce294497d18f4caa8e5c0'
matrix = DOCS / 'matriz-requisitos.csv'
with matrix.open(encoding='utf-8-sig', newline='') as source:
rows = list(csv.DictReader(source))
protected = ['ID', 'Pedido/trecho', 'Fonte/ocorrências', '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)', 'Estado']
before = [{k: r[k] for k in protected} for r in rows]
tag = '[Response/§27 QA 10/10/2026] '
values = {
'Implementação encontrada': 'Resposta de leitura versionada com IDs bigint/referências legados intactos, recursos UUID hidratados e metadados efetivamente gravados. Integra o editor existente e GET /minhas-oportunidades/{id}/response. Succession aceite na venda de negócio conforme contrato original; §27 recusa cadastro de parcela repetido na mesma oportunidade antes de submeter.',
'Evidência funcional': 'opportunity-response-http-qa.json: dez respostas recuperadas noutra sessão MFA e iguais ao consumidor Inertia; Validador permitido, organização alheia 403 e sessão anónima 401. QA40: rascunho duplicado recuperável, submissão 422 no campo cadastral, correção e submissão aceite. QA41: succession guardada, submetida e recuperada. Testes isolados específicos em OpportunityCanonicalWorkflowTest; nenhum ensaio visual.',
'Lacuna': 'Response normativo integral ainda parcial: algoritmo de completude, identidade/metadados de itens inline, correspondência explícita de estados e autor de alteração/exclusão de recursos. §27 conservado como fonte, sem criar tabelas especializadas paralelas. Quatro perfis e SLA não aprovados; condições de unidades/urbanismo/entidade sem exercícios mantêm decisões próprias.',
'API': 'GET privado /minhas-oportunidades/{id}/response; recurso autorizado antes de hidratar, Cache-Control private/no-store. Escritas existentes e validações 422 preservadas; nunca publica nem aprova por leitura.',
'Gravação': 'QA40 e QA41 sintéticos, usando oportunidades e coleções existentes. Nenhuma alteração das dez oportunidades anteriores, titulares, prazos ou base ativa.',
'Recuperação': 'Novas sessões Fortify/MFA recuperam IDs, referências, valores e metadados. Números cadastrais ausentes permanecem desconhecidos; sem renumerar UUIDs/bigints ou fabricar aprovação/completude.',
'Interface': 'Resposta entregue ao editor Inertia existente; interface, C37, Promotor QA21 e A/B continuam pendentes. Não confundir recuperação das props com clique ou render observado.'
}
for row in rows:
if row['ID'] == 'AUD-OP-04':
for key, value in values.items():
old = re.sub(r'\s*\|\s*' + re.escape(tag) + r'.*?(?=\s*\|\s*\[|$)', '', row[key], flags=re.S)
row[key] = old + ' | ' + tag + value
assert before == [{k: r[k] for k in protected} for r in rows]
assert len(rows) == 3386 and sum(int(r['Quantidade de linhas de origem']) for r in rows) == 6838
with matrix.open('w', encoding='utf-8-sig', newline='') as f:
writer = csv.DictWriter(f, fieldnames=list(rows[0])); writer.writeheader(); writer.writerows(rows)
marker = 'qa-response-20261010'
def prepend(path, content):
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')
path.write_text(first + '\n\n<!-- ' + marker + ':begin -->\n' + content.strip() + '\n<!-- ' + marker + ':end -->\n\n' + rest.lstrip(), encoding='utf-8')
summary = f'''## Incremento atual — Response/§27 e propostas para decisão
Somente QA **http://127.0.0.1:18097**, `{proof['target']}`. Mesmo inventário/matriz; nenhum fecho integral ou escrita ativa.
- **AUD-OP-04:** `/minhas-oportunidades/{{id}}/response` integra a leitura do editor existente. Preserva IDs, códigos, referências UUID e estados atuais. Dez casos #30–39 recuperados noutra sessão MFA; Validador permitido, organização alheia 403 e anónimo 401. A versão do adaptador declara expressamente que a resposta normativa completa continua parcial.
- **C65 / QA40:** §27 exige unicidade de número cadastral por oportunidade. Rascunho incompleto/duplicado permanece recuperável; submissão devolve 422 junto a `canonical.land_rights.parcels.1.cadastral_number`. Após correção, submissão aceite. Números ausentes não são inventados.
- **C66 / QA41:** `succession` constava no contrato original mas era recusada pela lista semântica de venda. Lista reconciliada; criação/edição, submissão e nova sessão demonstradas. Não alterar silenciosamente outras modalidades.
- **Ainda parcial:** algoritmo de completude, metadados/identidade de itens inline, equivalência explícita dos estados e recuperação dos autores de alteração/exclusão dos recursos. §27 não cria tabelas paralelas. Condições sem regra inequívoca continuam como decisões D-S1/S2.
Provas: `opportunity-response-http-qa.json`, `OpportunityCanonicalWorkflowTest`, `profile-installation-checks-qa.json` e schema original preservado. O recibo identifica separadamente a tentativa anterior em que passaram os dez critérios de leitura e o ensaio semântico posterior; falhas do harness foram conservadas, não apresentadas como falhas do portal.
**Propostas modulares e SLA continuam não aprovados.** O ficheiro `propostas-perfis-modulares-sla.md` explicita compromissos internos de atendimento, janela operacional do Promotor, pausas por tarefa, espera externa separada e ausência de decisão automática. Nenhum `due_at`, titular ou perfil modular ativado.
**Ativação:** o restauro reportado é QA. Para uma futura ativação, conferir a identidade, schema, dados e configuração efetivos da base ativa para a revisão candidata, capturar hashes/manifesto dessa revisão e provar o seu backup/restauro. As provas QA ou `ready=true` não substituem essa ligação. Sequência 030000 → 040000 → 190000; C37, QA21 e A/B visual pendentes, candidata não fixada.
**Próximo trabalho independente:** completar metadados/Response sem novos cadastros; PDFs gerados/exportações/derivadas por consumidor; países/origem com filtros e estatísticas por formulário; correspondências inequívocas, mantendo Mandie/Guro ambíguo. Cartografia permanece 408 resolvidas e 173 lacunas neste incremento.
'''
for name in ['relatorio-executivo.md', 'backlog.md', 'workflow-oportunidades-qa.md']:
prepend(DOCS / name, summary)
prepend(DOCS / 'plano-ativacao.md', '**Preparação da futura ativação.**' + summary.split('**Ativação:**', 1)[1].split('**Próximo trabalho independente:**', 1)[0])
case_block = '- **C65' + summary.split('- **C65', 1)[1].split('- **Ainda parcial:**', 1)[0]
prepend(DOCS / 'casos-reproducao.md', case_block + '\nC65 foi identificado por confronto código/§27; C66 por divergência da lista do servidor com o contrato. A prova atual é pós-correção. Não atribuir a estes casos falhas antigas não reproduzidas.')
prepend(ROOT / 'docs/investmoz/v2/gestao-oportunidades-dashboard.md', summary)
(PRIVATE / 'response-audit-update-qa.json').write_text(json.dumps({'requirements': 3386, 'source_occurrences': 6838, 'protected_fields_preserved': True,
'schema_sha256': schema_sha, 'states_unchanged': True, 'updated': ['AUD-OP-04'], 'active_writes': 0}), encoding='utf-8')
print(json.dumps({'requirements': 3386, 'source_occurrences': 6838, 'updated': ['AUD-OP-04'], 'states_unchanged': True, 'active_writes': 0}))
+269
View File
@@ -0,0 +1,269 @@
"""Prepare targeted edits and reconcile existing reports from completed QA receipts, no database access."""
import collections
import csv
import hashlib
import json
import re
from datetime import datetime, timezone
from pathlib import Path
csv.field_size_limit(16000000)
ROOT = Path(__file__).resolve().parent.parent
DOC = ROOT / 'docs/investmoz/v2/auditoria-portal'
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
OUT = PRIVATE / 'legacy-sectors-finalize-20261010'
OUT.mkdir(exist_ok=True)
def read(name):
return json.loads((PRIVATE / name).read_text(encoding='utf-8'))
http = read('legacy-sectors-http-qa.json')
browser = read('legacy-sectors-browser-qa.json')
persistence = read('legacy-sectors-persistence-qa.json')
imported = read('legacy-sectors-apply-qa.json')
replay = read('legacy-sectors-replay-qa.json')
backup = read('profile-installation-backup-qa.json')
copy = json.loads((PRIVATE.parent / 'portal-qa-recovery/20261010_154445/damaged-copy-proof.json').read_text())
origin = read('legacy-financial-origin-20261010/proof.json')
assert http['result'] == browser['result'] == 'passed'
assert backup['restore_verified'] and backup['files_restore_verified'] and backup['target'] == imported['target']
assert copy['all_names_sizes_hashes_equal'] and len(persistence['cases']) == 8
assert datetime.fromisoformat(backup['verified_at']) >= datetime.fromisoformat(browser['completed_at'].replace('Z', '+00:00'))
assert origin['html_http_status'] == origin['inertia_http_status'] == 200 and origin['initial_origin_content_length'] == origin['inertia_origin_content_length'] == 0
assert imported['before']['opportunities'] == imported['after']['opportunities'] == 49
assert replay['before'] == replay['after'] and len(imported['resources']) == 22
stamp = datetime.now(timezone.utc).isoformat()
tag = '[Setores QA após C71 10/10/2026] '
rows = list(csv.reader((DOC / 'matriz-requisitos.csv').open(encoding='utf-8-sig', newline='')))
headers = rows[0]
canonical = {r[0]: dict(zip(headers, r)) for r in rows[1:]}
assert len(canonical) == len(rows) - 1 == 3393
backup_tables = len(backup['tables'])
backup_files = len(backup.get('files', []))
assert backup_tables == 295 and backup_files == 103
evidence = ('22 originais importados privadamente após prévia renovada, hashes recuperados,22 scans ClamD reais e replay sem novos documentos/recursos/auditoria/scans. '
'Divulgação autenticada separada:16 artigos com texto+5 imagens públicos; Mercado Financeiro privado, vazio confirmado na origem HTML e Inertia200. '
'Chromium desktop:16 artigos, pesquisa/imagens, Energia e Mineração distintas, C72 título preservado; QA21 criou4 dossiers, retorno, logout/novaMFA/F5/replay; QA23 negado403 e anónimo sem contexto. '
'Persistência READ ONLY:8 dossiers HTTP/Chromium, uma referência/evento cada,22 hashes únicos,0 relações geográficas automáticas. '
'legacy-sectors-{apply,replay,http,browser,persistence}-qa.json; legacy-financial-origin-20261010/proof.json. C37/QA18,A/B,móvel/acessibilidade/cobertura integral não encerrados.')
ops_evidence = (f'C71 corrigido somenteQA: novo cluster íntegro em diretório estável, mesma porta55441/base;295 tabelas/34.902 linhas byte-a-byte e31sequências preservadas. '
f'Schema/owners/ACL/triggers comparados semanticamente;2 constraints varchar/text e slots de coluna normalizados, sem afirmar DDL byte-a-byte. '
f'Cluster danificado original e cópia retidos:{copy["files"]} ficheiros, todos nomes/bytes/hashes iguais. '
f'Backup/restauro posterior ao lote:{backup["verified_at"]},295tabelas/103ficheiros,restauro em{backup["restored_target"]["database"]}:55441. '
'APP_KEY/MFA/conta18 preservados. Históricos QA preservados fisicamente, não todos restaurados no novo runtime. Provas portal-qa-recovery/20261010_154445 e profile-installation-backup-qa.json. Candidata global não fixada.')
updates = {}
for requirement in ['AUD-OPS-01'] + [f'AUD-LEG-{i:02d}' for i in range(1, 8)]:
old = canonical[requirement]
update = {}
update['Evidência funcional'] = old['Evidência funcional'] + ' | ' + tag + (ops_evidence if requirement == 'AUD-OPS-01' else evidence)
update['Consolidação'] = old['Consolidação'] + ' | ' + tag + 'Mesmo ID/referências/versões. Subcritérios deste lote demonstrados emQA; decisões e aceitações independentes mantidas.'
variants = json.loads(old['Critérios e versões por origem (JSON)'])
for variant in variants:
variant['incremento_setores_qa_20261010'] = {'atualizado_em': stamp, 'ambito': 'QA18097;base55441isolada',
'evidencia': ops_evidence if requirement == 'AUD-OPS-01' else evidence,
'estado_requisito_atual': 'parcial' if requirement == 'AUD-OPS-01' else old['Estado'],
'exclusoes': ['ativação', 'C37/QA18', 'A/B', 'móvel', 'acessibilidade', 'cobertura integral']}
update['Critérios e versões por origem (JSON)'] = json.dumps(variants, ensure_ascii=False, separators=(',', ':'))
if requirement == 'AUD-OPS-01':
update['Estado'] = 'parcial'
update['Lacuna'] = ('C71 técnico resolvido emQA; falta aceitação global C37/QA18,A/B,demais consumidores e revisão candidata. '
'Futura ativação exige snapshot/backup/restauro próprios do ativo e ações autenticadas;CLI--actor é apenas atribuiçãoQA. '
'Não repetir030/040/09-190naQA. | Histórico da falha e lacunas anteriores: ' + old['Lacuna'])
update['Critério de aceitação'] = ('Revisão/destino atual, backup/restauro integral correspondente, guarda/compatibilidade/reversão e ativação autenticada única; '
'QA atual295tabelas/103ficheiros demonstrada. Não substitui prova ativa. | Critério de versão anterior preservado: ' + old['Critério de aceitação'])
update['Gravação'] = old['Gravação'] + ' | ' + tag + 'Recuperação em destino vazio isolado; comparações e backup/restauro verificados,0escritasativas.'
update['Recuperação'] = old['Recuperação'] + ' | ' + tag + ops_evidence
update['API'] = old['API'] + ' | ' + tag + 'QA18097/login200 após retirada da manutenção; alvo real55441confirmado.'
update['Resultado apresentado'] = old['Resultado apresentado'] + ' | ' + tag + 'Manutenção retirada; percursos setoriais recuperados. Aceitação QA18 permanece independente.'
else:
for field in ['Interface', 'API', 'Gravação', 'Recuperação', 'Resultado apresentado']:
update[field] = old[field] + ' | ' + tag + evidence
pending = {
'AUD-LEG-01': '18/50 versões observadas cadastradas:17comtexto consultáveis e1vazia privada;32versões ainda não importadas, sitemap/paginação/cobertura histórica eC68pendentes.',
'AUD-LEG-02': 'Identidade aplicada e desktop dos novos setores observado; acessibilidade sistemática/móvel e restantes conteúdos pendentes. Editor Assunto opcional C74 sem ensaio visual próprio.',
'AUD-LEG-03': 'Quatro percursos setoriais com retorno e nova sessão demonstrados; relações territoriais concretas, ferramenta/Balcão/acompanhamento deA/B e demais perfis continuam independentes.',
'AUD-LEG-04': 'Consulta pública/proteção privada dos setores demonstradas. Disponibilização ativa,C37/QA18,A/B e aceitações manuais adicionais permanecem separadas.',
'AUD-LEG-05': 'Mesma biblioteca/versão/imagem reutilizada. Turismo editorial sem geometrias inferidas; relações territoriais e por etapa ainda exigem fundamento concreto.',
'AUD-LEG-06': '17caminhos comtexto preservados incluindo agronegócio e4novos retornos; restante mapaURL/aliases/acervo ainda incompleto. C37/QA18 eA/B não encerrados.',
'AUD-LEG-07': '22originais/5imagens/hashes/scans/replay e16novos artigos públicos testados;32versões restantes, anexos/links externos/C68,móvel/acessibilidade e perfis adicionais por testar.'}
update['Lacuna'] = pending[requirement] + ' | Histórico anterior: ' + old['Lacuna']
updates[requirement] = update
canonical[requirement].update(update)
packet = {'captured_at': stamp, 'files': []}
def table(name):
raw = (DOC / name).read_bytes()
return list(csv.reader(raw.decode('utf-8-sig').splitlines(keepends=True))), hashlib.sha256(raw).hexdigest()
def add(name, data, digest, patches, allowed, appended=None, source_hash_updates=None):
packet['files'].append({'filename': name, 'input_sha256': digest, 'rows': data, 'patches': patches,
'allowed_columns': allowed, 'append': appended or [], 'source_hash_updates': source_hash_updates or []})
data, digest = table('matriz-requisitos.csv')
add('matriz-requisitos.csv', data, digest, [{'row': i, 'values': updates[r[0]]} for i, r in enumerate(data) if i and r[0] in updates],
list(set(k for value in updates.values() for k in value)))
states = collections.Counter(r['Estado'] for r in canonical.values())
assert sum(states.values()) == 3393 and states['concluído e demonstrado'] == 7 and states['com falha'] == 0
data, digest = table('backlog-priorizado.csv')
bh = data[0]; patches = []; known = set()
for i, values in enumerate(data[1:], 1):
old = dict(zip(bh, values)); item = canonical[old['ID']]; known.add(old['ID'])
new = {'Prioridade': item['Prioridade'], 'Estado': item['Estado'], 'Pendência': item['Lacuna'],
'Dependência/pergunta': item['Dependência'], 'Aceitação': item['Critério de aceitação']}
if old['ID'] in updates:
new['Correção/componentes'] = 'Biblioteca/importador/LegacyContent/portal/legacy/percurso existentes; QA cluster/backup quandoOPS01; ver relatório atual.'
new['Ordem do próximo lote'] = 'C37/A-B e aceitações independentes; continuar expansão/documentos/países/cartografia sem reabrir fontes oficiais.'
changed = {k: v for k, v in new.items() if old[k] != v}
if changed: patches.append({'row': i, 'values': changed})
appended = []
for requirement, item in canonical.items():
if requirement not in known:
new = {'Prioridade': item['Prioridade'], 'ID': requirement, 'Estado': item['Estado'], 'Pendência': item['Lacuna'],
'Correção/componentes': item['Implementação encontrada'], 'Dependência/pergunta': item['Dependência'],
'Aceitação': item['Critério de aceitação'], 'Ordem do próximo lote': 'Critério individual; consultar backlog atual e matriz única'}
appended.append([new[h] for h in bh])
assert len(data) - 1 + len(appended) == 3393
add('backlog-priorizado.csv', data, digest, patches, [h for h in bh if h != 'ID'], appended)
manifest = json.loads((ROOT / 'storage/app/private/document-import-staging/legacy-setores-20261010/manifest.json').read_text(encoding='utf-8'))
resources = {r['external_id']: r for r in imported['resources']}
by_url = {r['source_url'].rstrip('/'): resources[r['external_id']] for r in manifest['documents']}
for filename in ['legado-inventario.csv', 'legado-conteudos-reconciliados.csv']:
data, digest = table(filename); h = data[0]; patches = []
for i, values in enumerate(data[1:], 1):
item = dict(zip(h, values)); row = None
if filename == 'legado-inventario.csv': row = by_url.get(item['URL'].rstrip('/'))
else:
row = next((r for r in resources.values() if r['external_id'].startswith('legacy-tempo-post-' + item['ID legado'] + '-')), None)
if row:
empty = row['external_id'].startswith('legacy-tempo-post-171994-')
status = (f'RecursoQA{row["id"]}; original/hash/ClamD/replay demonstrados. ' +
('Corpo vazio na origemHTML/Inertia200;privado,sem página publicada.' if empty else 'DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.'))
changes = {'Situação': status}
if filename == 'legado-inventario.csv': changes['Destino proposto'] = f'biblioteca existente recursoQA{row["id"]};URLs/versão originais preservados'
else: changes['Recurso QA'] = str(row['id'])
patches.append({'row': i, 'values': changes})
add(filename, data, digest, patches, ['Situação', 'Destino proposto', 'Recurso QA'])
block = f'''<!-- qa-c71-sectors-result-20261010:begin -->
## Resultado atual — {stamp}
**QA18097 recuperada; nenhuma alteração na base ativa.** C71 técnico corrigido: faltavam26 ficheiros de catálogos partilhados, incluindo `global/2964`, necessários ao `pg_dump`. O agente/processo que os removeu continua desconhecido. O cluster danificado foi preservado e substituído por cluster novo em diretório estável, apenas no alvoQA55441. As295tabelas/34.902linhas e31sequências iniciais conferiram; schema/owners/ACL/triggers equivalentes após normalização explícita de2constraints varchar/text e slots de colunas removidas. A cópia física retida confere em{copy['files']}ficheiros por nomes, tamanhos ehashes; isso não afirma restauro lógico de todos os bancos históricosQA.
**Backup/restauro posterior ao lote:** {backup['verified_at']},295tabelas e103ficheiros privados conferidos em `{backup['restored_target']['database']}:55441`. APP_KEY, MFA e contas preservados. Não é prova de backup da base ativa nem fixa candidata global. QA/login voltou aHTTP200; manutenção arquivada. 2FAS Auth continua compatível com TOTP; a rejeição manualQA18 registada foi de palavra-passe antes do MFA, sem justificar troca de aplicação.
| Requisito / caso | Evidência deste lote | Lacuna concreta |
| --- | --- | --- |
| AUD-OPS-01 / C71 | Cluster saudável, dados/sequências/permissões preservados, backup/restauro renovado,0escritasativas. Estado global passa de com falha para parcial. | C37/QA18,A/B e outros consumidores; candidata e backup ativo/autorização própria. |
| AUD-LEG-01/05/07 |22ficheiros (17artigos+5imagens) importados privadamente, hashes recuperados,22scans ClamD reais; replay sem duplicações/eventos/scans novos. |32das50versões observadas continuam não importadas; sitemap misto/paginação/links externos/C68 e cobertura integral. |
| AUD-LEG-02/03/06 / C72 |16artigos novos comtexto públicos, título deInfraestruturas preservado. Chromium desktop: imagens/pesquisa/artigos→MFA21→4dossiers→retorno→logout→nova sessão→F5/replay. HTTP e persistência separados,8dossiers no total. | Móvel/acessibilidade/mais perfis e percursosA/B completos não encerrados. |
| AUD-LEG-04 |Leitura pública; guardar requer login. Anónimo/QA23 sem contexto privado; QA23recebe403 ao dossier alheio. Estado de consulta/proteçãoQA mantido concluído no âmbito demonstrado. | Disponibilização ativa e demais aceitações são independentes. |
| Mercado Financeiro |OrigemHTML e respostaInertiaHTTP200 contêm corpo vazio; exemplar22 no lote fica privado na biblioteca (recurso23). `/mercado-financeiro` e respetiva exploração404,sem página vazia apresentada como entrega. | Obter conteúdo real/versão comtexto; não usar Fintech nem inventar corpo. |
| C74 |Imagens semAssunto eram recusadas porPUT422. QA aceita tema vazio sem inferir setor; interface tornaAssunto opcional. Cinco metadados vazios guardados/recuperados; teste dedicado1/11. | Editor administrativo sem ensaio visual próprio; alteração protegida pela guardaQA. |
**Contagens canónicas:**3393requisitos/3393linhas,6845ocorrências de origem; {states['parcial']}parciais,145não implementados,5dependentes de decisão,1implementado mas não ativado,7concluídos e demonstrados,0com falha. Casos adicionais e aceitações abertas continuam visíveis dentro dos requisitos parciais;0com falha global não significa portal sem erros. BacklogCSV é projeção reconciliada dos mesmos3393IDs, incluindo39 anteriormente omitidos, não outra matriz.
**RotasQA:** `/explorar/energia-mineracao` → `/energia-mineracao`, `/energia`, `/mineracao`; `/explorar/turismo` → `/turismo` e11artigos territoriais; `/explorar/infraestruturas` → `/infraestruturas`. Agronegócio preservado. Energia/Mineração são fontes distintas; Turismo mantém texto editorial e0associações territoriais automáticas. Todos os dados de fonte/autoria/datas/hash/URLs/versões originais são conservados. Os artigos não criaram oportunidades (49antes/depois), geometrias ou perfis. Divulgação foi uma ação autenticada do gestorQA14 pelo mecanismo existente, separada da importaçãoCLIQA (atribuição `--actor`, não autenticação ativa); nenhum desafioMFA foi observado nessa conta. QA21/QA23 usaramMFA normalmente.
**Próximo trabalho executável:** restantes32versões/acervo eC68 por ficheiro; Response/§27/validações, PDFs/derivadas epaíses/origem/estatísticas por consumidor;173correspondências territoriais ainda abertas comMandie/Guroambíguo. Não acrescentar jornadas educativas para contornar essas pendências. Perfis modulares eSLA conservam decisões próprias. C37/QA18,A/B,móvel/acessibilidade permanecem abertos; este lote não demonstraCRUD/workflow de oportunidades nem os encerra.
**Ativação futura:** revisão/hashes/flags e aceitação global→leitura/backup/restauro do ativo correspondente→código/build compatíveis com flags desligadas→030000→040000→09/190000 somente se ausentes→importação privada de conteúdo/cartografia com prévia e scan→divulgação separada→disponibilização incremental. QA já tem as3instalações;não as repetir. Previsão desta extensão:22originais (reutilizar hashes existentes),21potencialmente divulgáveis e1exemplar vazio privado;0contas/dossiers/opportunities/geometrias transferidos. O CLIrecusa aplicação ativa: falta preparar/demonstrar uma ação autenticada emlote que preserve exatamente as identidades/versionamento existentes; upload unitário não foi provado equivalente. Cartografia:408candidatas/173pendentes no retratoQA anterior, não importadas por esta frente. `ready=true` continua preparação, não instalação/disponibilização.
**Reversão:** retirar divulgação em ações auditadas e ocultar consumidores/flag, preservando originais, versões, consentimentos/referências eeventos. Restauro técnico usa o novo dump íntegro e chave estável numa QA distinta,comparação antes de corte;não voltar ao cluster danificado. Nunca drop de tabelas utilizadas nem copiar fixturesQA para o ativo. Provas: `legacy-sectors-{{apply,replay,http,browser,persistence}}-qa.json`, `legacy-financial-origin-20261010/proof.json`, `portal-qa-recovery/20261010_154445/*-proof.json` e `profile-installation-backup-qa.json` (privadas,sem credenciais/cookies/códigos nas capturas).
**Versões históricas preservadas abaixo:** frases sobreC71aberto,22ficheiros apenas preparados ou rotas apenas deagronegócio descrevem os incrementos anteriores e são substituídas, para o estado atual, pelas provas desta secção.
<!-- qa-c71-sectors-result-20261010:end -->
'''
def legible(value):
value = re.sub(r'\b(as|As|de|e|os|em|por)(?=\d)', r'\1 ', value)
value = re.sub(r'(\d+)(tabelas|linhas|ficheiros|sequências|constraints|instalações|artigos|imagens|itens|originais|versões|referências|eventos|scans|dossiers|requisitos|ocorrências|parciais|correspondências|nomes|exemplar|contas|pendentes|candidatas|não|dependentes|implementado|concluídos|com|das|novos|potencialmente)', r'\1 \2', value)
for old, new in {'comtexto': 'com texto', 'semt​exto': 'sem texto', 'sem página': 'sem página', 'original/hash/ClamD': 'original / hash / ClamD',
'semcontorno': 'sem contorno', 'semcontas': 'sem contas', 'apósguarda': 'após guarda', 'porficheiro': 'por ficheiro',
'preservedos': 'preservados', 'privadas,sem': 'privadas, sem', 'ativos,sem': 'ativos, sem',
'21divulgados': '21 divulgados', '1artigo': '1 artigo', 'comQA21': 'com QA21',
'QA23negado403': 'QA23 negado 403', 'das50': 'das 50', 'eC68imagem404': 'e C68 (imagem 404)',
'19etapas': '19 etapas', '3393IDs': '3393 IDs', '39estavam': '39 estavam',
':22': ': 22', ':3393': ': 3393', ',6845': ', 6845', ';3235': '; 3235', ',145': ', 145',
',5 dependentes': ', 5 dependentes', ',1 implementado': ', 1 implementado', ',7 concluídos': ', 7 concluídos',
',0 com falha': ', 0 com falha', 'CLI--actor': 'CLI `--actor`',
'sequência030→040→09/190': 'sequência 030000 → 040000 → 09/190000'}.items():
value = value.replace(old, new)
return value
block = legible(block)
backlog_block = f'''<!-- qa-c71-sectors-result-20261010:begin -->
## Prioridades atuais após C71 — {stamp}
**QA recuperada e lote setorial demonstrado; base ativa fora do âmbito.** C71 técnico corrigido, com backup/restauro final de295tabelas/103ficheiros. AUD-OPS-01 volta a parcial, porque aceitação global e ativação continuam pendentes. Biblioteca:22novos originais importados/scaneados;21divulgados por ação separada e1artigo vazio privado. Chromium desktop comQA21:4dossiers criados, retorno, logout/novaMFA/F5/replay; QA23negado403. Ver relatório executivo e provas individuais.
| Prioridade / requisito | Falta e próxima ação | Critério concreto |
| --- | --- | --- |
| P0 / AUD-UI-01,C37/QA18 | Incorporar tentativa manual sanitizada; primeira rejeição atual foi de palavra-passe, antes do MFA. 2FAS continua compatível; sem nova aplicação ou reset de segredo. | MFA→`/investimentos`→F5→logout→novo login na sessão problemática; não usar QA21 como substituto. |
| P1 / AUD-LEG-01/06/07 |32das50versões ainda não importadas, sitemap/paginação/acervo eC68imagem404. Reconciliar próximo lote por identidade/hash/citação. | Prévia, armazenamento privado, scan real, replay/recuperação e divulgação separada; não inventar corpo de Mercado Financeiro. |
| P1 / AUD-LEG-02/03/07 | Móvel/acessibilidade/mais perfis e editor administrativo C74 sem ensaio visual próprio; restantes ligações/consumidores. | Prova de cada ação visível e permissão; preservar confirmação manual limitada do piloto e diferenças entre versões. |
| P1 / percurso,I0–I3,A/B | Cenários completos com ferramenta→resultado→Balcão→acompanhamento, e ações do dashboard/workflow continuam independentes. | Cinco fases/19etapas, nova sessão, preparação paralela, isolamento/permissões, retorno ao dossier. |
| P1 / países e cartografia | Completar países/origem/filtros/estatísticas por formulário e173correspondências territoriais; Mandie/Guro permanece ambíguo. | UI→API→gravação→recuperação→resultado por consumidor; território≠geometria≠mapa. |
| P1 / documentos,Response/§27 | Completar condições semânticas, PDFs/exportações/derivadas por consumidor. | Scan próprio, originais/hash preservados, permissões e resultados recuperáveis. |
| P1/P2 / decisões e ativação | Perfis modulares/SLA, calendário/nomeações reais e candidata permanecem separados. Preparar ação autenticada de importação em lote preservando identidade:CLI--actor sóQA. | Decisão concreta por regra; backup/restauro da base ativa da candidata e sequência030→040→09/190 sem repetições. |
**Mesma matriz:**3393requisitos/linhas,6845ocorrências;3235parciais,145não implementados,5dependentes de decisão,1implementado mas não ativado,7concluídos e demonstrados,0com falha global. O backlogCSV agora projeta todos os3393IDs;39estavam omitidos. Casos/aceitações dentro dos parciais continuam abertos. As secções abaixo são histórico anterior à recuperação/importação, não o estado atual.
<!-- qa-c71-sectors-result-20261010:end -->
'''
activation_block = f'''<!-- qa-c71-sectors-result-20261010:begin -->
## Preparação atual após recuperação e lote setorial — {stamp}
C71 corrigido emQA: comparação integral e backup/restauro renovado {backup['verified_at']} ({backup_tables}tabelas/{backup_files}ficheiros), em `{backup['restored_target']['database']}:55441`. Esta prova não é backup do ativo nem fixa candidata. C37/QA18,A/B,móvel/acessibilidade/outros consumidores e decisões próprias permanecem pendentes.
1. Concluir as aceitações da revisão; fixar código/manifesto/configuração e hashes. A guarda atual do legado continua restrita aQA; código preparado não é habilitação ativa.
2. Conferir o alvo ativo por leitura e provar backup/restauro próprio dessa revisão, incluindo ficheiros e chave estável. O último retrato ativo anterior continua histórico.
3. Disponibilizar código/build compatíveis com flags desligadas; preservarURLs,IDs,roteiros/consentimentos. Não copiar contas ou dossiers sintéticos.
4. Prévia e aplicação autenticadas030000→040000→**2026_10_09_190000_prepare_investor_path**, apenas se ausentes e coerentes. QA já tem as3: não repetir. O Balcão07/190 não substitui a extensão09/190; dependências/efeitos e bootstrap separado estão na tabela histórica abaixo.
5. Conteúdo: prévia/hash/identidade/versão, armazenamento privado eClamD. **22candidatos do lote (17artigos+5imagens)**;17artigos comtexto no total incluindo piloto anterior,1exemplar vazio privado. Reutilizarbytes/identidades já existentes; qualquer conflito fica pendente. A CLI com--actor é apenas ensaioQA, não instalação autenticada ativa. Ainda falta preparar/demonstrar um ponto autenticado de importação emlote com as mesmas identidades; upload unitário não foi demonstrado equivalente.
6. Cartografia emlote distinto, por pais e correspondências inequívocas; retratoQA408candidatas/173pendentes, Mandie/Guroambíguo.0geometrias importadas nesta frente; medir novamente no alvo. Publicação das camadas separada.
7. Divulgar os conteúdos autorizados em ação própria: previsão21recursos deste lote; Mercado Financeiro permanece privado/vazio e nunca apresentado como página concluída. QA demonstrou divulgação autenticada do gestor14, sem presumir MFA não observado. Aprovação/consulta/cálculo/decisão externa são estados independentes.
8. Disponibilizar consumidores/percurso incrementalmente após provas;0fixturesQA transferidos. `ready=true` não instala, não publica e não fecha requisitos.
**Reversão:** retirar divulgação e ocultar consumidores/flags com auditoria, mantendo originais, versões, fontes dos dossiers eeventos. Preferir reconciliação/restauro numaQA distinta comdump íntegro/chave preservada antes de corte; não reativar o cluster danificado nem eliminar tabelas utilizadas. Conservados o original danificado e a cópia física de{copy['files']}ficheiros (nomes/tamanhos/hashes iguais), sem afirmar restauro lógico de todos os históricos no novo runtime. Plano não executado na base ativa.
As secções abaixo preservam a evolução anterior; referências aC71aberto ou lote apenas preparado foram superadas pelas provas atuais, sem apagar dependências independentes.
<!-- qa-c71-sectors-result-20261010:end -->
'''
reports = {'relatorio-executivo.md': block, 'legado-piloto-qa.md': block, 'backlog.md': legible(backlog_block), 'plano-ativacao.md': legible(activation_block)}
for filename, current_block in reports.items():
path = DOC / filename
original = path.read_text(encoding='utf-8')
archive = OUT / (filename + '.before')
if not archive.exists(): archive.write_text(original, encoding='utf-8')
original = re.sub(r'<!-- qa-c71-sectors-result-20261010:begin -->.*?<!-- qa-c71-sectors-result-20261010:end -->\s*', '', original, flags=re.S)
title, body = original.split('\n', 1)
path.write_text(title + '\n\n' + current_block + '\n' + body.lstrip(), encoding='utf-8')
case_path = DOC / 'casos-reproducao.md'
case = case_path.read_text(encoding='utf-8')
case_block = f'''<!-- qa-c71-sector-cases-20261010:begin -->
## Estado dos casos C71–C76 após o lote — {stamp}
**C71 / AUD-OPS-01:** causa/operação/reprodução históricas preservadas abaixo. CorrigidoQA: recuperação emcluster novo, comparação295tabelas/34.902linhas/31sequências e schema/permissões compatíveis; cópia física{copy['files']}ficheiros com hashes iguais. Prévia22itens renovada antes de aplicação; backup/restauro final295/103verificado. Processo que removeu26catálogos continua não identificado; nenhum remendo de catálogo ou rotação de chave. Requisito inteiro parcial pelas demais dependências/ativação. A manutenção foi retirada; GET/login200no18097. Ver provas no relatório atual.
**C72 / AUD-LEG-02/07:** título deInfraestruturas desaparecia pelo sanitizador excluirh1/filhos; h1→h2 preserva conteúdo e limpeza. Agora exemplar importado/scaneado/consultável e título observado no Chromium, captura `infra-original-heading`; original/hash intacto. CorrigidoQA, não ativo.
**C73 / AUD-UI-01/C37:** a manutenção técnica é independente das3rejeições de palavra-passe15:38–15:39UTC; estas ocorreram antes do MFA. QA18secret/chave/credencial reconfirmados15:57:57porREADONLY. 2FAS Auth já suportaTOTP; não requer nova aplicação/remoção da entrada antiga. C37continua pendente no navegador do responsável, sem novo ensaio MFA18 deste lote.
**C74 / AUD-LEG-07, consumidor documental:** reproduzir comimagem do manifesto semtema: login gestorQA14→PUT `/management/portal-services/resources/24` comAssuntovazio→HTTP422campo`topic`. Esperado: guardar metadados originais autorizados seminventarassociação. Causa: importador admite vazio, validaçãoAPI/frontend exigiamvalor. CorreçãoQA: tema nullable sobguarda realQA, vazio normalizado para stringcompatível; interfaceAssuntoopcional. RepetiçãoPUTs das5imagens+recuperaçãoSQL/download passaram; originais/hashes/scans/ausência de relações preservados. Teste dedicado1/11asserções; visualdoeditor ainda pendente. DivulgaçãoPOSTseparada, semcontorno da autorização.
**C75 / AUD-LEG-02/03, regressão detectada antes da divulgação:** GET `/explorar/agronegocio` no testeLegacyContentTest retornava500 na nova parametrização, `Undefined property stdClass::$title`. Causa: projeçãoSQL selecionava documento mas omitia título do recurso usado nas leituras relacionadas. Corrigido incluindo`r.title`; teste4/81eHTTP/Chromium passaram. Não atribuir ao middleware. Nenhuma ocorrência ativa alegada.
**C76 / AUD-LEG-03/06, regressão detectada antes da divulgação:** POST `/investimentos` comartigo válido retornava422. Esperado: validar ID da biblioteca e guardar fonte numdossier próprio. Causa: `value(d.external_id)`sobrebuilder comprojeção pré-existente lia primeiro campoUUID; não encontrava a identidade configurada. Correção: leitura explícita`first()?->external_id`, preservando controles público/hash/scan/contexto. Testes4/81; HTTP4dossiers/Chromium4dossiers eREADONLY8referências únicas. CorrigidoQA, nenhuma escrita ativa.
Falhas do harness conservadas separadamente: primeira leitura usou indevidamente rota pública de download para exemplar privado404 (a rota protegida correta é `/management/documents/{{UUID}}/download`); recompilação durante sessãoInertia produziu409/reload esperado; primeiras esperasnetworkidle/imagem ainda não decodificada falharam sob concorrência. Corrigida ordenação/espera no teste, sem alterar autenticação/aplicação para fabricar sucesso. Histórico de tentativasprivadas preservado; resultado final sófoi declarado após percursos completos.
<!-- qa-c71-sector-cases-20261010:end -->
'''
case = re.sub(r'<!-- qa-c71-sector-cases-20261010:begin -->.*?<!-- qa-c71-sector-cases-20261010:end -->\s*', '', case, flags=re.S)
title, body = case.split('\n', 1)
case_path.write_text(title + '\n\n' + legible(case_block) + '\n' + body.lstrip(), encoding='utf-8')
data, digest = table('inventario-fontes.csv'); h = data[0]; patches = []; source_updates = []
for i, values in enumerate(data[1:], 1):
item = dict(zip(h, values)); exemplar = Path(item['Exemplar/caminho'])
if exemplar.parent == DOC and exemplar.name in ['legado-piloto-qa.md', 'legado-inventario.csv', 'legado-conteudos-reconciliados.csv']:
patches.append({'row': i, 'values': {'Nota de leitura': 'C71 recuperado e 22 originais QA importados/scaneados; 21 divulgados separadamente; 16 novos artigos e 4 dossiers demonstrados. Históricos/restrições preservados. | Nota anterior: ' + item['Nota de leitura'],
'Edição/versão identificada': item['Edição/versão identificada'] + ' | loteQAsetores' + stamp + '; exemplar anteriorSHA256=' + item['SHA256']}})
source_updates.append({'row': i})
add('inventario-fontes.csv', data, digest, patches, ['Nota de leitura', 'Edição/versão identificada', 'SHA256', 'Bytes'], source_hash_updates=source_updates)
(OUT / 'csv-edits.json').write_text(json.dumps(packet, ensure_ascii=False), encoding='utf-8')
(OUT / 'counts.json').write_text(json.dumps({'requirements': len(canonical), 'rows': len(rows) - 1, 'source_occurrences': 6845,
'states': dict(states), 'canonical_backlog_missing_rows_added': len(appended), 'active_writes': 0}, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({'prepared_csv_tables': 5, 'matrix_rows': len(updates), 'backlog_missing_rows': len(appended), 'states': dict(states), 'database_access': False}, ensure_ascii=False))
+72
View File
@@ -0,0 +1,72 @@
// Patch the existing CSV cells using Artifact Tool; never create a second requirements matrix.
import fs from 'node:fs/promises';
import path from 'node:path';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { pathToFileURL, fileURLToPath } from 'node:url';
import crypto from 'node:crypto';
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const DOC = path.join(ROOT, 'docs/investmoz/v2/auditoria-portal');
const PRIVATE = path.join(ROOT, 'storage/app/private/investmoz-v2/portal-audit');
const runtime = path.join(PRIVATE, 'legacy-sector-reconciliation-20261010');
const requireRuntime = createRequire(path.join(runtime, 'package.json'));
const { Workbook } = await import(pathToFileURL(requireRuntime.resolve('@oai/artifact-tool')).href);
const packetName = process.argv[2] || 'legacy-sectors-finalize-20261010';
assert(/^[a-z0-9-]+$/.test(packetName), 'Editing packet must remain inside the private audit directory');
const packetDirectory = path.join(PRIVATE, packetName);
const packet = JSON.parse(await fs.readFile(path.join(packetDirectory, 'csv-edits.json'), 'utf8'));
const sha = b => crypto.createHash('sha256').update(b).digest('hex');
const serialize = rows => '\ufeff' + rows.map(row => row.map(v => /[",\r\n]/.test(v) ? '"' + v.replaceAll('"', '""') + '"' : v).join(',')).join('\r\n') + '\r\n';
const matches = (value, literal) => value instanceof Date ? value.getTime() === Date.parse(literal) : (value == null ? '' : String(value)) === literal;
const outputs = [];
for (const edit of packet.files) {
const filename = edit.filename, file = path.join(DOC, filename), input = await fs.readFile(file, 'utf8');
assert.equal(sha(Buffer.from(input)), edit.input_sha256, 'Source changed since targeted editing was prepared');
const source = edit.rows, strings = source.map(row => [...row]);
const workbook = await Workbook.fromCSV(input.replace(/^\ufeff/, ''), { sheetName: 'Registos' });
const sheet = workbook.worksheets.getItem('Registos'), headers = source[0];
const observed = sheet.getRangeByIndexes(0, 0, source.length, headers.length).values;
assert(observed.every((r, i) => r.every((v, c) => matches(v, source[i][c]))), 'Imported cells must match existing source');
const changes = [];
for (const patch of edit.patches) {
const row = patch.row;
assert(row > 0 && row < source.length);
const updated = [...strings[row]];
for (const [field, value] of Object.entries(patch.values)) {
const col = headers.indexOf(field); assert(col >= 0 && edit.allowed_columns.includes(field));
updated[col] = value;
}
sheet.getRangeByIndexes(row, 0, 1, headers.length).values = [updated]; strings[row] = updated;
changes.push({ row: row + 1, fields: Object.keys(patch.values) });
}
if (edit.append?.length) {
assert.equal(filename, 'backlog-priorizado.csv', 'Only the existing backlog projection can gain missing canonical IDs');
sheet.getRangeByIndexes(strings.length, 0, edit.append.length, headers.length).values = edit.append;
strings.push(...edit.append);
}
// Source registry hashes refer to the final bytes authored above; original hashes remain in version history.
if (filename === 'inventario-fontes.csv') {
for (const patch of edit.source_hash_updates) {
const row = patch.row, exemplar = strings[row][headers.indexOf('Exemplar/caminho')];
const final = outputs.find(out => path.normalize(path.join(DOC, out.filename)) === path.normalize(exemplar));
const bytes = final ? Buffer.from(final.text) : await fs.readFile(exemplar);
strings[row][headers.indexOf('SHA256')] = sha(bytes);
strings[row][headers.indexOf('Bytes')] = String(bytes.length);
sheet.getRangeByIndexes(row, 0, 1, headers.length).values = [strings[row]];
}
}
workbook.recalculate();
const result = sheet.getRangeByIndexes(0, 0, strings.length, headers.length).values;
assert(result.every((r, i) => r.every((v, c) => matches(v, strings[i][c]))), 'Authored fields differ from inspected workbook cells');
const inspected = await workbook.inspect({ kind: 'table', range: 'Registos!A1:C3', include: 'values', tableMaxRows: 3, tableMaxCols: 3 });
assert(inspected.ndjson);
// Preserve all original literal identifiers/offset dates. CSV has no workbook formatting or rendering surface.
const text = serialize(strings), temporary = path.join(packetDirectory, filename + '.verified');
await fs.writeFile(temporary, text, 'utf8');
outputs.push({ filename, text, temporary, changes, appended: edit.append?.length || 0 });
}
// Commit only after every table and dependency has passed preservation checks.
for (const out of outputs) await fs.copyFile(out.temporary, path.join(DOC, out.filename));
await fs.writeFile(path.join(packetDirectory, packet.proof_filename || 'csv-authoring-proof.json'), JSON.stringify({
database_access: false, changed: outputs.map(({ filename, text, changes, appended }) => ({ filename, sha256: sha(Buffer.from(text)), changes, appended })) }, null, 2));
console.log(JSON.stringify({ tables: outputs.length, changed: outputs.map(({ filename, changes, appended }) => ({ filename, rows: changes.length, appended })), database_writes: 0 }));
@@ -0,0 +1,128 @@
"""Scoped C86/C87 evidence into the same canonical audit; no CSV authoring here."""
import csv
import hashlib
import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
csv.field_size_limit(16000000)
ROOT = Path(__file__).resolve().parent.parent
DOC = ROOT / 'docs/investmoz/v2/auditoria-portal'
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
OUT = PRIVATE / 'territory-resource-finalize-20261010'
OUT.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc)
def read(file):
data = file.read_bytes()
return json.loads(data.decode('utf-16' if data[:2] == b'\xff\xfe' else 'utf-8-sig'))
browser = read(PRIVATE / 'territory-resource-chromium-20261010.json')
sql = read(PRIVATE / 'territory-resource-persistence-qa.json')
shot = read(PRIVATE / 'territory-resource-map-shot-20261010.json')
before = read(PRIVATE / 'location-country-before-fix-qa.json')
unit = read(PRIVATE / 'location-country-after-fix-qa.json')
assessment = read(PRIVATE / 'location-country-assessment-qa.json')
status = read(PRIVATE / 'territory-resource-final-status-qa.json')
backup = read(PRIVATE / 'profile-installation-backup-qa.json')
closed = read(PRIVATE / 'c37-manual-close-20261010/acceptance.json')
assert browser['result'] == sql['result'] == unit['result'] == assessment['result'] == 'passed'
assert all(browser['checks'].values()) and sql['read_only'] and len(sql['resources']) == 2
assert before['result'] == 'failed' and 'received 200' in before['failures'][0]['message']
assert shot['result'] == 'passed_read_only_map_screenshot' and shot['administrative_edition_caption_correct'] and shot['assessment_foreign_hierarchy_not_required']
assert closed['state'] == 'closed_scoped_qa18_authentication'
assert status['active_writes'] == 0 and status['active_manifest_sha256'] == '75352185c2a85c34ab726613db864a598e301a5b342ebbc80ac8e678eed17a94'
assert status['code_hash'] != backup['code_hash'], 'Old backup must not be silently presented as candidate-matched'
tag = '[C86/C87 territorial QA 10/10/2026] '
evidence = ('C86 reproduzido: API aceitava país PT com hierarquia MZ por HTTP200; agora422 em country_code sem alterar payload/versão/auditoria do draft. '
'UI retira os IDs moçambicanos do preenchimento ao mudar país; outro país admite rascunho incompleto, sem exigir província/distrito MZ. '
'QA21 oportunidade51: country MZ, Cabo Delgado→ANCUABE, guardar recurso→nova sessão→recuperar campos/mapa→F5; '
'dois limites administrativos visíveis. Outro recurso PT recuperado sem IDs MZ. Trocar pai limpa descendentes; QA23 organização alheia recebe403 em consulta e escrita. '
'PostgreSQL QA em transação READ ONLY confirma2recursos/versões1 e a relação pai; zero ligações territoriais aceites criadas e nenhuma publicação. '
'C87: legenda fixa de limites oficiais por verificar contrariava o contexto administrativo; agora distingue edição administrativa de parcela, sem alterar fonte/dados/aceitação oficial. '
'Provas: territory-resource-chromium-20261010.json, territory-resource-persistence-qa.json, territory-resource-map-shot-20261010.json, location-country-{before,after}-fix-qa.json e location-country-assessment-qa.json. '
'C37 autenticação18 fechado por confirmação manual anterior; não repetido.')
gap = ('Cobertura demonstrada somente para localizações de preparação da oportunidade51, QA21 e recusaQA23; '
'concluir projetos/clusters/corredores e restantes campos/mapas, níveis inferiores e perfis. Tiles externos bloqueados e não demonstrados. '
'País da localização não é origem de solicitação: formulários restantes, filtros e estatísticas positivas continuam pendentes. '
'Documentos/derivadas, workflow/restauração, contratos especializados e decisões modulares/SLA preservados. '
'Não importar geometrias para inferir parcelas/direitos nem relações aceites;173 correspondências anteriores e Mandie/Guro continuam pendentes. '
'A/B, móvel/acessibilidade integral, candidata e ativação não encerrados.')
ids = {'AUD-OP-02', 'AUD-GEO-01', 'AUD-GEO-02', 'AUD-GEO-03'}
files = []
for filename in ['matriz-requisitos.csv', 'backlog-priorizado.csv']:
source = DOC / filename
rows = list(csv.reader(source.open(encoding='utf-8-sig', newline='')))
headers = rows[0]
patches = []
for index, row in enumerate(rows[1:], 1):
old = dict(zip(headers, row))
if old['ID'] not in ids:
continue
if filename.startswith('matriz'):
values = {'Evidência funcional': evidence, 'Interface': 'QA21 recursos51: seleção, gravação, edição/recuperação noutra sessão em390px, mapa de Ancuabe/CaboDelgado e Portugal incompleto sem hierarquia MZ. C87 legenda por propósito administrativo.',
'API': 'POST guardar201; alteração PT+hierarquia MZ422 country_code; payload/versão guardados preservados; QA23 GET/POST403; geometria selecionada disponível e2áreas de contexto.',
'Gravação': 'Dois recursos privados na tabela existente opportunity_location_drafts, ambosv1; nenhuma nova tabela/importação/conta/ligação aceite/publicação.',
'Recuperação': 'Campos MZ/província/distrito e PT sem IDs recuperados noutra sessão; consulta SQL READ ONLY confirma payloads e parent_id.',
'Resultado apresentado': 'Dois polígonos administrativos visíveis; edição2019-06-07 e fonte INE mantidas. Lista não reabre autenticidade oficial nem apresenta limite como parcela.',
'Consolidação': 'Subcritérios C86/C87 atualizados; fontes, versões, IDs e estados globais preservados.'}
values = {field: old[field] + ' | ' + tag + value for field, value in values.items()}
values['Lacuna'] = tag + gap + ' | Histórico preservado: ' + old['Lacuna']
variants = json.loads(old['Critérios e versões por origem (JSON)'])
for variant in variants:
variant['incremento_territorial_c86_c87_20261010'] = {'registado_em': stamp.isoformat(), 'evidencia': evidence, 'lacuna_atual': gap, 'ativacao': False}
values['Critérios e versões por origem (JSON)'] = json.dumps(variants, ensure_ascii=False, separators=(',', ':'))
else:
values = {'Pendência': tag + gap + ' | Histórico: ' + old['Pendência'],
'Correção/componentes': old['Correção/componentes'] + ' | ' + tag + 'OpportunityResources country–hierarchy validation; editor and assessment applicability; InteractiveInvestmentMap caption by existing purpose. Existing entities/services reused.',
'Aceitação': old['Aceitação'] + ' | ' + tag + 'QA21 localizações MZ/PT guardadas e recuperadas noutra sessão; mapa2limites; QA23 recusa403; SQLREADONLY2recursos. Demais consumidores continuam pendentes.'}
patches.append({'row': index, 'values': values})
assert len(rows) == 3394 and len(patches) == len(ids)
files.append({'filename': filename, 'input_sha256': hashlib.sha256(source.read_bytes()).hexdigest(), 'rows': rows,
'patches': patches, 'allowed_columns': sorted({field for patch in patches for field in patch['values']}), 'append': []})
(OUT / 'csv-edits.json').write_text(json.dumps({'files': files}, ensure_ascii=False), encoding='utf-8')
section = f'''## Lote territorial C86/C87 — {stamp.astimezone(timezone(timedelta(hours=2))).isoformat()}
**C37 encerrado somente para autenticação QA18, por confirmação manual expressa. Não foi repetido.** Aceitação ligada aos recibos de logout/recuperação em `c37-manual-close-20261010/acceptance.json`. A/B, auditoria integral e ativação continuam independentes.
| Requisito/caso | Correção e prova QA | Limite do fecho |
| --- | --- | --- |
| OP02; GEO01/02; C86 | API antes aceitava PT com província MZ por200; agora422 junto a country_code, preservando versão/payload/auditoria. UI limpa somente a seleção administrativa no preenchimento ao mudar país; estrangeiro incompleto pode guardar e recuperar. Conferência deixou de exigir província/distrito MZ a Portugal. | Aplicado a recursos de localização; não encerra origem, filtros ou estatísticas das solicitações. Registos anteriores não foram migrados/corrigidos silenciosamente. |
| GEO01/02/03 | QA21 oportunidade51: MZ→CaboDelgado→ANCUABE→guardar→nova sessão→campos e mapa recuperados→F5. Distrito `5b03104f-9c47-5542-8de3-e81b8d5fdd55`; dois limites disponíveis e dois polígonos renderizados. Segundo recurso PT recuperado sem IDs MZ. Troca de pai limpa descendentes. | Demais consumidores/projetos/clusters/corredores e níveis inferiores não encerrados. Tiles externos bloqueados no Chromium; mapa base não demonstrado. Não houve nova importação cartográfica ou inferência de parcelas/direitos. |
| OP02; autorização | QA23 organização alheia: consulta403 e escrita403. PostgreSQL QA READ ONLY confirmou os2recursos v1 e o parentesco geográfico; ligações territoriais aceites permaneceram {sql['accepted_territorial_links']}. | Outros perfis, documentos/derivadas e lifecycle/workflow conservam provas/pendências próprias. |
| GEO03; C87 | Lista repetia “limites oficiais por verificar” mesmo para administrativos do INE. Texto agora distingue edição administrativa de parcela; fonte e edição2019-06-07 preservadas. Popup já distinguia o propósito, lista foi alinhada. Chromium390px e imagem inspecionada. | A aceitação oficial não foi reaberta. A edição não afirma atualidade jurídica ou disponibilidade de investimento. Outras camadas e geometria de parcela têm critérios próprios. |
Provas sanitizadas: `territory-resource-chromium-20261010.json`, `territory-resource-persistence-qa.json`, `territory-resource-map-shot-20261010.json` e regressões `location-country-*qa.json`. Suite de recursos5/71 passou antes do ajuste de conferência; regressão específica final1/20 passou depois. TypeScript passou após o seletor; compilação QA finalexit0. Contagens de testes não substituem os percursos acima. Falhas do harness (versão Inertia omitida, seletor por atributo incorreto e caixa dos nomes) foram preservadas como tentativas; não são falhas do portal. Nome oficial ANCUABE não foi renomeado.
Mesma matriz:3393requisitos/3393linhas/6845ocorrências; quatro requisitos recebem subcritérios, sem alteração de estados globais. Países/origem/estatísticas restantes,173 correspondências anteriores com Mandie/Guro ambíguo, documentos, Response/§27, workflow/restauração e decisões modulares/SLA permanecem abertos. Não iniciar novas jornadas educativas.
**Código preparado e demonstração QA, sem disponibilidade ativa.** Zero migrações/instalações novas e zero escritas ativas. Manifesto ativo SHA256 `{status['active_manifest_sha256']}` intacto. O backup/restauro QA18:19 liga-se ao fingerprint anterior; não foi apresentado como prova da revisão nova. Para futura candidata: renovar verificações/backup/restauro associados à revisão, testar compatibilidade/reversão no clone do ativo e preservar **030000→040000→09/190000**, sem repetir instalações QA. O backup ativo READ ONLY17:48 permanece histórico; nenhum corte autorizado.
'''
for filename in ['relatorio-executivo.md', 'backlog.md']:
file = DOC / filename
file.write_text('<!-- qa-territory-resource-c86-c87-20261010:begin -->\n' + section + '\n<!-- qa-territory-resource-c86-c87-20261010:end -->\n\n' + file.read_text(encoding='utf-8-sig'), encoding='utf-8')
cases = '''## Casos C86/C87 — recursos e cartografia
**C86 (P1, OP02/GEO02):** editar localização MZ com província e mudar payload para PT mantendo IDs MZ. Esperado422 no país e preservação do original; observadoantes200 e gravação contraditória. Causa: catálogo de países e seleção territorial validados separadamente; editor e conferência não aplicavam país. Correção: regra cruzada no servidor; UI retira a hierarquia somente no preenchimento após mudança explícita; rascunho estrangeiro incompleto aceite; conferência não pede campos MZ para estrangeiro. Provas: regressão antes/depois, browserQA21 e SQLREADONLY; tentativa rejeitada não muda payload/versão/auditoria. Dois recursos guardados/recuperados noutra sessão; QA23 consulta/escrita403. Nenhuma conversão de registos históricos.
**C87 (P1, GEO03):** recuperar recurso CaboDelgado/Ancuabe e ler a lista sob o mapa. Esperado limite administrativo/edição/fonte; observado “limites oficiais por verificar” genérico. Causa: lista tinha texto fixo, apesar de popup usar purpose=administrative_boundary. Correção: lista usa o propósito existente e distingue edição administrativa de polígono a comparar; fontes oficiais aceites preservadas. Chromium390px mostrou dois limites e legenda correta, origemINE/2019-06-07. Tiles externos não testados; geometrias/dados/relações não modificados. Provas: territory-resource-map-shot-20261010.json e imagens anteriores preservadas.
C37 já encerrado por confirmação manual somente para autenticação QA18; não reabrir pelo estado histórico dos recibos. A/B e demais critérios independentes continuam abertos.
'''
file = DOC / 'casos-reproducao.md'
file.write_text('<!-- qa-territory-resource-cases-20261010:begin -->\n' + cases + '\n<!-- qa-territory-resource-cases-20261010:end -->\n\n' + file.read_text(encoding='utf-8-sig'), encoding='utf-8')
file = DOC / 'plano-ativacao.md'
activation = f'''## C86/C87 — efeito e reversão, sem instalação nova
Recursos030000, perfis040000 e percurso09/190000 continuam instalados emQA; não repetir. Este lote altera validação cruzada país/hierarquia, preenchimento/conferência e legenda do componente existente; **0 migrations,0 tabelas e0 novas contas**. Dois recursos de contexto criados apenas emQA não serão transferidos ao ativo.
Fingerprint atual `{status['code_hash']}` diverge do backupQA18:19 (`{backup['code_hash']}`); essa prova permanece datada e não habilita a candidata atual. Não repetir testes de C37 fechado. Fixar futura revisão após critérios independentes, conferir/regenerar provas de compatibilidade/backup/restauro correspondentes e aplicar somente instalações ausentes na sequência030000→040000→09/190000 por ação autenticada. Correções, importação cartográfica e publicação continuam operações distintas.
Compatibilidade: schema/IDs/JSON existentes preservados; registos contraditórios antigos permanecem recuperáveis com mensagem e correção explícita, sem migração silenciosa. Reversão do lote: código/build anteriores, conservando drafts/versões/auditoria e sem desfazer schema; dados gravados pelo lote são compatíveis com as tabelas anteriores. Reversão ainda deve ser ensaiada na futura candidata/clone do ativo. Zero escritas ativas; C37 fechado não equivale a ativação ou fechoA/B.
'''
file.write_text('<!-- qa-territory-resource-activation-20261010:begin -->\n' + activation + '\n<!-- qa-territory-resource-activation-20261010:end -->\n\n' + file.read_text(encoding='utf-8-sig'), encoding='utf-8')
(OUT / 'scope-proof.json').write_text(json.dumps({'recorded_at': stamp.isoformat(), 'c37_closed_scoped': True, 'cases': ['C86', 'C87'],
'requirements_touched': sorted(ids), 'current_code_hash': status['code_hash'],
'old_qa_backup_matches_current_code': False, 'active_writes': 0}, indent=2), encoding='utf-8')
print(json.dumps({'prepared_tables': 2, 'requirements_touched': len(ids), 'cases': ['C86', 'C87'], 'c37_closed_scoped': True, 'active_writes': 0}))

Some files were not shown because too many files have changed in this diff Show More